9 atomic tasks for alknet-secret spec conformance and gap closure, derived from architect's implementation review. Dependencies form a 5-generation graph starting with spec update, then parallel implementation tasks, ending with a review gate. Tasks address: DerivedKey zeroize security, key caching with TTL, irpc protocol integration, password derivation, secp256k1/Ethereum derivation, encryption salt/KDF, crypto test vectors, and final spec conformance review.
5.1 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | ||
|---|---|---|---|---|---|---|---|---|---|
| derive-password-implementation | Implement deterministic password derivation for DerivePassword | pending |
|
narrow | low | component | implementation |
Description
The SecretProtocol::DerivePassword variant exists in the protocol enum but has no corresponding service method. The spec (after update) defines deterministic password derivation as:
- Algorithm: HMAC-SHA512 at the derivation path
m/74'/1'/0'/{hash}'where{hash}'is a site-specific hardened index - Output: Truncate the derived key material to
lengthbytes, encode as Base64url (URL-safe Base64 without padding) - Path format:
m/74'/1'/0'/{hash}'— SLIP-0010 hardened-only derivation
The current SecretServiceHandle has methods for DeriveEd25519, DeriveEncryptionKey, DeriveEthereumKey, Encrypt, and Decrypt, but no derive_password.
Implementation:
-
Add
SecretServiceHandle::derive_password(&self, path: &str, length: usize) -> Result<Vec<u8>, SecretServiceError> -
The implementation:
- Derive a key at the given path using SLIP-0010 (same as Ed25519 derivation)
- Take the first
lengthbytes of the private key material - Encode as Base64url (no padding) using
base64::engine::general_purpose::URL_SAFE_NO_PAD - The output is a deterministic password string that can be regenerated from the same seed + path
Wait — there's a design choice here. Should
DerivePasswordreturn raw bytes (Vec<u8>) or an encoded string (String)? The spec says the protocol variant returnsVec<u8>, but a "password" is typically a string. Let me check the protocol definition more carefully.The protocol says:
#[rpc(tx=oneshot::Sender<Vec<u8>>)] #[wrap(DerivePassword)] DerivePassword { path: String, length: usize },So the return type is
Vec<u8>. The encoding to a usable password string should happen at the call site or be a separate method. For the protocol, return raw derived bytes.Resolution:
derive_password()returnsVec<u8>(raw derived bytes). A convenience methodderive_password_string(path: &str, length: usize) -> Result<String, SecretServiceError>returns the Base64url-encoded string for use as an actual password. The protocol variant returnsVec<u8>. -
Add
SecretProtocol::DerivePassworddispatch to the service actor (irpc task depends onirpc-secret-protocol-integration). -
The
derive_passwordmethod inSecretServiceHandle:pub fn derive_password(&self, path: &str, length: usize) -> Result<Vec<u8>, SecretServiceError> { let inner = self.inner.read().unwrap(); if !inner.unlocked { return Err(SecretServiceError::ServiceLocked); } let seed = inner.seed.as_ref().ok_or(SecretServiceError::ServiceLocked)?; let key = derivation::derive_path_from_seed(seed.as_bytes(), path)?; // Return first `length` bytes of private key let result = key.private_key()[..length.min(key.private_key().len())].to_vec(); Ok(result) }
Acceptance Criteria
SecretServiceHandle::derive_password(&self, path: &str, length: usize) -> Result<Vec<u8>, SecretServiceError>method addedderive_passwordreturns the firstlengthbytes of the derived private key at the given pathderive_passwordrequires unlocked state (returnsServiceLockedif locked)SecretServiceHandle::derive_password_string(&self, path: &str, length: usize) -> Result<String, SecretServiceError>convenience method returns Base64url-encoded stringderive_passworduses the key cache (ifkey-caching-ttltask is complete)- Unit test:
derive_passwordat a known path returns deterministic bytes - Unit test:
derive_passwordat the same path returns the same bytes (deterministic) - Unit test:
derive_passwordat a different path returns different bytes - Unit test:
derive_passwordlength parameter truncates correctly - Unit test:
derive_password_stringreturns valid Base64url (no padding) - Unit test:
derive_passwordreturnsServiceLockederror when service is locked
References
- docs/architecture/secret-service.md — Key derivation, password derivation path
- crates/alknet-secret/src/service.rs — SecretServiceHandle (add derive_password)
- crates/alknet-secret/src/protocol.rs — SecretProtocol::DerivePassword variant
- crates/alknet-secret/src/derivation.rs — derive_path_from_seed, PATHS
Notes
The
lengthparameter inDerivePasswordspecifies bytes, not characters. Since Ed25519 derived keys are 32 bytes, the maximum useful length is 32. For longer passwords, the spec says to use Base64url encoding of the full 32 bytes, which gives a ~43-character string. Password managers typically want 16-32 byte keys encoded as ~22-43 character strings.
The
PATHS::DEVICE_PREFIXpattern (m/74'/0'/0') allows parameterized device identity. Similarly,site_password_path(hash)(m/74'/1'/0'/{hash}') allows site-specific passwords. Both work with the same derivation function — the path is just different.
Summary
To be filled on completion