Skip to content

Add PasswordTransformer trait with platform implementations#1189

Open
jkmassel wants to merge 4 commits intotrunkfrom
add/password-encryption
Open

Add PasswordTransformer trait with platform implementations#1189
jkmassel wants to merge 4 commits intotrunkfrom
add/password-encryption

Conversation

@jkmassel
Copy link
Contributor

@jkmassel jkmassel commented Feb 24, 2026

Description

Add encrypted credential storage for persisting site authentication across app launches.

This PR introduces a PasswordTransformer trait and platform-specific implementations that encrypt credentials at rest, enabling AccountRepository (also introduced here) to persist sites and tokens securely.

This is the foundation for credential storage in:

Changes

  1. PasswordTransformer trait + AES-256-GCM implementation (wp_mobile)

    • UniFFI-exported trait with encrypt/decrypt methods
    • Uses #[uniffi::export(with_foreign)] so platforms can provide hardware-backed implementations
    • Pure-Rust AesGcmPasswordTransformer serves as a cross-platform fallback (particularly for Linux where platform keystores are unavailable)
    • AccountRepository for storing and retrieving encrypted site credentials
  2. Swift bindings for persistence types

    • Re-exports AccountRepository, PasswordTransformer, etc. to the public Swift API
    • Conditionally exposes AesGcmPasswordTransformer on Linux via #if os(Linux)
    • On Apple platforms, SecureEnclavePasswordTransformer fills this role instead
  3. SecureEnclavePasswordTransformer (Apple)

    • ECIES encryption (P-256 ECDH + HKDF-SHA256 + AES-256-GCM) backed by the Secure Enclave
    • Falls back to software keys on simulators
    • Keys persisted via Keychain with the application name as kSecAttrService for visibility in Keychain Access
    • Tests run under @Suite(.serialized) to prevent cooperative thread pool deadlocks during SE key creation
  4. KeystorePasswordTransformer (Android)

    • AES-256-GCM encryption backed by the Android Keystore
    • Prefers StrongBox (API 28+), falls back to TEE, works in software on emulators
    • Exposes isHardwareBacked property so callers can check the security level
    • Explicit base64 validation on decrypt for clear error messages

Test plan

  • cargo test --lib — Rust unit tests for AesGcmPasswordTransformer and AccountRepository
  • Swift unit tests for SecureEnclavePasswordTransformer (22 tests: round-trip, key persistence, error paths)
  • Android instrumented tests for KeystorePasswordTransformer (15 tests: round-trip, key reuse, error paths, tampered ciphertext)
  • SwiftLint and Detekt pass
  • cargo clippy and cargo fmt pass

jkmassel and others added 4 commits February 25, 2026 18:19
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>
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>
…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>
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>
@jkmassel jkmassel force-pushed the add/password-encryption branch from 58196fc to 22fbd5d Compare February 26, 2026 01:19
@jkmassel jkmassel marked this pull request as ready for review February 26, 2026 01:19
@crazytonyli
Copy link
Contributor

crazytonyli commented Feb 26, 2026

Just for my understanding, we are going to use SecureEnclavePasswordTransformer on Apple platforms, AesGcmPasswordTransformer on Linux, and KeystorePasswordTransformer on Android, right? I'm not familiar with Android, so I won't comment on that.

The SecureEnclavePasswordTransformer seems to contain two implementations:

  1. one backed by a SecureEnclave private key (not directly usable by third parties), and
  2. the other with a traditional private key that's stored on disk

IMO, the second kind of private key is not secure, because it's stored on disk without any protection. According to the comment on SecureEnclavePasswordTransformer(keyFile:), it's intended to be used for the dev env. If that's the only use case, do we even need to go through the encryption and decryption process? I think we can just have a PlaintextPasswordTransformer, which has no encryption, for local development (like the example apps)?

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 SecureKeyValueStore trait to replace the PasswordTransformer. The definition of PasswordTransformer requires the implementation to be able to encrypt and decrypt any plaintext. But I don't think we really need that level of abstraction.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants