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
This commit is contained in:
@@ -39,37 +39,42 @@ alknet-secret/
|
|||||||
│ ├── derivation.rs # SLIP-0010: HD key derivation, path constants
|
│ ├── derivation.rs # SLIP-0010: HD key derivation, path constants
|
||||||
│ ├── encryption.rs # AES-256-GCM: encrypt/decrypt, EncryptedData type
|
│ ├── encryption.rs # AES-256-GCM: encrypt/decrypt, EncryptedData type
|
||||||
│ ├── protocol.rs # SecretProtocol irpc service enum, DerivedKey, KeyType
|
│ ├── protocol.rs # SecretProtocol irpc service enum, DerivedKey, KeyType
|
||||||
│ ├── service.rs # SecretServiceImpl: in-memory seed, Unlock/Lock lifecycle
|
│ ├── service.rs # SecretService, SecretServiceHandle, SecretServiceActor
|
||||||
│ └── cache.rs # Key caching: LRU cache with TTL, derivation path as key
|
│ ├── cache.rs # Key caching: LRU cache with TTL, derivation path as key
|
||||||
|
│ └── ethereum.rs # BIP-0032 secp256k1 HD key derivation (behind feature flag)
|
||||||
└── tests/
|
└── tests/
|
||||||
├── derivation_tests.rs # Path derivation, coin type 74' consistency
|
├── derivation_tests.rs # Path derivation, coin type 74' consistency
|
||||||
├── encryption_tests.rs # Round-trip encrypt/decrypt, key version
|
├── encryption_tests.rs # Round-trip encrypt/decrypt, key version
|
||||||
├── service_tests.rs # Unlock/Lock lifecycle, derive on locked = error
|
├── service_tests.rs # Unlock/Lock lifecycle, derive on locked = error
|
||||||
└── vectors_tests.rs # Known-answer tests: BIP39, SLIP-0010, AES-256-GCM
|
└── test_vectors.rs # Known-answer tests: BIP39, SLIP-0010, AES-256-GCM
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bip39 = "2"
|
bip39 = { version = "2", features = ["rand"] }
|
||||||
ed25519-bip32 = "0.x" # IOHK SLIP-0010 Ed25519 HD derivation
|
ed25519-bip32 = "0.4" # IOHK SLIP-0010 Ed25519 HD derivation
|
||||||
aes-gcm = "0.10" # AES-256-GCM
|
aes-gcm = "0.10" # AES-256-GCM
|
||||||
sha2 = "0.10" # SHA-256 (also used for HMAC-SHA512 in password derivation)
|
sha2 = "0.10" # SHA-256 (also used for HMAC-SHA512 in password derivation)
|
||||||
|
hmac = "0.12" # HMAC-SHA512 for key derivation
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
irpc = "0.x" # Always-on, not feature-gated (ADR-027)
|
irpc = { workspace = true } # Always-on, not feature-gated (ADR-027)
|
||||||
|
irpc-derive = { workspace = true } # Proc-macro for #[rpc_requests]
|
||||||
|
tokio = { version = "1", features = ["sync", "rt", "macros"] } # Async runtime for SecretServiceActor
|
||||||
zeroize = { version = "1", features = ["derive"] } # Secure memory wiping (ADR-038)
|
zeroize = { version = "1", features = ["derive"] } # Secure memory wiping (ADR-038)
|
||||||
base64 = "0.22" # Base64url encoding for derived passwords
|
base64 = "0.22" # Base64url encoding for derived passwords
|
||||||
|
rand = "0.8" # Random IV/salt generation for AES-256-GCM
|
||||||
|
|
||||||
[dependencies.libsecp256k1]
|
[dependencies.secp256k1]
|
||||||
version = "0.7"
|
version = "0.29"
|
||||||
optional = true # BIP-0032 secp256k1 derivation (behind feature flag)
|
optional = true # BIP-0032 secp256k1 derivation (behind feature flag)
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
secp256k1 = ["libsecp256k1"] # Enable Ethereum/secp256k1 key derivation
|
secp256k1 = ["dep:secp256k1"] # Enable Ethereum/secp256k1 key derivation
|
||||||
|
|
||||||
# Future (Phase B): key rotation via KDF
|
# Future (Phase B): key rotation via KDF
|
||||||
# hkdf = "0.12" # HKDF for salt-based key stretching (deferred)
|
# hkdf = "0.12" # HKDF for salt-based key stretching (deferred)
|
||||||
@@ -79,10 +84,15 @@ secp256k1 = ["libsecp256k1"] # Enable Ethereum/secp256k1 key derivation
|
|||||||
irpc is always a dependency (not behind a feature flag). Per ADR-027, irpc
|
irpc is always a dependency (not behind a feature flag). Per ADR-027, irpc
|
||||||
in alknet-secret and alknet-storage is not feature-gated because these crates
|
in alknet-secret and alknet-storage is not feature-gated because these crates
|
||||||
are used in production deployments where the service layer is always active.
|
are used in production deployments where the service layer is always active.
|
||||||
|
`irpc-derive` provides the `#[rpc_requests]` proc-macro that generates
|
||||||
|
`SecretMessage` and channel plumbing. `tokio` is needed for the
|
||||||
|
`SecretServiceActor` message loop (async channel receivers and task spawning).
|
||||||
|
|
||||||
The `libsecp256k1` crate is feature-gated behind `secp256k1` because
|
The `secp256k1` crate is feature-gated behind the `secp256k1` feature because
|
||||||
Ethereum/BIP-0032 derivation is not needed in minimal deployments. Only
|
Ethereum/BIP-0032 derivation is not needed in minimal deployments. Only
|
||||||
deployments that require `DeriveEthereumKey` should enable this feature.
|
deployments that require `DeriveEthereumKey` should enable this feature. Note
|
||||||
|
that the crate name is `secp256k1` (the Rust library), not `libsecp256k1`
|
||||||
|
(the C library that the Rust crate wraps).
|
||||||
|
|
||||||
The `hkdf` and `pbkdf2` crates are deferred to Phase B. They will be needed for
|
The `hkdf` and `pbkdf2` crates are deferred to Phase B. They will be needed for
|
||||||
salt-based key stretching when key rotation is implemented (see
|
salt-based key stretching when key rotation is implemented (see
|
||||||
@@ -95,14 +105,15 @@ The crate exposes these types as its stable public interface:
|
|||||||
```rust
|
```rust
|
||||||
// Core types (always available)
|
// Core types (always available)
|
||||||
pub use mnemonic::{Mnemonic, Language, Seed};
|
pub use mnemonic::{Mnemonic, Language, Seed};
|
||||||
pub use derivation::{ExtendedPrivKey, DerivationPath, PATHS};
|
pub use derivation::{ExtendedPrivKey, DerivationError, PATHS};
|
||||||
pub use encryption::{EncryptedData, EncryptionError};
|
pub use encryption::{EncryptedData, EncryptionError};
|
||||||
pub use protocol::{SecretProtocol, DerivedKey, KeyType, SecretMessage};
|
pub use protocol::{SecretProtocol, DerivedKey, KeyType, SecretMessage};
|
||||||
pub use service::{SecretService, SecretServiceHandle, SecretServiceError};
|
pub use service::{SecretService, SecretServiceHandle, SecretServiceActor, SecretServiceError};
|
||||||
|
pub use cache::CacheConfig;
|
||||||
|
|
||||||
// secp256k1 types (behind feature flag)
|
// secp256k1 types (behind feature flag)
|
||||||
#[cfg(feature = "secp256k1")]
|
#[cfg(feature = "secp256k1")]
|
||||||
pub use derivation::Secp256k1ExtendedPrivKey;
|
pub use ethereum::Secp256k1ExtendedPrivKey;
|
||||||
```
|
```
|
||||||
|
|
||||||
Other crates consume this interface:
|
Other crates consume this interface:
|
||||||
@@ -154,8 +165,8 @@ encryption) while ensuring that `Lock` purges all sensitive material.
|
|||||||
|
|
||||||
```rust
|
```rust
|
||||||
let mnemonic = Mnemonic::from_phrase(&phrase, Language::English)?;
|
let mnemonic = Mnemonic::from_phrase(&phrase, Language::English)?;
|
||||||
let seed = mnemonic.to_seed(Some(&passphrase));
|
let seed = mnemonic.to_seed(None); // or Some("passphrase")
|
||||||
let master_key = ExtendedPrivKey::new_master(Network::Alknet, &seed)?;
|
let key = derive_path_from_seed(seed.as_bytes(), PATHS::IDENTITY)?;
|
||||||
```
|
```
|
||||||
|
|
||||||
#### SLIP-0010 Ed25519 HD Key Derivation
|
#### SLIP-0010 Ed25519 HD Key Derivation
|
||||||
@@ -193,6 +204,13 @@ does not impose a special character set — the Base64url alphabet (`A-Z`,
|
|||||||
set is required in the future, a versioned path can be introduced
|
set is required in the future, a versioned path can be introduced
|
||||||
(e.g., `m/74'/1'/1'/{hash}'`).
|
(e.g., `m/74'/1'/1'/{hash}'`).
|
||||||
|
|
||||||
|
The `SecretServiceHandle` provides two methods for password derivation:
|
||||||
|
- `derive_password(path, length)` → `Vec<u8>` (raw truncated bytes)
|
||||||
|
- `derive_password_string(path, length)` → `String` (Base64url-encoded)
|
||||||
|
|
||||||
|
The irpc `DerivePassword` variant returns raw bytes (`Vec<u8>`). Consumers
|
||||||
|
who need a string representation can Base64url-encode the result.
|
||||||
|
|
||||||
#### secp256k1 Derivation (Ethereum)
|
#### secp256k1 Derivation (Ethereum)
|
||||||
|
|
||||||
`DeriveEthereumKey` uses **BIP-0032** (not SLIP-0010) at path
|
`DeriveEthereumKey` uses **BIP-0032** (not SLIP-0010) at path
|
||||||
@@ -217,20 +235,30 @@ use `#[zeroize(drop)]` to ensure sensitive key material is overwritten before
|
|||||||
deallocation:
|
deallocation:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[derive(Zeroize)]
|
#[derive(Zeroize, Deserialize)]
|
||||||
#[zeroize(drop)]
|
#[zeroize(drop)]
|
||||||
pub struct DerivedKey {
|
pub struct DerivedKey {
|
||||||
|
#[zeroize(skip)]
|
||||||
pub key_type: KeyType,
|
pub key_type: KeyType,
|
||||||
#[zeroize]
|
#[zeroize]
|
||||||
|
#[serde(deserialize_with = "deserialize_private_key")]
|
||||||
pub private_key: Vec<u8>,
|
pub private_key: Vec<u8>,
|
||||||
|
#[zeroize(skip)]
|
||||||
pub public_key: Vec<u8>,
|
pub public_key: Vec<u8>,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Because `private_key` is zeroized on drop, `DerivedKey` cannot derive `Clone`
|
`DerivedKey` is **move-only** — it does not implement `Clone`. This is a
|
||||||
directly on the `private_key` field. Instead, `Clone` is implemented manually
|
stronger security property than manual `Clone` with zeroization of the source:
|
||||||
with a custom `clone()` that zeroizes the source's `private_key` after copying
|
a move-only type cannot be accidentally duplicated, and the `#[zeroize(drop)]`
|
||||||
it, ensuring no two `DerivedKey` instances share the same `Vec<u8>` allocation.
|
annotation ensures the `private_key` is zeroized when the key goes out of scope.
|
||||||
|
There is no risk of use-after-zeroize from a manual `clone()` that destroys
|
||||||
|
the source.
|
||||||
|
|
||||||
|
Serialization redacts `private_key` in human-readable formats (JSON shows
|
||||||
|
`"[REDACTED]"`) but preserves the actual bytes in binary formats (postcard) so
|
||||||
|
that irpc remote communication works correctly. Deserialization always reads
|
||||||
|
the full bytes.
|
||||||
|
|
||||||
### AES-256-GCM Encryption for External Credentials
|
### AES-256-GCM Encryption for External Credentials
|
||||||
|
|
||||||
@@ -299,32 +327,20 @@ enum SecretProtocol {
|
|||||||
|
|
||||||
#[rpc(tx=oneshot::Sender<()>)]
|
#[rpc(tx=oneshot::Sender<()>)]
|
||||||
#[wrap(Unlock)]
|
#[wrap(Unlock)]
|
||||||
Unlock { passphrase: String },
|
Unlock { mnemonic: String, passphrase: Option<String> },
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct DerivedKey {
|
|
||||||
key_type: KeyType,
|
|
||||||
private_key: Vec<u8>,
|
|
||||||
public_key: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
enum KeyType {
|
|
||||||
Ed25519,
|
|
||||||
Aes256Gcm,
|
|
||||||
Secp256k1,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct EncryptedData {
|
|
||||||
key_version: u32,
|
|
||||||
salt: String, // Base64-encoded (reserved for future KDF, not used in v1)
|
|
||||||
iv: String, // Base64-encoded
|
|
||||||
data: String, // Base64-encoded
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note**: The `Unlock` variant carries both the mnemonic phrase and an optional
|
||||||
|
BIP39 passphrase. The `mnemonic` field is the space-separated BIP39 word list.
|
||||||
|
The `passphrase` field is the optional BIP39 password extension (sometimes
|
||||||
|
called the "25th word"). Most deployments use `passphrase: None`, but the field
|
||||||
|
is available for users who need additional security beyond the mnemonic alone.
|
||||||
|
|
||||||
|
> **Implementation gap**: The current code has `Unlock { passphrase: String }`
|
||||||
|
> with only a single field (the mnemonic), and the actor handler passes `None`
|
||||||
|
> for the BIP39 passphrase. This needs to be updated to match the spec above.
|
||||||
|
> See the `unlock-passphrase-gap` task.
|
||||||
|
|
||||||
#### irpc Integration Model
|
#### irpc Integration Model
|
||||||
|
|
||||||
The `SecretProtocol` enum defines the **wire protocol** — the set of operations
|
The `SecretProtocol` enum defines the **wire protocol** — the set of operations
|
||||||
@@ -351,6 +367,12 @@ There are two dispatch paths for consuming the secret service:
|
|||||||
nodes use this path. The seed never leaves the secret service node — only
|
nodes use this path. The seed never leaves the secret service node — only
|
||||||
derived keys are transmitted.
|
derived keys are transmitted.
|
||||||
|
|
||||||
|
The `SecretServiceActor` processes incoming `SecretMessage` variants by
|
||||||
|
dispatching to the corresponding `SecretServiceHandle` methods. It provides
|
||||||
|
a `spawn(handle)` convenience method that creates an mpsc channel, spawns the
|
||||||
|
actor on a tokio task, and returns a `(Client<SecretProtocol>, SecretServiceActor)`
|
||||||
|
tuple for immediate use.
|
||||||
|
|
||||||
The `SecretService` type owns the irpc service handler and a
|
The `SecretService` type owns the irpc service handler and a
|
||||||
`SecretServiceHandle`. It dispatches incoming `SecretMessage` variants to the
|
`SecretServiceHandle`. It dispatches incoming `SecretMessage` variants to the
|
||||||
handle's methods. For call protocol exposure (e.g., `/head/secrets/derive`),
|
handle's methods. For call protocol exposure (e.g., `/head/secrets/derive`),
|
||||||
@@ -419,7 +441,7 @@ test vectors:
|
|||||||
|
|
||||||
These tests ensure that the implementation is correct and compatible with
|
These tests ensure that the implementation is correct and compatible with
|
||||||
other BIP39/SLIP-0010/AES-256-GCM implementations. They are placed in
|
other BIP39/SLIP-0010/AES-256-GCM implementations. They are placed in
|
||||||
`tests/vectors_tests.rs`.
|
`tests/test_vectors.rs`.
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
|
|
||||||
@@ -439,11 +461,12 @@ other BIP39/SLIP-0010/AES-256-GCM implementations. They are placed in
|
|||||||
(postcard serialization). For call protocol exposure (e.g.,
|
(postcard serialization). For call protocol exposure (e.g.,
|
||||||
`/head/secrets/derive`), the service is wrapped in an operation that
|
`/head/secrets/derive`), the service is wrapped in an operation that
|
||||||
serializes to JSON.
|
serializes to JSON.
|
||||||
- `DerivedKey.private_key` must derive `Zeroize` per ADR-038. Clone is
|
- `DerivedKey.private_key` must derive `Zeroize` per ADR-038. `DerivedKey`
|
||||||
implemented manually to zeroize the source on clone.
|
is move-only (not `Clone`) — this is stronger than manual Clone with
|
||||||
|
zeroization of the source, as it prevents accidental duplication.
|
||||||
- secp256k1 (Ethereum) derivation is gated behind the `secp256k1` feature flag
|
- secp256k1 (Ethereum) derivation is gated behind the `secp256k1` feature flag
|
||||||
because it requires a different derivation algorithm (BIP-0032) and an
|
because it requires a different derivation algorithm (BIP-0032) and an
|
||||||
additional dependency (`libsecp256k1`).
|
additional dependency (`secp256k1`).
|
||||||
|
|
||||||
## Phase Progression
|
## Phase Progression
|
||||||
|
|
||||||
|
|||||||
109
tasks/integration/phase3/secret-service/unlock-passphrase-gap.md
Normal file
109
tasks/integration/phase3/secret-service/unlock-passphrase-gap.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
id: unlock-passphrase-gap
|
||||||
|
name: Fix Unlock protocol variant to carry both mnemonic and BIP39 passphrase
|
||||||
|
status: pending
|
||||||
|
depends_on: [irpc-secret-protocol-integration]
|
||||||
|
scope: narrow
|
||||||
|
risk: low
|
||||||
|
impact: component
|
||||||
|
level: 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:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[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:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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.
|
||||||
Reference in New Issue
Block a user