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
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.
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/walletThe 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
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.
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}.
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.
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:
NothingStored: the credential-store entry is deleted first,
then 0x04 is written, so no copy of the secret is left behind on
this machine.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.
Frame::OnSaveNew → new-wallet wizard →
Frame::NewWallet): after the wizard has produced and verified the
seed phrase, PromptForStorageChoice runs and Frame::NewWallet
persists the decision through ApplyStorageChoice. For
NothingStored the seed phrase is handed to NewWalletDisplay as
the unlock secret so the just-created wallet opens immediately.Frame::RecreateWalletFromExistingSeedPhrase):
the user types his seed phrase, it is whitespace-canonicalized, and
the same storage-choice dialog and persistence path follow.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:
Frame::PromptForPassphrase) for 0x01/0x03, with unlimited
retries on a wrong passphrase (a local login).NotStored (0x04): the multi-line PromptForSeedPhraseToUnlock
dialog; the typed phrase is canonicalized, the master key is derived
(the costly Argon2id step), and the derived extended public key is
compared with the one stored in Misc row 5; a wrong phrase
re-prompts, cancelling aborts the open.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.
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:
StorageChoiceOf(m_Mode)). Cancelling
changes nothing.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;ApplyStorageChoice persists the decision (same code as creation),
and the carried seed text and passphrase are wiped from memory
best-effort.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.
reaction.la gpg key 154588427F2709CD9D7146B01C99BB982002C39F
This work is licensed under the Creative Commons Attribution 4.0 International License.