diff --git a/src/androidMain/kotlin/com/liftric/kvault/KVault.kt b/src/androidMain/kotlin/com/liftric/kvault/KVault.kt index 3829ef1..eee06a1 100644 --- a/src/androidMain/kotlin/com/liftric/kvault/KVault.kt +++ b/src/androidMain/kotlin/com/liftric/kvault/KVault.kt @@ -1,236 +1,9 @@ package com.liftric.kvault import android.content.Context -import android.content.SharedPreferences -import android.util.Base64 -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey +import com.liftric.kvault.impl.KVaultImpl -actual open class KVault(context: Context, fileName: String? = null) { - private val encSharedPrefs: SharedPreferences - - init { - val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() - encSharedPrefs = EncryptedSharedPreferences.create( - context, - fileName ?: "secure-shared-preferences", - masterKey, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) - } - - /** - * Saves a string value in the SharedPreferences. - * @param key The key to store - * @param stringValue The value to store - * @return True or false, depending on whether the value has been stored in the SharedPreferences - */ - actual fun set(key: String, stringValue: String): Boolean { - return encSharedPrefs - .edit() - .putString(key, stringValue) - .commit() - } - - /** - * Saves an int value in the SharedPreferences. - * @param key The key to store - * @param intValue The value to store - * @return True or false, depending on whether the value has been stored in the SharedPreferences - */ - actual fun set(key: String, intValue: Int): Boolean { - return encSharedPrefs - .edit() - .putInt(key, intValue) - .commit() - } - - /** - * Saves a long value in the SharedPreferences. - * @param key The key to store - * @param longValue The value to store - * @return True or false, depending on whether the value has been stored in the SharedPreferences - */ - actual fun set(key: String, longValue: Long): Boolean { - return encSharedPrefs - .edit() - .putLong(key, longValue) - .commit() - } - - /** - * Saves a float value in the SharedPreferences. - * @param key The key to store - * @param floatValue The value to store - * @return True or false, depending on whether the value has been stored in the SharedPreferences - */ - actual fun set(key: String, floatValue: Float): Boolean { - return encSharedPrefs - .edit() - .putFloat(key, floatValue) - .commit() - } - - /** - * Saves a double value in the SharedPreferences. - * @param key The key to store - * @param doubleValue The value to store - * @return True or false, depending on whether the value has been stored in the SharedPreferences - */ - actual fun set(key: String, doubleValue: Double): Boolean { - return encSharedPrefs - .edit() - .putLong(key, doubleValue.toRawBits()) - .commit() - } - - /** - * Saves a boolean value in the SharedPreferences. - * @param key The key to store - * @param boolValue The value to store - * @return True or false, depending on whether the value has been stored in the SharedPreferences - */ - actual fun set(key: String, boolValue: Boolean): Boolean { - return encSharedPrefs - .edit() - .putBoolean(key, boolValue) - .commit() - } - - /** - * Saves a byte array value in the store. - * @param key The key to store - * @param dataValue The value to store - */ - actual fun set(key: String, dataValue: ByteArray): Boolean = - encSharedPrefs - .edit() - .putString(key, Base64.encodeToString(dataValue, Base64.DEFAULT)) - .commit() - - /** - * Checks if object with key exists in the SharedPreferences. - * @param forKey The key to query - * @return True or false, depending on whether the value has been stored in the SharedPreferences - */ - actual fun existsObject(forKey: String): Boolean { - return encSharedPrefs.contains(forKey) - } - - /** - * Returns the string value of an object in the SharedPreferences. - * @param forKey The key to query - * @return The stored string value, or null if it is missing - */ - actual fun string(forKey: String): String? { - return encSharedPrefs.getString(forKey, null) - } - - /** - * Returns the int value of an object in the SharedPreferences. - * @param forKey The key to query - * @return The stored int value, or null if it is missing - */ - actual fun int(forKey: String): Int? { - return if (existsObject(forKey)) { - encSharedPrefs.getInt(forKey, Int.MIN_VALUE) - } else { - null - } - } - - /** - * Returns the long value of an object in the SharedPreferences. - * @param forKey The key to query - * @return The stored long value, or null if it is missing - */ - actual fun long(forKey: String): Long? { - return if (existsObject(forKey)) { - encSharedPrefs.getLong(forKey, Long.MIN_VALUE) - } else { - null - } - } - - /** - * Returns the float value of an object in the SharedPreferences. - * @param forKey The key to query - * @return The stored float value, or null if it is missing - */ - actual fun float(forKey: String): Float? { - return if (existsObject(forKey)) { - encSharedPrefs.getFloat(forKey, Float.MIN_VALUE) - } else { - null - } - } - - /** - * Returns the double value of an object in the SharedPreferences. - * @param forKey The key to query - * @return The stored double value, or null if it is missing - */ - actual fun double(forKey: String): Double? { - return if (existsObject(forKey)) { - Double.fromBits(encSharedPrefs.getLong(forKey, Double.MIN_VALUE.toRawBits())) - } else { - null - } - } - - /** - * Returns the boolean value of an object in the SharedPreferences. - * @param forKey The key to query - * @return The stored boolean value, or null if it is missing - */ - actual fun bool(forKey: String): Boolean? { - return if (existsObject(forKey)) { - encSharedPrefs.getBoolean(forKey, false) - } else { - null - } - } - - /** - * Returns the data value of an object in the store. - * @param forKey The key to query - * @return The stored bytes value - */ - actual fun data(forKey: String): ByteArray? { - return encSharedPrefs.getString(forKey, null)?.let { - Base64.decode(it, Base64.DEFAULT) - } - } - - /** - * Returns all keys of the objects in the SharedPreferences. - * @return A list with all keys - */ - actual fun allKeys(): List { - return encSharedPrefs.all.map { it.key } - } - - /** - * Deletes object with the given key from the SharedPreferences. - * @param forKey The key to query - * @return True or false, depending on whether the object has been deleted - */ - actual fun deleteObject(forKey: String): Boolean { - return encSharedPrefs - .edit() - .remove(forKey) - .commit() - } - - /** - * Deletes all objects from the SharedPreferences. - * @return True or false, depending on whether the objects have been deleted - */ - actual fun clear(): Boolean { - return encSharedPrefs - .edit() - .clear() - .commit() - } -} +fun KVault( + context: Context, + fileName: String? = null +): KVault = KVaultImpl(context, fileName) diff --git a/src/androidMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt b/src/androidMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt new file mode 100644 index 0000000..2ffa986 --- /dev/null +++ b/src/androidMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt @@ -0,0 +1,240 @@ +package com.liftric.kvault.impl + +import android.content.Context +import android.content.SharedPreferences +import android.util.Base64 +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.liftric.kvault.KVault + +actual open class KVaultImpl( + context: Context, + fileName: String? = null +): KVault { + private val encSharedPrefs: SharedPreferences + + init { + val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() + encSharedPrefs = EncryptedSharedPreferences.create( + context, + fileName ?: "secure-shared-preferences", + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + + /** + * Saves a string value in the SharedPreferences. + * @param key The key to store + * @param stringValue The value to store + * @return True or false, depending on whether the value has been stored in the SharedPreferences + */ + actual override fun set(key: String, stringValue: String): Boolean { + return encSharedPrefs + .edit() + .putString(key, stringValue) + .commit() + } + + /** + * Saves an int value in the SharedPreferences. + * @param key The key to store + * @param intValue The value to store + * @return True or false, depending on whether the value has been stored in the SharedPreferences + */ + actual override fun set(key: String, intValue: Int): Boolean { + return encSharedPrefs + .edit() + .putInt(key, intValue) + .commit() + } + + /** + * Saves a long value in the SharedPreferences. + * @param key The key to store + * @param longValue The value to store + * @return True or false, depending on whether the value has been stored in the SharedPreferences + */ + actual override fun set(key: String, longValue: Long): Boolean { + return encSharedPrefs + .edit() + .putLong(key, longValue) + .commit() + } + + /** + * Saves a float value in the SharedPreferences. + * @param key The key to store + * @param floatValue The value to store + * @return True or false, depending on whether the value has been stored in the SharedPreferences + */ + actual override fun set(key: String, floatValue: Float): Boolean { + return encSharedPrefs + .edit() + .putFloat(key, floatValue) + .commit() + } + + /** + * Saves a double value in the SharedPreferences. + * @param key The key to store + * @param doubleValue The value to store + * @return True or false, depending on whether the value has been stored in the SharedPreferences + */ + actual override fun set(key: String, doubleValue: Double): Boolean { + return encSharedPrefs + .edit() + .putLong(key, doubleValue.toRawBits()) + .commit() + } + + /** + * Saves a boolean value in the SharedPreferences. + * @param key The key to store + * @param boolValue The value to store + * @return True or false, depending on whether the value has been stored in the SharedPreferences + */ + actual override fun set(key: String, boolValue: Boolean): Boolean { + return encSharedPrefs + .edit() + .putBoolean(key, boolValue) + .commit() + } + + /** + * Saves a byte array value in the store. + * @param key The key to store + * @param dataValue The value to store + */ + actual override fun set(key: String, dataValue: ByteArray): Boolean = + encSharedPrefs + .edit() + .putString(key, Base64.encodeToString(dataValue, Base64.DEFAULT)) + .commit() + + /** + * Checks if object with key exists in the SharedPreferences. + * @param forKey The key to query + * @return True or false, depending on whether the value has been stored in the SharedPreferences + */ + actual override fun existsObject(forKey: String): Boolean { + return encSharedPrefs.contains(forKey) + } + + /** + * Returns the string value of an object in the SharedPreferences. + * @param forKey The key to query + * @return The stored string value, or null if it is missing + */ + actual override fun string(forKey: String): String? { + return encSharedPrefs.getString(forKey, null) + } + + /** + * Returns the int value of an object in the SharedPreferences. + * @param forKey The key to query + * @return The stored int value, or null if it is missing + */ + actual override fun int(forKey: String): Int? { + return if (existsObject(forKey)) { + encSharedPrefs.getInt(forKey, Int.MIN_VALUE) + } else { + null + } + } + + /** + * Returns the long value of an object in the SharedPreferences. + * @param forKey The key to query + * @return The stored long value, or null if it is missing + */ + actual override fun long(forKey: String): Long? { + return if (existsObject(forKey)) { + encSharedPrefs.getLong(forKey, Long.MIN_VALUE) + } else { + null + } + } + + /** + * Returns the float value of an object in the SharedPreferences. + * @param forKey The key to query + * @return The stored float value, or null if it is missing + */ + actual override fun float(forKey: String): Float? { + return if (existsObject(forKey)) { + encSharedPrefs.getFloat(forKey, Float.MIN_VALUE) + } else { + null + } + } + + /** + * Returns the double value of an object in the SharedPreferences. + * @param forKey The key to query + * @return The stored double value, or null if it is missing + */ + actual override fun double(forKey: String): Double? { + return if (existsObject(forKey)) { + Double.fromBits(encSharedPrefs.getLong(forKey, Double.MIN_VALUE.toRawBits())) + } else { + null + } + } + + /** + * Returns the boolean value of an object in the SharedPreferences. + * @param forKey The key to query + * @return The stored boolean value, or null if it is missing + */ + actual override fun bool(forKey: String): Boolean? { + return if (existsObject(forKey)) { + encSharedPrefs.getBoolean(forKey, false) + } else { + null + } + } + + /** + * Returns the data value of an object in the store. + * @param forKey The key to query + * @return The stored bytes value + */ + actual override fun data(forKey: String): ByteArray? { + return encSharedPrefs.getString(forKey, null)?.let { + Base64.decode(it, Base64.DEFAULT) + } + } + + /** + * Returns all keys of the objects in the SharedPreferences. + * @return A list with all keys + */ + actual override fun allKeys(): List { + return encSharedPrefs.all.map { it.key } + } + + /** + * Deletes object with the given key from the SharedPreferences. + * @param forKey The key to query + * @return True or false, depending on whether the object has been deleted + */ + actual override fun deleteObject(forKey: String): Boolean { + return encSharedPrefs + .edit() + .remove(forKey) + .commit() + } + + /** + * Deletes all objects from the SharedPreferences. + * @return True or false, depending on whether the objects have been deleted + */ + actual override fun clear(): Boolean { + return encSharedPrefs + .edit() + .clear() + .commit() + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/com/liftric/kvault/KVault.kt b/src/commonMain/kotlin/com/liftric/kvault/KVault.kt index f7cba8a..7df1a5f 100644 --- a/src/commonMain/kotlin/com/liftric/kvault/KVault.kt +++ b/src/commonMain/kotlin/com/liftric/kvault/KVault.kt @@ -1,6 +1,7 @@ package com.liftric.kvault -expect open class KVault { + +interface KVault { /** * Saves a string value in the store. * @param key The key to store @@ -125,3 +126,4 @@ expect open class KVault { */ fun clear(): Boolean } + diff --git a/src/commonMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt b/src/commonMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt new file mode 100644 index 0000000..384e71b --- /dev/null +++ b/src/commonMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt @@ -0,0 +1,129 @@ +package com.liftric.kvault.impl + +import com.liftric.kvault.KVault + +expect open class KVaultImpl: KVault { + /** + * Saves a string value in the store. + * @param key The key to store + * @param stringValue The value to store + */ + override fun set(key: String, stringValue: String): Boolean + + /** + * Saves an int value in the store. + * @param key The key to store + * @param intValue The value to store + */ + override fun set(key: String, intValue: Int): Boolean + + /** + * Saves a long value in the store. + * @param key The key to store + * @param longValue The value to store + */ + override fun set(key: String, longValue: Long): Boolean + + /** + * Saves a float value in the store. + * @param key The key to store + * @param floatValue The value to store + */ + override fun set(key: String, floatValue: Float): Boolean + + /** + * Saves a double value in the store. + * @param key The key to store + * @param doubleValue The value to store + */ + override fun set(key: String, doubleValue: Double): Boolean + + /** + * Saves a boolean value in the store. + * @param key The key to store + * @param boolValue The value to store + */ + override fun set(key: String, boolValue: Boolean): Boolean + + /** + * Saves a byte array value in the store. + * @param key The key to store + * @param dataValue The value to store + */ + override fun set(key: String, dataValue: ByteArray): Boolean + + /** + * Checks if object with key exists in the store. + * @param forKey The key to query + * @return True or false, depending on wether it is in the store or not + */ + override fun existsObject(forKey: String): Boolean + + /** + * Returns the string value of an object in the store. + * @param forKey The key to query + * @return The stored string value + */ + override fun string(forKey: String): String? + + /** + * Returns the int value of an object in the store. + * @param forKey The key to query + * @return The stored int value + */ + override fun int(forKey: String): Int? + + /** + * Returns the long value of an object in the store. + * @param forKey The key to query + * @return The stored long value + */ + override fun long(forKey: String): Long? + + /** + * Returns the double value of an object in the store. + * @param forKey The key to query + * @return The stored double lue + */ + override fun double(forKey: String): Double? + + /** + * Returns the float value of an object in the store. + * @param forKey The key to query + * @return The stored float value + */ + override fun float(forKey: String): Float? + + /** + * Returns the boolean value of an object in the store. + * @param forKey The key to query + * @return The stored boolean value + */ + override fun bool(forKey: String): Boolean? + + /** + * Returns the data value of an object in the store. + * @param forKey The key to query + * @return The stored bytes value + */ + override fun data(forKey: String): ByteArray? + + /** + * Returns all keys of the stored objects. + * @return A list with all keys + */ + override fun allKeys(): List + + /** + * Deletes object with the given key from the store. + * @param forKey The key to query + * @return True or false, depending on whether the object has been deleted + */ + override fun deleteObject(forKey: String): Boolean + + /** + * Deletes all objects from the store. + * @return True or false, depending on whether the objects have been deleted + */ + override fun clear(): Boolean +} \ No newline at end of file diff --git a/src/iosMain/kotlin/com/liftric/kvault/KVault.kt b/src/iosMain/kotlin/com/liftric/kvault/KVault.kt index c18defd..b78b1c8 100644 --- a/src/iosMain/kotlin/com/liftric/kvault/KVault.kt +++ b/src/iosMain/kotlin/com/liftric/kvault/KVault.kt @@ -1,348 +1,9 @@ package com.liftric.kvault -import kotlinx.cinterop.* -import platform.CoreFoundation.* -import platform.Foundation.* -import platform.Security.* -import platform.darwin.OSStatus -import platform.darwin.noErr -import platform.posix.memcpy +import com.liftric.kvault.impl.KVaultImpl -/** - * Keychain wrapper. - * Note: Using the deprecated init() the service name was the apps bundle identifier or if null "com.liftric.KVault". - * - * @param serviceName Name of the service. Used to categories entries. - * @param accessGroup Name of the access group. Used to share entries between apps. - * @param accessibility Level of the accessibility for the Keychain instance. - * @constructor Initiates a Keychain with the given parameters. - */ -actual open class KVault( - val serviceName: String? = null, - val accessGroup: String? = null, - val accessibility: Accessible = Accessible.WhenUnlocked -) { - /** - * kSecAttrAccessible attributes wrapper. - * attribute enables you to control item availability relative to the lock state of the device. - * It also lets you specify eligibility for restoration to a new device. - * If the attribute ends with the string ThisDeviceOnly, the item can be restored to the same device - * that created a backup, but it isn’t migrated when restoring another device’s backup data. - */ - enum class Accessible(val value: CFStringRef?) { - WhenPasscodeSetThisDeviceOnly(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), - WhenUnlockedThisDeviceOnly(kSecAttrAccessibleWhenUnlockedThisDeviceOnly), - WhenUnlocked(kSecAttrAccessibleWhenUnlocked), - AfterFirstUnlock(kSecAttrAccessibleAfterFirstUnlock), - AfterFirstUnlockThisDeviceOnly(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) - } - - /** - * Saves a string value in the Keychain. - * @param key The key to store - * @param stringValue The value to store - * @return True or false, depending on whether the value has been stored in the Keychain - */ - actual fun set(key: String, stringValue: String): Boolean { - return addOrUpdate(key, stringValue.toNSData()) - } - - /** - * Saves an int value in the Keychain. - * @param key The key to store - * @param intValue The value to store - * @return True or false, depending on whether the value has been stored in the Keychain - */ - actual fun set(key: String, intValue: Int): Boolean { - return addOrUpdate(key, NSNumber(int = intValue).toNSData()) - } - - /** - * Saves a long value in the Keychain. - * @param key The key to store - * @param longValue The value to store - * @return True or false, depending on whether the value has been stored in the Keychain - */ - actual fun set(key: String, longValue: Long): Boolean { - return addOrUpdate(key, NSNumber(long = longValue).toNSData()) - } - - /** - * Saves a float value in the Keychain. - * @param key The key to store - * @param floatValue The value to store - * @return True or false, depending on whether the value has been stored in the Keychain - */ - actual fun set(key: String, floatValue: Float): Boolean { - return addOrUpdate(key, NSNumber(float = floatValue).toNSData()) - } - - /** - * Saves a double value in the Keychain. - * @param key The key to store - * @param doubleValue The value to store - * @return True or false, depending on whether the value has been stored in the Keychain - */ - actual fun set(key: String, doubleValue: Double): Boolean { - return addOrUpdate(key, NSNumber(double = doubleValue).toNSData()) - } - - /** - * Saves a boolean value in the Keychain. - * @param key The key to store - * @param boolValue The value to store - * @return True or false, depending on whether the value has been stored in the Keychain - */ - actual fun set(key: String, boolValue: Boolean): Boolean { - return addOrUpdate(key, NSNumber(bool = boolValue).toNSData()) - } - - /** - * Saves a byte array value in the store. - * @param key The key to store - * @param dataValue The value to store - */ - actual fun set(key: String, dataValue: ByteArray): Boolean { - return addOrUpdate(key, dataValue.toNSData()) - } - - /** - * Returns the string value of an object in the Keychain. - * @param forKey The key to query - * @return The stored string value, or null if it is missing - */ - actual fun string(forKey: String): String? { - return value(forKey)?.stringValue - } - - /** - * Returns the int value of an object in the Keychain. - * @param forKey The key to query - * @return The stored int value, or null if it is missing - */ - actual fun int(forKey: String): Int? { - return value(forKey)?.toNSNumber()?.intValue - } - - /** - * Returns the long value of an object in the Keychain. - * @param forKey The key to query - * @return The stored long value, or null if it is missing - */ - actual fun long(forKey: String): Long? { - return value(forKey)?.toNSNumber()?.longValue - } - - /** - * Returns the float value of an object in the Keychain. - * @param forKey The key to query - * @return The stored float value, or null if it is missing - */ - actual fun float(forKey: String): Float? { - return value(forKey)?.toNSNumber()?.floatValue - } - - /** - * Returns the double value of an object in the Keychain. - * @param forKey The key to query - * @return The stored double value, or null if it is missing - */ - actual fun double(forKey: String): Double? { - return value(forKey)?.toNSNumber()?.doubleValue - } - - /** - * Returns the boolean value of an object in the Keychain. - * @param forKey The key to query - * @return The stored boolean value, or null if it is missing - */ - actual fun bool(forKey: String): Boolean? { - return value(forKey)?.toNSNumber()?.boolValue - } - - /** - * Returns the data value of an object in the store. - * @param forKey The key to query - * @return The stored bytes value - */ - actual fun data(forKey: String): ByteArray? { - return value(forKey)?.toByteArray() - } - - /** - * Returns all keys of the objects in the Keychain. - * @return A list with all keys - */ - @Suppress("UNCHECKED_CAST") - actual fun allKeys(): List = context { - val query = query( - kSecClass to kSecClassGenericPassword, - kSecReturnAttributes to kCFBooleanTrue, - kSecMatchLimit to kSecMatchLimitAll - ) - - memScoped { - val result = alloc() - val isValid = SecItemCopyMatching(query, result.ptr).validate() - if (isValid) { - val items = CFBridgingRelease(result.value) as? List> - items?.mapNotNull { it["acct"] as? String } ?: listOf() - } else { - listOf() - } - } - } - - /** - * Checks if object with the given key exists in the Keychain. - * @param forKey The key to query - * @return True or false, depending on whether it is in the Keychain or not - */ - actual fun existsObject(forKey: String): Boolean = context(forKey) { (account) -> - val query = query( - kSecClass to kSecClassGenericPassword, - kSecAttrAccount to account, - kSecReturnData to kCFBooleanFalse, - ) - - SecItemCopyMatching(query, null) - .validate() - } - - /** - * Deletes object with the given key from the Keychain. - * @param forKey The key to query - * @return True or false, depending on whether the object has been deleted - */ - actual fun deleteObject(forKey: String): Boolean = context(forKey) { (account) -> - val query = query( - kSecClass to kSecClassGenericPassword, - kSecAttrAccount to account - ) - - SecItemDelete(query) - .validate() - } - - /** - * Deletes all objects. - * If the service name and/or the access group are null, all items in the apps' - * Keychain will be deleted. - * @return True or false, depending on whether the objects have been deleted - */ - actual fun clear(): Boolean = context { - val query = query( - kSecClass to kSecClassGenericPassword - ) - - SecItemDelete(query) - .validate() - } - - // =============== - // PRIVATE METHODS - // =============== - - private fun addOrUpdate(key: String, value: NSData?): Boolean { - return if (existsObject(key)) { - update(key, value) - } else { - add(key, value) - } - } - - private fun add(key: String, value: NSData?): Boolean = context(key, value) { (account, data) -> - val query = query( - kSecClass to kSecClassGenericPassword, - kSecAttrAccount to account, - kSecValueData to data, - kSecAttrAccessible to accessibility.value - ) - SecItemAdd(query, null) - .validate() - - } - - private fun update(key: String, value: Any?): Boolean = context(key, value) { (account, data) -> - val query = query( - kSecClass to kSecClassGenericPassword, - kSecAttrAccount to account, - kSecReturnData to kCFBooleanFalse, - ) - - val updateQuery = query( - kSecValueData to data - ) - - SecItemUpdate(query, updateQuery) - .validate() - } - - private fun value(forKey: String): NSData? = context(forKey) { (account) -> - val query = query( - kSecClass to kSecClassGenericPassword, - kSecAttrAccount to account, - kSecReturnData to kCFBooleanTrue, - kSecMatchLimit to kSecMatchLimitOne, - ) - - memScoped { - val result = alloc() - SecItemCopyMatching(query, result.ptr) - CFBridgingRelease(result.value) as? NSData - } - } - - // ======== - // HELPERS - // ======== - - private class Context(val refs: Map) { - fun query(vararg pairs: Pair): CFDictionaryRef? { - val map = mapOf(*pairs).plus(refs.filter { it.value != null }) - return CFDictionaryCreateMutable( - null, map.size.convert(), null, null - ).apply { - map.entries.forEach { CFDictionaryAddValue(this, it.key, it.value) } - }.apply { - CFAutorelease(this) - } - } - } - - private fun context(vararg values: Any?, block: Context.(List) -> T): T { - val standard = mapOf( - kSecAttrService to CFBridgingRetain(serviceName), - kSecAttrAccessGroup to CFBridgingRetain(accessGroup) - ) - val custom = arrayOf(*values).map { CFBridgingRetain(it) } - return block.invoke(Context(standard), custom).apply { - standard.values.plus(custom).forEach { CFBridgingRelease(it) } - } - } - - private fun String.toNSData(): NSData? = - NSString.create(string = this).dataUsingEncoding(NSUTF8StringEncoding) - - private fun NSNumber.toNSData() = NSKeyedArchiver.archivedDataWithRootObject(this) - private fun NSData.toNSNumber() = NSKeyedUnarchiver.unarchiveObjectWithData(this) as? NSNumber - - private val NSData.stringValue: String? - get() = NSString.create(this, NSUTF8StringEncoding) as String? - - private fun NSData.toByteArray(): ByteArray = - ByteArray(length.toInt()).apply { - if (isNotEmpty()) { - usePinned { - memcpy(it.addressOf(0), this@toByteArray.bytes, this@toByteArray.length) - } - } - } - - private fun ByteArray.toNSData(): NSData = - memScoped { - NSData.create(bytes = allocArrayOf(this@toNSData), length = this@toNSData.size.convert()) - } - - private fun OSStatus.validate(): Boolean = toUInt() == noErr -} +fun KVault( + serviceName: String? = null, + accessGroup: String? = null, + accessibility: KVaultImpl.Accessible = KVaultImpl.Accessible.WhenUnlocked +): KVault = KVaultImpl(serviceName, accessGroup, accessibility) diff --git a/src/iosMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt b/src/iosMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt new file mode 100644 index 0000000..392d431 --- /dev/null +++ b/src/iosMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt @@ -0,0 +1,393 @@ +package com.liftric.kvault.impl + +import com.liftric.kvault.KVault +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.alloc +import kotlinx.cinterop.allocArrayOf +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.usePinned +import kotlinx.cinterop.value +import platform.CoreFoundation.CFAutorelease +import platform.CoreFoundation.CFDictionaryAddValue +import platform.CoreFoundation.CFDictionaryCreateMutable +import platform.CoreFoundation.CFDictionaryRef +import platform.CoreFoundation.CFStringRef +import platform.CoreFoundation.CFTypeRef +import platform.CoreFoundation.CFTypeRefVar +import platform.CoreFoundation.kCFBooleanFalse +import platform.CoreFoundation.kCFBooleanTrue +import platform.Foundation.CFBridgingRelease +import platform.Foundation.CFBridgingRetain +import platform.Foundation.NSData +import platform.Foundation.NSKeyedArchiver +import platform.Foundation.NSKeyedUnarchiver +import platform.Foundation.NSNumber +import platform.Foundation.NSString +import platform.Foundation.NSUTF8StringEncoding +import platform.Foundation.create +import platform.Foundation.dataUsingEncoding +import platform.Security.SecItemAdd +import platform.Security.SecItemCopyMatching +import platform.Security.SecItemDelete +import platform.Security.SecItemUpdate +import platform.Security.kSecAttrAccessGroup +import platform.Security.kSecAttrAccessible +import platform.Security.kSecAttrAccessibleAfterFirstUnlock +import platform.Security.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly +import platform.Security.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly +import platform.Security.kSecAttrAccessibleWhenUnlocked +import platform.Security.kSecAttrAccessibleWhenUnlockedThisDeviceOnly +import platform.Security.kSecAttrAccount +import platform.Security.kSecAttrService +import platform.Security.kSecClass +import platform.Security.kSecClassGenericPassword +import platform.Security.kSecMatchLimit +import platform.Security.kSecMatchLimitAll +import platform.Security.kSecMatchLimitOne +import platform.Security.kSecReturnAttributes +import platform.Security.kSecReturnData +import platform.Security.kSecValueData +import platform.darwin.OSStatus +import platform.darwin.noErr +import platform.posix.memcpy + +/** + * Keychain wrapper. + * Note: Using the deprecated init() the service name was the apps bundle identifier or if null "com.liftric.KVault". + * + * @param serviceName Name of the service. Used to categories entries. + * @param accessGroup Name of the access group. Used to share entries between apps. + * @param accessibility Level of the accessibility for the Keychain instance. + * @constructor Initiates a Keychain with the given parameters. + */ +actual open class KVaultImpl( + private val serviceName: String? = null, + private val accessGroup: String? = null, + private val accessibility: Accessible = Accessible.WhenUnlocked +): KVault { + /** + * kSecAttrAccessible attributes wrapper. + * attribute enables you to control item availability relative to the lock state of the device. + * It also lets you specify eligibility for restoration to a new device. + * If the attribute ends with the string ThisDeviceOnly, the item can be restored to the same device + * that created a backup, but it isn’t migrated when restoring another device’s backup data. + */ + enum class Accessible(val value: CFStringRef?) { + WhenPasscodeSetThisDeviceOnly(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), + WhenUnlockedThisDeviceOnly(kSecAttrAccessibleWhenUnlockedThisDeviceOnly), + WhenUnlocked(kSecAttrAccessibleWhenUnlocked), + AfterFirstUnlock(kSecAttrAccessibleAfterFirstUnlock), + AfterFirstUnlockThisDeviceOnly(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) + } + + /** + * Saves a string value in the Keychain. + * @param key The key to store + * @param stringValue The value to store + * @return True or false, depending on whether the value has been stored in the Keychain + */ + actual override fun set(key: String, stringValue: String): Boolean { + return addOrUpdate(key, stringValue.toNSData()) + } + + /** + * Saves an int value in the Keychain. + * @param key The key to store + * @param intValue The value to store + * @return True or false, depending on whether the value has been stored in the Keychain + */ + actual override fun set(key: String, intValue: Int): Boolean { + return addOrUpdate(key, NSNumber(int = intValue).toNSData()) + } + + /** + * Saves a long value in the Keychain. + * @param key The key to store + * @param longValue The value to store + * @return True or false, depending on whether the value has been stored in the Keychain + */ + actual override fun set(key: String, longValue: Long): Boolean { + return addOrUpdate(key, NSNumber(long = longValue).toNSData()) + } + + /** + * Saves a float value in the Keychain. + * @param key The key to store + * @param floatValue The value to store + * @return True or false, depending on whether the value has been stored in the Keychain + */ + actual override fun set(key: String, floatValue: Float): Boolean { + return addOrUpdate(key, NSNumber(float = floatValue).toNSData()) + } + + /** + * Saves a double value in the Keychain. + * @param key The key to store + * @param doubleValue The value to store + * @return True or false, depending on whether the value has been stored in the Keychain + */ + actual override fun set(key: String, doubleValue: Double): Boolean { + return addOrUpdate(key, NSNumber(double = doubleValue).toNSData()) + } + + /** + * Saves a boolean value in the Keychain. + * @param key The key to store + * @param boolValue The value to store + * @return True or false, depending on whether the value has been stored in the Keychain + */ + actual override fun set(key: String, boolValue: Boolean): Boolean { + return addOrUpdate(key, NSNumber(bool = boolValue).toNSData()) + } + + /** + * Saves a byte array value in the store. + * @param key The key to store + * @param dataValue The value to store + */ + actual override fun set(key: String, dataValue: ByteArray): Boolean { + return addOrUpdate(key, dataValue.toNSData()) + } + + /** + * Returns the string value of an object in the Keychain. + * @param forKey The key to query + * @return The stored string value, or null if it is missing + */ + actual override fun string(forKey: String): String? { + return value(forKey)?.stringValue + } + + /** + * Returns the int value of an object in the Keychain. + * @param forKey The key to query + * @return The stored int value, or null if it is missing + */ + actual override fun int(forKey: String): Int? { + return value(forKey)?.toNSNumber()?.intValue + } + + /** + * Returns the long value of an object in the Keychain. + * @param forKey The key to query + * @return The stored long value, or null if it is missing + */ + actual override fun long(forKey: String): Long? { + return value(forKey)?.toNSNumber()?.longValue + } + + /** + * Returns the float value of an object in the Keychain. + * @param forKey The key to query + * @return The stored float value, or null if it is missing + */ + actual override fun float(forKey: String): Float? { + return value(forKey)?.toNSNumber()?.floatValue + } + + /** + * Returns the double value of an object in the Keychain. + * @param forKey The key to query + * @return The stored double value, or null if it is missing + */ + actual override fun double(forKey: String): Double? { + return value(forKey)?.toNSNumber()?.doubleValue + } + + /** + * Returns the boolean value of an object in the Keychain. + * @param forKey The key to query + * @return The stored boolean value, or null if it is missing + */ + actual override fun bool(forKey: String): Boolean? { + return value(forKey)?.toNSNumber()?.boolValue + } + + /** + * Returns the data value of an object in the store. + * @param forKey The key to query + * @return The stored bytes value + */ + actual override fun data(forKey: String): ByteArray? { + return value(forKey)?.toByteArray() + } + + /** + * Returns all keys of the objects in the Keychain. + * @return A list with all keys + */ + @Suppress("UNCHECKED_CAST") + actual override fun allKeys(): List = context { + val query = query( + kSecClass to kSecClassGenericPassword, + kSecReturnAttributes to kCFBooleanTrue, + kSecMatchLimit to kSecMatchLimitAll + ) + + memScoped { + val result = alloc() + val isValid = SecItemCopyMatching(query, result.ptr).validate() + if (isValid) { + val items = CFBridgingRelease(result.value) as? List> + items?.mapNotNull { it["acct"] as? String } ?: listOf() + } else { + listOf() + } + } + } + + /** + * Checks if object with the given key exists in the Keychain. + * @param forKey The key to query + * @return True or false, depending on whether it is in the Keychain or not + */ + actual override fun existsObject(forKey: String): Boolean = context(forKey) { (account) -> + val query = query( + kSecClass to kSecClassGenericPassword, + kSecAttrAccount to account, + kSecReturnData to kCFBooleanFalse, + ) + + SecItemCopyMatching(query, null) + .validate() + } + + /** + * Deletes object with the given key from the Keychain. + * @param forKey The key to query + * @return True or false, depending on whether the object has been deleted + */ + actual override fun deleteObject(forKey: String): Boolean = context(forKey) { (account) -> + val query = query( + kSecClass to kSecClassGenericPassword, + kSecAttrAccount to account + ) + + SecItemDelete(query) + .validate() + } + + /** + * Deletes all objects. + * If the service name and/or the access group are null, all items in the apps' + * Keychain will be deleted. + * @return True or false, depending on whether the objects have been deleted + */ + actual override fun clear(): Boolean = context { + val query = query( + kSecClass to kSecClassGenericPassword + ) + + SecItemDelete(query) + .validate() + } + + // =============== + // PRIVATE METHODS + // =============== + + private fun addOrUpdate(key: String, value: NSData?): Boolean { + return if (existsObject(key)) { + update(key, value) + } else { + add(key, value) + } + } + + private fun add(key: String, value: NSData?): Boolean = context(key, value) { (account, data) -> + val query = query( + kSecClass to kSecClassGenericPassword, + kSecAttrAccount to account, + kSecValueData to data, + kSecAttrAccessible to accessibility.value + ) + SecItemAdd(query, null) + .validate() + + } + + private fun update(key: String, value: Any?): Boolean = context(key, value) { (account, data) -> + val query = query( + kSecClass to kSecClassGenericPassword, + kSecAttrAccount to account, + kSecReturnData to kCFBooleanFalse, + ) + + val updateQuery = query( + kSecValueData to data + ) + + SecItemUpdate(query, updateQuery) + .validate() + } + + private fun value(forKey: String): NSData? = context(forKey) { (account) -> + val query = query( + kSecClass to kSecClassGenericPassword, + kSecAttrAccount to account, + kSecReturnData to kCFBooleanTrue, + kSecMatchLimit to kSecMatchLimitOne, + ) + + memScoped { + val result = alloc() + SecItemCopyMatching(query, result.ptr) + CFBridgingRelease(result.value) as? NSData + } + } + + // ======== + // HELPERS + // ======== + + private class Context(val refs: Map) { + fun query(vararg pairs: Pair): CFDictionaryRef? { + val map = mapOf(*pairs).plus(refs.filter { it.value != null }) + return CFDictionaryCreateMutable( + null, map.size.convert(), null, null + ).apply { + map.entries.forEach { CFDictionaryAddValue(this, it.key, it.value) } + }.apply { + CFAutorelease(this) + } + } + } + + private fun context(vararg values: Any?, block: Context.(List) -> T): T { + val standard = mapOf( + kSecAttrService to CFBridgingRetain(serviceName), + kSecAttrAccessGroup to CFBridgingRetain(accessGroup) + ) + val custom = arrayOf(*values).map { CFBridgingRetain(it) } + return block.invoke(Context(standard), custom).apply { + standard.values.plus(custom).forEach { CFBridgingRelease(it) } + } + } + + private fun String.toNSData(): NSData? = + NSString.create(string = this).dataUsingEncoding(NSUTF8StringEncoding) + + private fun NSNumber.toNSData() = NSKeyedArchiver.archivedDataWithRootObject(this) + private fun NSData.toNSNumber() = NSKeyedUnarchiver.unarchiveObjectWithData(this) as? NSNumber + + private val NSData.stringValue: String? + get() = NSString.create(this, NSUTF8StringEncoding) as String? + + private fun NSData.toByteArray(): ByteArray = + ByteArray(length.toInt()).apply { + if (isNotEmpty()) { + usePinned { + memcpy(it.addressOf(0), this@toByteArray.bytes, this@toByteArray.length) + } + } + } + + private fun ByteArray.toNSData(): NSData = + memScoped { + NSData.create(bytes = allocArrayOf(this@toNSData), length = this@toNSData.size.convert()) + } + + private fun OSStatus.validate(): Boolean = toUInt() == noErr +} \ No newline at end of file