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")
|
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 ──────────────────────────────────────────
|
// ── Type Guard ──────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -160,6 +160,53 @@ public enum BLFieldEncrypt {
|
|||||||
return SymmetricKey(data: data)
|
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
|
// MARK: - Type Guard
|
||||||
|
|
||||||
/// Check if a JSON value represents an encrypted field.
|
/// Check if a JSON value represents an encrypted field.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user