From 51a13844f4b09076f8ecef04a2686441ab8e2b88 Mon Sep 17 00:00:00 2001 From: Abe White Date: Wed, 22 Jan 2025 11:58:28 -0600 Subject: [PATCH] Add code and instructions to work around EncryptedSharedPreferences crash --- README.md | 17 ++++++- Sources/SkipKeychain/SkipKeychain.swift | 61 +++++++++++++++---------- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 678c6f9..207a161 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ This is a [Skip](https://skip.tools) Swift/Kotlin library project providing a simple unified API to secure key/value storage. It uses the Keychain on Darwin platforms and EncyptedSharedPreferences on Android. - ## Usage ```swift @@ -17,6 +16,22 @@ try keychain.removeValue(forKey: "key") assert(keychain.string(forKey: "key") == nil) ``` +## Backups + +Google recommends excluding encrypted shared preference files from backups to prevent restoring an encrypted file whose encryption key is lost. Follow the instructions [here](https://developer.android.com/identity/data/autobackup#include-exclude-android-12) to create backup rules for your app. Your rules should contain the following to exclude the SkipKeychain shared preferences file: + +```xml + + + + + + + + + +``` + ## Building This project is a Swift Package Manager module that uses the diff --git a/Sources/SkipKeychain/SkipKeychain.swift b/Sources/SkipKeychain/SkipKeychain.swift index 218ae30..dbc3ce2 100644 --- a/Sources/SkipKeychain/SkipKeychain.swift +++ b/Sources/SkipKeychain/SkipKeychain.swift @@ -151,7 +151,18 @@ public struct Keychain { let context = ProcessInfo.processInfo.androidContext let alias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) - preferences = EncryptedSharedPreferences.create("tools.skip.SkipKeychain", alias, context, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM) + let fileName = "tools.skip.SkipKeychain" // DO NOT CHANGE: Users may specify this name in their Android backup rules + let factory: () -> SharedPreferences = { + EncryptedSharedPreferences.create(fileName, alias, context, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM) + } + do { + preferences = factory() + } catch is javax.crypto.AEADBadTagException { + // Likely caused by restoring a backup where the key is no longer available: https://github.com/tink-crypto/tink-java/issues/23 + // Delete the old file and try again + do { context.deleteSharedPreferences(fileName) } catch {} + preferences = factory() + } return preferences! } #endif @@ -189,30 +200,34 @@ public struct Keychain { /// Return the set of all stored keys. public func keys() throws -> [String] { - #if !SKIP - lock.lock() - defer { lock.unlock() } + #if !SKIP + lock.lock() + defer { lock.unlock() } - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecReturnAttributes as String: true, - kSecReturnRef as String: true, - kSecMatchLimit as String: kSecMatchLimitAll, - ] - var result: AnyObject? - let code = withUnsafeMutablePointer(to: &result) { - SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) - } - guard code == errSecSuccess || code == errSecItemNotFound else { - throw KeychainError(code: code) - } - guard let dicts = result as? [[String: Any]] else { - return [] - } - return dicts.compactMap { $0[kSecAttrAccount as String] as? String } - #else + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecReturnAttributes as String: true, + kSecReturnRef as String: true, + kSecMatchLimit as String: kSecMatchLimitAll, + ] + var result: AnyObject? + let code = withUnsafeMutablePointer(to: &result) { + SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) + } + guard code == errSecSuccess || code == errSecItemNotFound else { + throw KeychainError(code: code) + } + guard let dicts = result as? [[String: Any]] else { + return [] + } + return dicts.compactMap { $0[kSecAttrAccount as String] as? String } + #else + do { return Array(initializePreferences().getAll().keys) - #endif + } catch { + throw KeychainError(message: error.localizedDescription) + } + #endif } /// Remove all stored key value pairs.