Add PasswordTransformer trait with platform implementations#1189
Add PasswordTransformer trait with platform implementations#1189
Conversation
867fb66 to
0aa9929
Compare
d1e59aa to
313a085
Compare
Introduces `AccountRepository` for encrypted credential storage and `AesGcmPasswordTransformer`, a pure-Rust AES-256-GCM password encryption implementation exposed via UniFFI. The `PasswordTransformer` trait uses `#[uniffi::export(with_foreign)]` so platforms can provide their own implementations (e.g. hardware- backed encryption) while the Rust one serves as a cross-platform fallback — particularly for Linux where platform keystores are unavailable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0df795f to
38a69b6
Compare
Exports persistence types (AccountRepository, PasswordTransformer, etc.) to the public Swift API. Conditionally re-exports AesGcmPasswordTransformer on Linux via #if os(Linux) so third-party clients can use it through `import WordPressAPI` without reaching into WordPressAPIInternal. Adds AesGcmPasswordTransformerTests exercising the Rust-backed UniFFI transformer — guarded with #if os(Linux) since the type is only available there (the Apple xcframework is built with --no-default-features which excludes aes-gcm-encryption). On Apple platforms, SecureEnclavePasswordTransformer fills this role instead. Fixes the testRoot() test on Linux by gating it with there, and adds request timeouts to prevent hangs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
115e7db to
4a65eee
Compare
…tion Hardware-backed PasswordTransformer for Apple platforms using ECIES (P-256 ECDH + HKDF-SHA256 + AES-256-GCM). Falls back to software keys on simulators. Guarded with #if canImport(CryptoKit) so it compiles out cleanly on Linux. All Secure Enclave tests run under a single @suite(.serialized) to prevent cooperative thread pool deadlocks — SE key creation blocks the calling thread, and Swift Testing's fixed-size cooperative pool deadlocks when all threads block simultaneously. See: https://forums.swift.org/t/cooperative-pool-deadlock-when-calling-into-an-opaque-subsystem/70685 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0dc3eff to
58196fc
Compare
58196fc to
22fbd5d
Compare
|
Just for my understanding, we are going to use The
IMO, the second kind of private key is not secure, because it's stored on disk without any protection. According to the comment on Also, I'm not sure if the SecureEnclave encryption is necessary. I think we can delegate the encryption to the keychain? Here is a pseudo-code: class KeychainPasswordTransformer {
func encrypt(password: String) {
let id = UUID()
keychain.store(username: id, password: password, service: "wordpress-rs-accounts")
return id
}
func decrypt(encrypted: String) -> String {
keychain.get(username: encrypted, service: "wordpress-rs-accounts)
}
}
I wonder if we can have a Can we ask the platform to store the information for us securely, without thinking about encryption & decryption? // pseudo-code: Just a simple key-value store.
trait SecureKeyValueStore {
fn store(id: String /* AccountId */, data: Vec<u8>)
fn get(id: String) -> Vec<u8>
}The platform can decide how to store the data securely, so we likely don't need any encryption or decryption code. |
When running the app for macOS local development, you have to type your keychain password on every launch because the app's signature has changed. This is protection against Keychain enumeration attacks – if I were reviewing a patch someone contributed, you don't want it to read every keychain entry and upload what it finds to some remote server. But that makes local development super annoying. One solution for this is using an Apple Development identity for dev builds, but then you need to propagate that to CI. That's a lot of complexity. In this PR, the Apple platforms approach works the same as the Android one – the system vends a key out of its secure hardware, and we use that to encrypt the secrets in-place. Here's an outline of how it's stored for each case:
The only insecure storage mechanism here is macOS without a SEP – that's Intel machines without a T-series coprocessor. I don't really want the protocol to act as a raw K/V store, because another big benefit to storing a single keychain entry for the whole app is that you don't run into sync issues between what's in the keychain and what's in the app's storage and you get guarantees about avoiding race conditions or write tearing. |
Yeah, I get what you want to avoid here. But I think it's okay to bypass any security measures for local development and just write the password to a file? I don't know much about cryptography. The large chunk of cryptography code looks quite intimidating to me, which is why I'd prefer to delegate that to the system. That's pretty much where all my suggestions are centered around 😅
You can generate a secret in Rust and store it in the K/V store, and use AesGcmPasswordTransformer internally. In doing that, you don't need the cryptography code in Swift and Kotlin. Here is a potentially similar way to remove the cryptography code in Swift and Kotlin: class Encryption: PasswordTransformer {
let transformer: AesGcmPasswordTransformer
init() {
let secret: String = getOrCreateFromKeychain()
transformer = AesGcmPasswordTransformer(secret: secret)
}
func encrypt() {
transformer.encrypt()
}
func decrypt() {
transformer.decrypt()
}
} |
Hardware-backed PasswordTransformer for Android using the Android Keystore with AES-256-GCM. Prefers StrongBox (API 28+) and falls back to TEE. Exposes isHardwareBacked property for callers to check the security level. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
22fbd5d to
6a0b82d
Compare
Description
Add encrypted credential storage for persisting site authentication across app launches.
This PR introduces a
PasswordTransformertrait and platform-specific implementations that encrypt credentials at rest, enablingAccountRepository(also introduced here) to persist sites and tokens securely.This is the foundation for credential storage in:
Changes
PasswordTransformertrait + AES-256-GCM implementation (wp_mobile)encrypt/decryptmethods#[uniffi::export(with_foreign)]so platforms can provide hardware-backed implementationsAesGcmPasswordTransformerserves as a cross-platform fallback (particularly for Linux where platform keystores are unavailable)AccountRepositoryfor storing and retrieving encrypted site credentialsSwift bindings for persistence types
AccountRepository,PasswordTransformer, etc. to the public Swift APIAesGcmPasswordTransformeron Linux via#if os(Linux)SecureEnclavePasswordTransformerfills this role insteadSecureEnclavePasswordTransformer(Apple)kSecAttrServicefor visibility in Keychain Access@Suite(.serialized)to prevent cooperative thread pool deadlocks during SE key creationKeystorePasswordTransformer(Android)isHardwareBackedproperty so callers can check the security levelTest plan
cargo test --lib— Rust unit tests forAesGcmPasswordTransformerandAccountRepositorySecureEnclavePasswordTransformer(22 tests: round-trip, key persistence, error paths)KeystorePasswordTransformer(15 tests: round-trip, key reuse, error paths, tampered ciphertext)cargo clippyandcargo fmtpass