feat(native-sdks): add Keychain/SecureStore key derivation to BLFieldEncrypt
- Swift: getOrCreateKey(service:account:), loadKey(), deleteKey() - Generates random AES-256 key, persists as hex in BLKeychain - Subsequent calls return the same key for stable per-device DEK - Kotlin: getOrCreateKey(store:account:), loadKey(), deleteKey() - Generates random AES-256 key, persists as hex in BLSecureStore - Uses EncryptedSharedPreferences for at-rest protection - All existing tests still pass (21/21 Kotlin)
This commit is contained in:
parent
e59dcdb9ac
commit
9bb322113a
@ -159,6 +159,59 @@ object BLFieldEncrypt {
|
||||
return SecretKeySpec(bytes, "AES")
|
||||
}
|
||||
|
||||
// ── Secure Store Key Derivation ────────────────────────
|
||||
|
||||
/**
|
||||
* Get or create a persistent encryption key in [BLSecureStore].
|
||||
*
|
||||
* On first call, generates a random 32-byte AES-256 key and stores it
|
||||
* as a hex string in EncryptedSharedPreferences. On subsequent calls,
|
||||
* loads the existing key. This provides a stable per-device DEK for
|
||||
* client-side encryption without requiring the backend to provision keys.
|
||||
*
|
||||
* @param store The [BLSecureStore] instance for the current app.
|
||||
* @param account Storage key name (default: `"field_encrypt_dek"`).
|
||||
* @return A 32-byte [SecretKey] backed by secure storage.
|
||||
*/
|
||||
fun getOrCreateKey(store: BLSecureStore, account: String = "field_encrypt_dek"): SecretKey {
|
||||
val existingHex = store.read(account)
|
||||
if (existingHex != null) {
|
||||
return keyFromHex(existingHex)
|
||||
}
|
||||
|
||||
val newKey = generateKey()
|
||||
val hex = newKey.encoded.toHexString()
|
||||
store.save(account, hex)
|
||||
return newKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an existing encryption key from [BLSecureStore] without creating one.
|
||||
*
|
||||
* @param store The [BLSecureStore] instance.
|
||||
* @param account Storage key name (default: `"field_encrypt_dek"`).
|
||||
* @return The stored [SecretKey], or `null` if none exists.
|
||||
*/
|
||||
fun loadKey(store: BLSecureStore, account: String = "field_encrypt_dek"): SecretKey? {
|
||||
val hex = store.read(account) ?: return null
|
||||
return try {
|
||||
keyFromHex(hex)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the stored encryption key from [BLSecureStore].
|
||||
*
|
||||
* @param store The [BLSecureStore] instance.
|
||||
* @param account Storage key name.
|
||||
* @return `true` if the key was deleted.
|
||||
*/
|
||||
fun deleteKey(store: BLSecureStore, account: String = "field_encrypt_dek"): Boolean {
|
||||
return store.delete(account)
|
||||
}
|
||||
|
||||
// ── Type Guard ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@ -160,6 +160,53 @@ public enum BLFieldEncrypt {
|
||||
return SymmetricKey(data: data)
|
||||
}
|
||||
|
||||
// MARK: - Keychain Key Derivation
|
||||
|
||||
/// Get or create a persistent encryption key in the Keychain.
|
||||
///
|
||||
/// On first call, generates a random 32-byte AES-256 key and stores it
|
||||
/// as a hex string in the Keychain. On subsequent calls, loads the
|
||||
/// existing key. This provides a stable per-device DEK for client-side
|
||||
/// encryption without requiring the backend to provision keys.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - service: Keychain service identifier (typically the app's bundle ID).
|
||||
/// - account: Keychain account key (e.g., `"field_encrypt_dek"`).
|
||||
/// - Returns: A 32-byte `SymmetricKey` backed by Keychain storage.
|
||||
/// - Throws: `BLFieldEncryptError.invalidHexEncoding` if stored key is corrupt.
|
||||
public static func getOrCreateKey(service: String, account: String = "field_encrypt_dek") throws -> SymmetricKey {
|
||||
if let existingHex = BLKeychain.read(service: service, key: account) {
|
||||
return try keyFromHex(existingHex)
|
||||
}
|
||||
|
||||
let newKey = generateKey()
|
||||
let hex = newKey.withUnsafeBytes { Data($0).hexString }
|
||||
BLKeychain.save(service: service, key: account, value: hex)
|
||||
return newKey
|
||||
}
|
||||
|
||||
/// Load an existing encryption key from the Keychain without creating one.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - service: Keychain service identifier.
|
||||
/// - account: Keychain account key (e.g., `"field_encrypt_dek"`).
|
||||
/// - Returns: The stored `SymmetricKey`, or `nil` if none exists.
|
||||
public static func loadKey(service: String, account: String = "field_encrypt_dek") -> SymmetricKey? {
|
||||
guard let hex = BLKeychain.read(service: service, key: account) else { return nil }
|
||||
return try? keyFromHex(hex)
|
||||
}
|
||||
|
||||
/// Delete the stored encryption key from the Keychain.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - service: Keychain service identifier.
|
||||
/// - account: Keychain account key.
|
||||
/// - Returns: `true` if the key was deleted or didn't exist.
|
||||
@discardableResult
|
||||
public static func deleteKey(service: String, account: String = "field_encrypt_dek") -> Bool {
|
||||
BLKeychain.delete(service: service, key: account)
|
||||
}
|
||||
|
||||
// MARK: - Type Guard
|
||||
|
||||
/// Check if a JSON value represents an encrypted field.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user