logo
vision scalability social networking revelation source
consensus immutability Bitcoin mixer snarks

Seed Phrase and Master Secret

abstract: The seed phrase, instead of being twelve random words, is two random nonsense sentences

This more readily memorable. From this, the master secret is generated in a costly way

1 Status

This design is implemented as of git tag seed-phrase-and-master-secret (commits 532c2cc and 6e0a192 on branch tooln). The sections below describe the code as it stands at that tag.

2 Master secret selection or creation

The database relies on secret keys derived from the master secret. The master secret is derived from the seed phrase through a costly key derivation (Argon2id in ristretto255::extended_private_key), so a guessed seed phrase is expensive to try.

When the program starts up, it defaults to using whatever database was last used, which is remembered in the configuration file. The user can select a database, or create a new one around another secret, from the file menu, but normally he never does this.

Instead, when the program starts up on a machine with no configuration file and no database with the expected name in the expected place, the user is presented with three buttons (the welcome panel built by welcome_to_rhocoin): to create a new secret (Frame::OnSaveNew, the new-wallet wizard), to enter an existing seed phrase (Frame::RecreateWalletFromExistingSeedPhrase), or to select a database file with an existing secret (Frame::OnFileOpen).

To force the code to take this path for testing purposes we delete the configuration file and the default database directory

rm ~/.config/rho.conf
rm -r ~/.local/share/rho
build/wallet

3 Secrets options

The seed phrase may be entered every time the user needs to use it for maximum security, but this is inconvenient.

The default option is that the seed phrase and master secret are stored in the secret store. Optionally the user may supply a passphrase.

The user has a choice of

  1. Secret store, no passphrase (default)
  2. Secret store, passphrase each time the program starts up.
  3. Nothing in the secrets store, seed words each time the program starts up.

This choice is offered in the same modal dialog (StorageChoiceDialog in src/frame.h/src/frame.cpp) at the end of the new-wallet wizard, in the recover-from-seed-phrase flow, and again whenever the user changes his mind about an existing wallet via Edit ▸ Set security. All three entry points funnel through Frame::PromptForStorageChoice, so the user sees the same dialog whichever path he takes.

4 The code at this tag

4.1 The mode byte (src/secrets_storage.h)

The wallet database records the storage policy in one byte, Misc row 6 (MISC_ROW_MODE in src/db_accessors.h), as secrets_storage::Mode:

value mode meaning
0x00 NoPassphrase credential store holds the master extended secret key in cleartext
0x01 Passphrase credential-store entry encrypted with the user’s passphrase
0x02 NoPassphraseWithSecret like 0x00, additionally carrying the human-readable seed phrase text
0x03 PassphraseWithSecret like 0x01, seed phrase text inside the AEAD plaintext
0x04 NotStored nothing persisted on this machine (design option 3)

Values 0x00–0x03 pack two orthogonal flag bits — bit 0 = passphrase-protected, bit 1 = seed phrase text stored — composed by the constexpr helper ComposeMode. 0x04 is a distinct value that is never written to the OS credential store. Small constexpr predicates (HasSeedPhrase, StoredInCredentialStore, RequiresSeedPhraseAtOpen) and StorageChoiceOf map between the on-disk byte and the user-facing StorageChoice {SecretStoreNoPassphrase, SecretStorePassphrase, NothingStored}.

4.2 Credential-store entries (src/secrets_storage.cpp)

For the credential-store modes the master extended secret key (the wallet’s extended private key) is saved with SaveMasterExtendedSecretKey under the service name rhocoin-wallet/<base58 extended public key> — the base58 includes a checksum and gives each wallet its own keychain slot, which is required because wxSecretStore::Delete only matches on service. The entry body starts with a format tag byte (identical to the mode byte, so DB and keychain never disagree), followed by either 64 raw key bytes (no passphrase) or an Argon2id salt, an XChaCha20 nonce and an XChaCha20-Poly1305 ciphertext of the key (passphrase modes). In the *WithSecret modes the canonical seed-phrase text follows the key, length-prefixed, inside the same (possibly encrypted) blob. Loading is done by LoadMasterExtendedSecretKey (the master key) and LoadSeedPhrase (the text only, used by Show seed phrase and by the policy-change path); DeleteMasterExtendedSecretKey removes the entry. The seed phrase text is optional: without it the wallet works normally, only Show seed phrase becomes unavailable.

4.3 Persisting a decision (secrets_storage::ApplyStorageChoice)

ApplyStorageChoice(db, pubkey, master key, optional seed text, choice, optional passphrase) is the single code path that persists a storage decision: it rewrites MISC_ROW_MODE and saves or deletes the credential-store entry. Both Frame::NewWallet (creation) and display_wallet::change_security_event_handler (changing an existing wallet) call it, so the two paths cannot drift apart. Ordering is chosen so a crash in the middle never strands the secret or orphans a copy of it:

4.4 The storage-choice dialog (StorageChoiceDialog, src/frame.cpp)

A modal wxDialog with a three-way wxRadioBox (the strings live in src/localization.cpp, sz_storage_choice_*), plus passphrase and confirm-passphrase fields that are only consulted for option 2. It defaults to option 1; when the caller supplies a current policy (the change path) that policy is preselected. If the OS credential store is unavailable (wxSecretStore::IsOk fails) the two secret-store options are disabled and store nothing is forced. Frame::PromptForStorageChoice is the only place the dialog is run and returns the chosen StorageChoice plus the passphrase for option 2, or reports cancellation.

4.5 Creation paths

4.6 Opening a wallet (Frame::PromptForWalletPassphraseIfNeeded)

Frame’s constructor auto-opens the last-used wallet, and Frame::OnFileOpen opens any chosen file; both first call PromptForWalletPassphraseIfNeeded, which opens the DB, validates the file-identifier/schema rows, reads the mode byte and dispatches:

display_wallet’s constructor then either loads the master key from the credential store (verifying it against the wallet’s stored public key) or, for NotStored, derives it from the supplied seed phrase.

4.7 Changing the policy of an existing wallet (src/display_wallet.cpp)

Edit ▸ Set security (myID_DISPLAY_WALLET_CHANGE_SECURITY, handler change_security_event_handler) lets the user change his mind at any time:

  1. The shared storage-choice dialog runs, preselected with the wallet’s current policy (StorageChoiceOf(m_Mode)). Cancelling changes nothing.
  2. If the new policy stores anything on this machine, the seed phrase text is carried into the new entry whenever possible:
    • current entry holds the seed text (HasSeedPhrase): it is loaded from the keychain via LoadSeedPhrase; for PassphraseWithSecret the current passphrase is prompted first, re-prompting on a wrong passphrase;
    • NotStored: the user re-types the seed phrase through the same PromptForSeedPhraseToUnlock dialog and pubkey verification used at open time, re-prompting on a wrong phrase;
    • modes 0x00/0x01 (legacy entries without seed text, which cannot be recovered from the master secret): a warning explains that the new entry will not contain the seed phrase either and offers a chance to back out.
  3. ApplyStorageChoice persists the decision (same code as creation), and the carried seed text and passphrase are wiped from memory best-effort.

4.8 Verification

build/wallet -ct creates the default option-1 wallet from the welcome panel and exits 0. The GUI paths were exercised over AT-SPI: all three options round-trip across restarts (mode byte, keychain entries and prompts behave as described above, always reproducing the same master key), the dialog preselects the current policy on every path, wrong passphrases and wrong seed phrases re-prompt, and cancelling any dialog leaves the wallet unchanged.

Creative Commons License reaction.la gpg key 154588427F2709CD9D7146B01C99BB982002C39F
This work is licensed under the Creative Commons Attribution 4.0 International License.