| Filename | Latest commit message | Latest commit date |
|---|---|---|
ADR 0017, step 3: halkyon_app's side, which Learn and Todo take. - The account's cryptography on the device (`SodiumAccountCrypto`): halkyon_keys and SRP's client half, each step on an isolate of its own. sodium hands out results as views of native memory its finalizers free once the isolate that made them ends, so everything crossing back is copied first: a view read freed memory and crashed the process. - `PasswordGateway` on the module's endpoints (steps without a session through a client holding none; an authenticator set up with a session granted but not yet the device's), and `PasswordRepository` running the flows: signing in (M2 checked, the keys opened before anything is trusted), signing up, a password for someone signed in, an account never finished, the authenticators, recovery codes, changing the password, a new recovery phrase, and getting back in. - The unlocked keys in the system's keyring per instance and person (`SecureAccountKeyStore`), gone once another person signs in or nobody, and with the instance's removal. - The screens: the start screen's Sign in and Create an account (and when GitHub sign-in ends), the sign-in sheet (codes, recovery codes, an account never finished, a lost authenticator), the account's setup (the recovery phrase shown once, three random words typed back before anything reaches the instance; the authenticator's QR code, drawn with pub.dev's qr; the recovery codes), getting back in, and the account section (`AccountSecuritySection`). - `FileSaver` moves from Learn into halkyon_app, for the phrase and the codes in Todo too; `AppDependencies` names it and `passwords`. - halkyon_keys rebuilds account keys from the secret keys a device kept; the key proof's message is in halkyon_base_client, checked against the register's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me> |
||
| .. | ||
| lib | ||
| test | ||
| .gitignore | ||
| analysis_options.yaml | ||
| BIP39-LICENSE | ||
| LICENSE | ||
| pubspec.yaml | ||
| README.md | ||
halkyon_keys
An account's keys on the device (ADR 0017), in pure Dart on libsodium (the sodium
package, its sumo API). Every primitive is libsodium's; this package only puts them
together. Callers pass a SodiumSumo from await SodiumSumoInit.init().
sodium's build hook compiles the libsodium it ships (1.0.22), so every machine that
builds or tests a package depending on this one needs a C compiler and make.
What it provides
| Piece | API | Parameters |
|---|---|---|
| The password's keys | newPasswordSalt, derivePasswordKeys → PasswordKeys (srpSecret, srpPrivateValue, keyEncryptionKey) |
Argon2id v1.3, 64 MiB, 3 passes (PasswordKdf.current), a 16-byte salt, a 32-byte master key; libsodium's KDF (context hlkpass1) splits it into SRP's secret (subkey 1, whose 256 bits big-endian are SRP's x) and the key-encryption key (subkey 2) |
| The password as typed | normalizePassword (PasswordRefused) |
RFC 8265's OpaqueString: other spaces to U+0020, Unicode NFC (unorm_dart, pinned exactly), empty or control characters refused; derivePasswordKeys applies it, so composed and decomposed accents unlock alike |
| The costs kept with an account | PasswordKdf (toJson, fromJson) |
kept beside the salt, so they can be raised for new passwords; fromJson refuses another algorithm or costs below PasswordKdf.minimum, kept apart from the current costs so raising those never locks out an older account |
| The account's key pairs | AccountKeys.generate |
X25519 to encrypt to, Ed25519 to sign with |
| Their wrapping | wrapAccountKeys, unwrapAccountKeys → WrappedAccountKeys (nonce, cipherText) |
XChaCha20-Poly1305, a random 24-byte nonce, the private halves (32 + 64 bytes) as plaintext, both public keys and a version label as additional data; unwrapping also rebuilds both public keys from the private halves and compares them with those given |
| The recovery phrase | newRecoveryPhrase, recoveryPhraseOf, entropyOfRecoveryPhrase |
BIP-39, English list: 24 words for 256 random bits and an 8-bit checksum; case and spacing as typed don't count |
| The recovery key | recoveryKeyOf |
libsodium's KDF over the phrase's 256 bits (context hlkrcvy1, subkey 1); a 12- to 21-word BIP-39 phrase is refused |
| Data keys | DataKey.generate, sealDataKey, openDataKey |
a random XChaCha20-Poly1305 key per store with an ID (1 to 255 bytes of UTF-8), sealed with its ID to the account's X25519 key (crypto_box_seal); it opens only under the ID it was sealed with |
| Content | sealContent, openContent → SealedContent (keyId, nonce, payload, signature) |
ADR 0002's envelope: XChaCha20-Poly1305 under the data key, the key's ID and a caller's context (its table and row) as additional data; the author's Ed25519 signature, detached, over the ID, nonce, payload and context, each framed by its length; the signature is checked before anything is decrypted |
| Refusals | KeyRefused with a KeyRefusal |
wrongKeyOrTampered, notTheAccountsKeys, wrongDataKey, forgedSignature, weakPasswordKdf, phraseLength, phraseWord, phraseChecksum |
Secrets stay in libsodium's guarded memory (SecureKey) where its API takes one:
the master key, both password subkeys, the private halves of the key pairs, the
recovery key and data keys. Wrapping, sealing a data key and reading x copy key bytes
to the Dart heap for a moment, wiped afterwards. dispose wipes and frees a holder's
keys, which may not be read afterwards.
What never leaves the device
The password, the master key, SRP's secret and x, the key-encryption key, the recovery phrase and its key, the private halves of the key pairs, data keys and content in the clear. What the register keeps is the salt and the costs, both public keys, the private keys wrapped twice (under the key-encryption key and under the recovery key), data keys sealed to the account, and sealed content: none of it opens without the password or the phrase.
Tests
dart test: libsodium's own Argon2id known answers (its pwhash_argon2id test, so the
compiled libsodium is the one these keys were made with), BIP-39's English vectors
(trezor/python-mnemonic's vectors.json, MIT), the word list's SHA-256, round trips,
and every refusal.
Licence
GNU Affero General Public License, version 3 or any later version (LICENSE). The
English word list is BIP-39's, under the MIT License (BIP39-LICENSE).