From b7b4e7cb4d88b8213fd1b3e4b9a104889d8fdfe0 Mon Sep 17 00:00:00 2001 From: Jacob Rhoda Date: Thu, 6 Jul 2023 14:26:34 -0400 Subject: [PATCH] Refactor API to use an interface. Interfaces allow mocking libraries to mock our types more easily. This will prevent users of KVault from having to wrap KVault with another type when they want to test code which uses it. --- .../kotlin/com/liftric/kvault/KVault.kt | 214 +---------- .../com/liftric/kvault/impl/KVaultImpl.kt | 217 +++++++++++ .../kotlin/com/liftric/kvault/KVault.kt | 4 +- .../com/liftric/kvault/impl/KVaultImpl.kt | 115 ++++++ .../kotlin/com/liftric/kvault/KVault.kt | 318 +--------------- .../com/liftric/kvault/impl/KVaultImpl.kt | 357 ++++++++++++++++++ 6 files changed, 703 insertions(+), 522 deletions(-) create mode 100644 src/androidMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt create mode 100644 src/commonMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt create mode 100644 src/iosMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt diff --git a/src/androidMain/kotlin/com/liftric/kvault/KVault.kt b/src/androidMain/kotlin/com/liftric/kvault/KVault.kt index 10acdda..00115e2 100644 --- a/src/androidMain/kotlin/com/liftric/kvault/KVault.kt +++ b/src/androidMain/kotlin/com/liftric/kvault/KVault.kt @@ -1,213 +1,9 @@ package com.liftric.kvault import android.content.Context -import android.content.SharedPreferences -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() - } - - /** - * 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 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) \ No newline at end of file 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..d9f1e79 --- /dev/null +++ b/src/androidMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt @@ -0,0 +1,217 @@ +package com.liftric.kvault.impl + +import android.content.Context +import android.content.SharedPreferences +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() + } + + /** + * 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 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 fef7a2d..6f45b2a 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 @@ -111,3 +112,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..e226273 --- /dev/null +++ b/src/commonMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt @@ -0,0 +1,115 @@ +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 + + /** + * 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 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 721895c..cdb1df5 100644 --- a/src/iosMain/kotlin/com/liftric/kvault/KVault.kt +++ b/src/iosMain/kotlin/com/liftric/kvault/KVault.kt @@ -1,315 +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 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()) - } - - /** - * 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 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 OSStatus.validate(): Boolean = toUInt() == noErr -} +fun KVault( + serviceName: String? = null, + accessGroup: String? = null, + accessibility: KVaultImpl.Accessible = KVaultImpl.Accessible.WhenUnlocked +): KVault = KVaultImpl(serviceName, accessGroup, accessibility) \ No newline at end of file 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..81e3331 --- /dev/null +++ b/src/iosMain/kotlin/com/liftric/kvault/impl/KVaultImpl.kt @@ -0,0 +1,357 @@ +package com.liftric.kvault.impl + +import com.liftric.kvault.KVault +import kotlinx.cinterop.alloc +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +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 + +/** + * 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()) + } + + /** + * 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 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 OSStatus.validate(): Boolean = toUInt() == noErr +} \ No newline at end of file