The Unlock variant had a single field used as the mnemonic, with no way to convey the BIP39 password extension (25th word). The actor handler silently passed for the passphrase, making it impossible to unlock with a BIP39 passphrase via irpc. Split into + to match the spec and SecretServiceHandle::unlock() signature.
4.4 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| unlock-passphrase-gap | Fix Unlock protocol variant to carry both mnemonic and BIP39 passphrase | complete |
|
narrow | low | component | implementation |
Description
The Unlock variant in SecretProtocol currently accepts only a single
passphrase: String field, which is used as the mnemonic phrase. The
SecretServiceHandle::unlock() method takes two parameters —
phrase: &str (the mnemonic) and passphrase: Option<&str> (the optional
BIP39 password extension) — but the irpc Unlock message cannot convey the
BIP39 passphrase.
The SecretServiceActor::handle_message() works around this by passing
passphrase as the mnemonic and None for the BIP39 passphrase:
SecretMessage::Unlock(msg) => {
let Unlock { passphrase } = inner;
let result = self.handle.unlock(&passphrase, None);
This means users who protect their mnemonic with a BIP39 passphrase (the "25th word") cannot unlock the service via irpc. This is a protocol gap that should be fixed before Phase A integration, since the wire format becomes harder to change once consumers depend on it.
What needs to happen
-
Update
Unlockvariant inprotocol.rsto carry both fields:#[rpc(tx = oneshot::Sender<Result<(), SecretServiceError>>)] #[wrap(Unlock)] Unlock { /// The BIP39 mnemonic phrase (space-separated word list). mnemonic: String, /// Optional BIP39 passphrase (the "25th word" password extension). passphrase: Option<String>, }, -
Update
SecretServiceActor::handle_message()to pass both fields:SecretMessage::Unlock(msg) => { let Unlock { mnemonic, passphrase } = inner; let result = self.handle.unlock(&mnemonic, passphrase.as_deref()); -
Update the
Unlockwrap struct that#[wrap]generates — this is automatic via the#[wrap(Unlock)]attribute, so no manual change needed beyond updating the enum variant. -
Update the
unlock_new()method's irpc path (if any) — currentlyunlock_new()generates a random mnemonic and returns the phrase. There is no irpc variant for this; it's a local-only operation. No change needed. -
Add a test: Verify that
Unlock { mnemonic, passphrase: Some("TREZOR") }produces a different seed thanUnlock { mnemonic, passphrase: None }. -
Verify backward compatibility: Since
Unlockis a new variant field, postcard deserialization of old messages (with onlypassphrase) will fail. This is acceptable because the secret service has not been deployed yet — there are no existing wire consumers. The protocol is still in development.
Acceptance Criteria
Unlockvariant inprotocol.rshasmnemonic: Stringandpassphrase: Option<String>fieldsSecretServiceActor::handle_message()passes both fields toSecretServiceHandle::unlock()- Unit test: Unlock with mnemonic + passphrase produces different seed than Unlock with same mnemonic + no passphrase
- Unit test: Unlock with mnemonic + None passphrase works (backward compat for the common case)
- Unit test: Unlock via
SecretMessage(irpc actor path) with both fields works correctly cargo test -p alknet-secret --all-featurespassescargo clippy -p alknet-secret --all-features -- -D warningspassescargo fmt -p alknet-secret -- --checkpasses
References
- docs/architecture/secret-service.md — SecretProtocol section (updated spec)
- crates/alknet-secret/src/protocol.rs —
Unlockvariant (current: single field) - crates/alknet-secret/src/service.rs —
SecretServiceHandle::unlock()andSecretServiceActor::handle_message()
Notes
This is a wire format change. Since alknet-secret is not yet deployed and has no existing consumers, this is safe to change now. After Phase A integration, wire format changes would require versioning or migration.
The spec already reflects the target state (
Unlock { mnemonic, passphrase }). This task brings the implementation in line with the spec.
The
Unlock { passphrase: String }field name was misleading — "passphrase" sounds like the BIP39 password, but it was actually the mnemonic phrase. The new field names are unambiguous:mnemonicis the word list,passphraseis the optional BIP39 password extension.