Files
alknet/tasks/integration/phase3/secret-service/unlock-passphrase-gap.md
glm-5.1 bda18f6bef docs(architecture): sync secret-service spec with implementation and add unlock-passphrase-gap task
Update secret-service.md to reflect the actual alknet-secret implementation:
- Fix dependency names/versions: secp256k1 (not libsecp256k1), version 0.29,
  add tokio/irpc-derive/hmac/rand, use workspace refs
- Add SecretServiceActor and CacheConfig to public API
- Add ethereum.rs module to crate structure, fix test_vectors.rs filename
- DerivedKey is move-only (not Clone), matching the stronger security impl
- Update BIP39 pseudocode to actual derive_path_from_seed() API
- Document derive_password_string() convenience method
- Document SecretServiceActor::spawn() in irpc integration model
- Update Unlock variant to target state: { mnemonic, passphrase: Option }
- Add implementation gap note pointing to unlock-passphrase-gap task

Add tasks/integration/phase3/secret-service/unlock-passphrase-gap.md:
- Fix Unlock protocol variant to carry both mnemonic and BIP39 passphrase
- Currently the irpc message only has passphrase: String (used as mnemonic)
- The handle supports both parameters but the protocol can't convey them
2026-06-10 09:18:59 +00:00

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 pending
irpc-secret-protocol-integration
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

  1. Update Unlock variant in protocol.rs to 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>,
    },
    
  2. 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());
    
  3. Update the Unlock wrap struct that #[wrap] generates — this is automatic via the #[wrap(Unlock)] attribute, so no manual change needed beyond updating the enum variant.

  4. Update the unlock_new() method's irpc path (if any) — currently unlock_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.

  5. Add a test: Verify that Unlock { mnemonic, passphrase: Some("TREZOR") } produces a different seed than Unlock { mnemonic, passphrase: None }.

  6. Verify backward compatibility: Since Unlock is a new variant field, postcard deserialization of old messages (with only passphrase) 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

  • Unlock variant in protocol.rs has mnemonic: String and passphrase: Option<String> fields
  • SecretServiceActor::handle_message() passes both fields to SecretServiceHandle::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-features passes
  • cargo clippy -p alknet-secret --all-features -- -D warnings passes
  • cargo fmt -p alknet-secret -- --check passes

References

  • docs/architecture/secret-service.md — SecretProtocol section (updated spec)
  • crates/alknet-secret/src/protocol.rs — Unlock variant (current: single field)
  • crates/alknet-secret/src/service.rs — SecretServiceHandle::unlock() and SecretServiceActor::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: mnemonic is the word list, passphrase is the optional BIP39 password extension.