Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude domain="sharedpref" path="tools.skip.SkipKeychain.xml"/>
</cloud-backup>
<device-transfer>
<exclude domain="sharedpref" path="tools.skip.SkipKeychain.xml"/>
</device-transfer>
</data-extraction-rules>
```

## Building

This project is a Swift Package Manager module that uses the
Expand Down
61 changes: 38 additions & 23 deletions Sources/SkipKeychain/SkipKeychain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down