Key isolation, memory hardening, threat model, and conformance requirements for OneCipher implementations.
1. OneCipher receives a sign request
2. Authenticate the caller with explicit request-scoped authorization
3. Evaluate attached policies before decryption when the surface requires them
4. Read the encrypted wallet or API-key-backed secret from disk
5. Derive the decryption key (Passkey/device-bound token/passphrase path, depending on the surface)
6. Decrypt key material (mnemonic or private key) into hardened memory
7. Derive the chain-specific signing key if needed
8. Sign the payload
9. Immediately zero out decrypted mnemonic/private key bytes, derived signing key bytes, and KDF-derived key bytes
10. Return only the signature or signed payload
Immediate zeroization is critical. In the Rust implementation this is handled with HardenedBytes — page-locked (mlock), DONT_DUMP-marked (MADV_DONTDUMP), and zeroized on drop.
All sensitive material (mnemonics, private keys, passphrases) flows through HardenedBytes:
| Property | Mechanism |
|---|---|
| Page locking | mlock() prevents swapping to disk |
| Dump protection | MADV_DONTDUMP excludes from core dumps |
| Zeroize on drop | Cryptographic zeroing when value goes out of scope |
| Scope | Used in oc-crypto, oc-signer |
The oc-crypto crate has zero I/O and zero network dependencies (R51/R52). It is the security foundation of the entire stack.
The CLI prompts for the passphrase when an owner-driven flow needs it.
Local signing-sensitive JSON-RPC and WalletSigner requests carry a fresh
PasskeyAuthorization proof. Read-only helper methods such as health checks,
wallet listing, challenge minting, and balance reads remain unauthenticated,
but any auth-class signing or wallet-rpc operation verifies the
challenge/signature pair before it proceeds.
WalletConnect-owned auth flows (wc_authRequest, daemon-controlled
onecipher_signAuth) do not forward a passkey proof over the relay. Instead,
the daemon injects a startup-minted internal token that the Key-Agent validates
before deriving the device-bound unlock token.
The CLI reads ONECIPHER_PASSPHRASE and clears it immediately after reading.
Warning: Environment variables remain the weakest supported owner credential delivery mechanism. They can leak via process inspection, crash dumps, or child-process inheritance if not cleared promptly.
| Threat | Mitigation |
|---|---|
| Agent/LLM misuses a wallet via automation | Local automation must present a fresh Passkey proof; daemon-internal flows require a startup-minted token and can still be approval-gated |
| Key leaked to logs | OneCipher does not log key material; audit logging records operations only |
| Core dump contains keys | Process hardening disables core dumps / attach where supported |
| Swap file contains keys | Hardened secret buffers use mlock() where available |
| Cold boot / memory forensics | Keys are zeroized immediately after signing; exposure window is short |
| Compromised process memory | Not fully mitigated in the current in-process model; a future subprocess enclave would address this |
| Passphrase brute force | Argon2id slows offline guessing; wallet envelopes use a minimum work factor of 2^16 |
| Token stolen, no disk access | Useless — encrypted key file not accessible |
| Disk access, no token | Can't decrypt — HKDF + AES-256-GCM |
| Token + disk access | Can decrypt, but requires bypassing OneCipher process entirely |
| Owner passphrase changed | API keys unaffected (independently encrypted) |
| API key revoked | Encrypted copy deleted — token decrypts nothing |
Decrypting key material via Argon2id adds latency. The implementation maintains a short-lived, in-memory cache of derived key material:
| Property | Requirement |
|---|---|
| TTL | No more than 30 seconds; 5 seconds recommended |
| Max entries | Bounded (32 entries) with LRU eviction |
| Memory protection | Cached key material MUST be mlock()'d and zeroized on eviction |
| Signal handling | Cache MUST be cleared on SIGTERM, SIGINT, and SIGHUP before process exit |
| Cache key | Derived from `SHA-256(mnemonic |
Caller → sign_transaction / sign_message / signAuth(...)
│
└─► onecipher-lib (same process)
├── passkey or daemon-token authorization
├── policy evaluation where applicable
├── decrypt wallet secret (mlock'd, zeroized on drop)
├── sign
└── return signature
Policy enforcement is handled by the code path, but transport locality alone is not treated as authorization. The caller and signer share an address space; the current model therefore relies on explicit authorization material plus in-process hardening (mlock, zeroize, anti-ptrace, anti-coredump) to reduce the window for key extraction.
Agent → sign_transaction(wallet, chain, tx, "ows_key_...")
│
└─► onecipher-lib (parent process)
├── token lookup + policy evaluation
└── fork/exec onecipher-enclave
├── receive (token, wallet_id, tx) over stdin
├── HKDF decrypt wallet secret
├── sign
├── zeroize
├── write signature to stdout
└── exit
The decrypt→sign→wipe path moves to a child process. The parent never has the decrypted secret in its address space. The child is stateless — spawned per request, no daemon, no unlock step.
An implementation claiming OneCipher conformance MUST declare the profiles it supports:
OneCipher <supported profiles>
Examples:
OneCipher Storage + Signing + Policy + EVM Chain ProfileOneCipher Storage + Signing + Lifecycle + Solana Chain Profile
Conforming implementations SHOULD ship or consume machine-readable test vectors for:
- Wallet file decryption and encryption
- API key file resolution and token verification
- Policy rule evaluation
- Chain-specific address derivation
- Transaction and message signing
When two conforming implementations exchange OneCipher artifacts, the following MUST remain interoperable:
- Wallet files can be parsed and validated consistently
- API key files can be resolved consistently by token hash
- Policy files produce the same allow or deny result for the same
PolicyContext - Canonical chain and account identifiers are preserved without lossy conversion
Implementations MUST preserve the error meanings defined by the Signing Interface. They MUST NOT:
- Turn a policy denial into a generic authentication failure
- Collapse unsupported-chain errors into malformed-input errors
- Treat expired API keys as missing keys
Secret Material — implementations MUST:
- Decrypt wallet or API-key-backed secret material only for the duration of the operation
- Zeroize decrypted mnemonic, private key, derived key, and KDF output buffers after use
- Avoid writing decrypted secret material to logs, telemetry, or audit records
Credential Handling — implementations MUST:
- Treat owner credentials and API tokens as secrets
- Avoid echoing credentials in logs or human-readable errors
- Verify API token scope and policy attachments before any token-backed secret is decrypted
Policy Enforcement — implementations MUST:
- Evaluate built-in policy rules deterministically
- Short-circuit on denial when the policy model requires it
- Deny the request if an executable policy exits unsuccessfully or returns malformed output
Implementations MUST NOT provide a fallback path that bypasses token-attached policy evaluation.
Audit Logging — audit logs MUST be append-only. Records SHOULD include operation type, wallet identifier, chain identifier, API key identifier, allow/deny outcome, and timestamp. Records MUST NOT contain raw passphrases, API tokens, mnemonics, or private keys.