Files
alknet/tasks/integration/phase3/secret-service/derive-password-implementation.md
glm-5.1 9ec7627d80 chore: add Phase 3 secret-service decomposition tasks
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.
2026-06-10 04:14:39 +00:00

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
spec-update-secret-service
derivedkey-zeroize-security
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 length bytes, 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:

  1. Add SecretServiceHandle::derive_password(&self, path: &str, length: usize) -> Result<Vec<u8>, SecretServiceError>

  2. The implementation:

    • Derive a key at the given path using SLIP-0010 (same as Ed25519 derivation)
    • Take the first length bytes 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 DerivePassword return raw bytes (Vec<u8>) or an encoded string (String)? The spec says the protocol variant returns Vec<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() returns Vec<u8> (raw derived bytes). A convenience method derive_password_string(path: &str, length: usize) -> Result<String, SecretServiceError> returns the Base64url-encoded string for use as an actual password. The protocol variant returns Vec<u8>.

  3. Add SecretProtocol::DerivePassword dispatch to the service actor (irpc task depends on irpc-secret-protocol-integration).

  4. The derive_password method in SecretServiceHandle:

    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 added
  • derive_password returns the first length bytes of the derived private key at the given path
  • derive_password requires unlocked state (returns ServiceLocked if locked)
  • SecretServiceHandle::derive_password_string(&self, path: &str, length: usize) -> Result<String, SecretServiceError> convenience method returns Base64url-encoded string
  • derive_password uses the key cache (if key-caching-ttl task is complete)
  • Unit test: derive_password at a known path returns deterministic bytes
  • Unit test: derive_password at the same path returns the same bytes (deterministic)
  • Unit test: derive_password at a different path returns different bytes
  • Unit test: derive_password length parameter truncates correctly
  • Unit test: derive_password_string returns valid Base64url (no padding)
  • Unit test: derive_password returns ServiceLocked error 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 length parameter in DerivePassword specifies 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_PREFIX pattern (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