Skip to content

refactor: rust - #117

Merged
OffRange merged 18 commits into
v2from
refactor/rust
Oct 12, 2026
Merged

OffRange merged 18 commits into
v2from
refactor/rust

Conversation

@OffRange

Copy link
Copy Markdown
Owner

No description provided.

OffRange and others added 18 commits October 10, 2026 11:05
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
passkey 0.6 makes the authenticator's crypto backend pluggable; pass the
crate's default RustCryptoBackend. argon2 0.6 needs no code changes; the
password-domain KAT and both golden backups still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
register_passkey filled user_name from rp.name, so every new passkey was
stored under the site's display name ("Example Corp") instead of the
account it belongs to ("alice@example.com").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only backups and the password KEK had known-answer tests. The wrapped ARK,
vault and item keys, item ciphertext, and stored passkey bytes are all
persisted too, and any drift in cipher, nonce layout, or AAD encoding would
make them unreadable without a compile error. Freeze one of each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every key uses AES-256-GCM-SIV, so name the cipher once and drop the
per-key algorithm generic. That collapses the old surface:

- AeadWrappedKey<T, W>, AeadCiphertext<K> and the WrappedKey trait become
  Sealed<T>, where T is the wrapped key type or [u8] for data.
  Sealed::from_parts returns an error on a bad nonce instead of panicking,
  which a corrupt row from Kotlin could previously trigger across the FFI.
- KeyMaterial/AeadKey/KeyWrapper/AeadEncryptor become SymmetricKey,
  Wraps<K> and Encrypts. The whole hierarchy now reads in keys.rs.
- define_aead_key!/define_wrap! become one crate-private symmetric_key!.
- TryDeriveFrom becomes RootKEK::from_password, owning PASSWORD_DOMAIN.
- ItemDataAad becomes Encrypts::Aad = [u8]; BCS encodes both identically.
- Unused ScopedSigningKey and its macro are removed.

Errors carry their cause: BCS/KDF strings become AadEncoding, EmptyPassword,
SaltTooShort, Argon2MemoryTooHigh, Argon2 and InvalidNonceLength.
Derived KDF output is zeroized.

ArkSession takes wrapped keys by reference and bundles salt + wrapped ARK
as PasswordWrappedArk. Argon2 still runs outside the session lock.

All goldens and the password KAT pass with their vectors unchanged. The
Kotlin-facing FFI is unchanged: regenerated bindings differ only in the
UniFFI checksums of methods taking ItemKey, whose declaration moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
passkey: RegistrationError and ProviderError overlapped variant for variant;
merge them into one PasskeyError, mapped once at the FFI. register and
authenticate share request parsing, origin and response encoding. The
remaining unwrap()s become NoCredentialCreated and ResponseEncoding errors
instead of panics in an async FFI call. The codec's wire types go private
behind encode/decode, so their serde shape is no longer public API.

totp: algorithm, digits and period travel as one TotpParams instead of
three positional arguments, and secrets are borrowed. get_totp,
get_totp_url and get_totp_info_from_uri become generate, to_uri and
parse_uri.

csv: ColumnMapping uses u32 like CsvColumn.index, so the FFI re-exports
the core type instead of keeping a usize-to-u32 copy of it.

The Kotlin-facing API is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-dalek

- [profile.release] lto + codegen-units = 1 shrinks the arm64 .so from
  3.96 MB to 2.78 MB. Symbols are kept (strip = debuginfo) because
  uniffi-bindgen reads its metadata from the library; bindings generated
  from the LTO build match exactly. panic stays unwind so UniFFI can turn a
  panic into a Kotlin exception.
- keygo-core has no unsafe code; forbid it so it stays that way.
- ed25519-dalek was only used by the removed ScopedSigningKey.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
keygo-uniffi and keygo-core (dev) both depend on it; the workspace now owns
the version and each crate keeps its own features.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the standard templates for files the operating system drops into
working folders: Finder's .DS_Store and friends, Windows thumbnail and
shortcut files, Linux editor backups and trash folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AGP strips native libraries when it packages the APK, after extracting
their symbol tables for Play Console (debugSymbolLevel = SYMBOL_TABLE).
Other consumers, such as a future PyO3 wheel built by maturin, strip on
their own terms too. The Cargo strip setting had no measurable effect here
anyway: the arm64 library is byte-for-byte the same size with or without it.

The size win from the release profile is LTO + codegen-units = 1: the
stripped arm64 library that ships goes from 2.67 MB to 2.25 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Unlock reports a failed password unwrap as WrongPassword, the same as
  verify_password; it used to surface as KeyWrap(UnwrapFailed). Kotlin
  already mapped both to "incorrect password", so the app behaves the same.
  FakeSession follows.
- Salts come from aead's Generate like keys and nonces, through one
  kdf::new_salt and SALT_LEN. crypto/random.rs and the direct rand
  dependency are gone.
- The CSV TOTP check decodes with totp-rs's Secret::try_from_base32, which
  calls the same base32 decoder, so the base32 dependency is gone.
- passkey-authenticator no longer enables "testable", which only pulled
  mockall into the shipped library.
- encryption::open takes the ciphertext by value, saving a full copy of
  every imported JSON backup.
- Registration and CreationInfo cross the FFI as remote records instead of
  field-for-field copies. Kotlin: RegistrationResponse -> Registration,
  PasskeyInformation -> CreationInfo.
- Drop the never-constructed InvalidKey variants from KeyWrapError and
  ItemCryptoError.
- SymmetricKey::as_bytes is a default method; one local macro supplies
  generate() for the ARK, vault and item keys.
- with_core's comment now says the ARK is copied for the call, which is
  what with_ark does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop comments that restate names or narrate tests, and shorten the rest
to a line or two. Format freezes, locking contracts, and other
non-obvious invariants stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Item data is encrypted under ItemDataAad { item_id, kind: SecretKind }
  instead of bytes built by hand in Kotlin. Rust now owns the pinned
  labels, and the custom Serialize keeps the 2.1.0 layout (item_id ||
  label) byte for byte. A new golden vector, sealed by the old raw-bytes
  path, proves it and replaces the synthetic b"item-aad" one.
- Kotlin: SecretBlueprint.label: String -> kind: SecretKind, a domain enum
  in core:item. core:security maps it to the UniFFI enum with an
  exhaustive when; the health report uses PasswordHealthReport.
- UserId, VaultId and ItemId are newtypes, so swapping them in a wrap or
  an ItemAad no longer compiles. BCS encodes them as before, and Kotlin
  still sees java.util.UUID.
- Wraps/Encrypts require an Aad, declared through aad! or an explicit
  impl, so [u8] and bare Uuids are no longer accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
passkey-rs 0.6.0's Option<Passkey> store ignores the request's rp_id
(upstream TODO), so a passkey registered for example.com signed an
assertion for evil.com. authenticate() now compares the request's rp_id
with the one sealed inside the stored passkey and fails with RpMismatch,
which reaches Kotlin as the existing InvalidDomain.

The new test pins the behaviour; once upstream filters by rp_id and the
test passes without our check, the check can go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
accountScope handed a vault key to CryptographicScopeImpl as an item key.
That compiled only because UniFFI lowers both to ByteArray, so the
password-health report bypassed the key types the Rust refactor added.

- Rust: NamespaceKey, wrapped by the ARK under NamespaceKeyAad and
  encrypting under NamespaceDataAad. Both AADs carry a domain tag
  (v1:ns/key, v1:ns/data), so a namespace key's wrap can never encode
  like a vault key's. ArkSession seals and opens records itself, so the
  key never crosses UniFFI. A new DecryptionFailed error separates a
  corrupted record from a key that will not unwrap.
- Kotlin: AccountRecordSealer (seal/open returning Result) replaces
  accountScope. HealthReportStoreRepositoryImpl no longer catches
  ItemCryptoException, and CryptographicScopeProvider loses accountScope
  and its VaultManager dependency.
- Every seal uses a fresh key, so a stored key that no longer unwraps
  cannot block later writes.

Breaking: reports stored by 2.1.0 used the old layout and no longer
open. The store is a cache, so a failed load triggers a rescan and the
next store overwrites the old report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A backup's Argon2 parameters come from its header, and only the memory
cost was bounded, so a crafted file with iters = u32::MAX hung the
import. Argon2Params::build() now rejects more than MAX_ARGON2_ITERS
(16) with Argon2ItersTooHigh, beside the existing memory check.

Every backup so far was written with the default of 3 iterations, so
none is rejected; a test keeps the defaults inside both caps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@OffRange
OffRange merged commit ee1b0cf into v2 Oct 12, 2026
8 checks passed
@OffRange
OffRange deleted the refactor/rust branch October 12, 2026 19:19
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.

1 participant