OneCipher design document — staged evolution of the architecture, storage, and cryptographic primitives.
Fixed critical security vulnerabilities: empty-password signing paths, Passkey verification stubs, x402 amount parsing, audit log keys, and CLI client connections. Established the HardenedBytes memory-hardening contract and the R56 dependency isolation hard gates.
Merged the dual-process architecture (Key-Agent + Network-Agent) into a
single onecipher binary. Created the signing-core facade (the
oc-signer crate; formerly referenced as oc-signing-core) and used
tokio::task::spawn_blocking for async→sync bridging. The Key-Agent
runs as a sync std::thread with UDS; the WC v2 server runs on tokio.
- Intent Layer (
oc-netagent::intent): Declarative intent framing, simulation, and execution for Pay/SignTransaction/SignMessage/CrossChainTransfer. - Real Session Keys: ERC-7579 (EVM) and Session Tokens (Solana).
- Policy v3: Cedar-like DSL with permit/forbid rules.
- CLI integration:
onecipher intentcommand. - Paymaster (ERC-4337 gas abstraction): owned by the sister project
ledgerflow; OneCipher acts as its WalletSigner via the loopback JSON-RPC server.
Documented but not yet implemented: TEE-based subprocess enclave, cross-chain routing via ERC-7683, and Cedar-policy full integration.
- The pre-unification architecture was wallet-specific, with three parallel encryption stacks running side by side (since removed by the age flag day: Argon2id + AES-GCM-SIV for wallets, Argon2id + XChaCha20 for
.ocbkbackups, and HKDF + AES-GCM for API tokens). - The goal is to unify these into a general-purpose secret vault that supports private keys, passwords, and TOTP secrets.
- Inspiration: ripasso (one file per secret + directory-tree namespaces + git integration) and sops (age with multiple recipients).
- age is chosen as the single encryption layer: pure Rust, no system gpg dependency, native multi-recipient support, and interoperable with rage.
ItemTypeenum:Mnemonic/PrivateKey/Password/Totp/Note/File.SecretEntrystruct:id/name/item_type/created_at/updated_at/metadata/encrypted_payload.SecretPayloadstruct (post-decryption):secret/notes/extra.KeyTyperemains compatible (Mnemonic/PrivateKeyare subsets ofItemType).
- Key model: an age X25519 master identity (
~/.onecipher/keys/age-identity.txt) plus a.age-recipientsfile listing multiple recipients. - Passphrase fallback: age scrypt mode (for devices without a local identity).
- Encryption flow: read
.age-recipients→age::Encryptor::for_recipients→ write the age envelope. - Decryption flow:
age::Decryptor→ try the local identity → on failure, prompt for a passphrase. - No sops-style data key wrapping; age's native multi-recipient stanza is sufficient.
- Directory layout:
~/.onecipher/secrets/tree structure, with leaf nodes as.agefiles. .age-recipients: one bech32 age public key per line, with support for per-subdirectory overrides.- Single-file format: standard age binary envelope (no custom header).
- Decrypted payload: JSON
{ "secret", "notes", "extra" }. - TOTP: the
otpauth://URI is stored in thesecretfield. - Index:
~/.onecipher/index.jsonl(plaintext metadata + an Ed25519 signature) to enable fast search.
- New
oc-secretcrate:age.rs/entry.rs/store.rs/recipients.rs/totp.rs/migrate.rs/git.rs. - Dependencies:
age/totp-rs/oc-core/oc-crypto/oc-keyagent(audit) — all synchronous, R56-compliant. oc-vault: generalize theVaultwrapper; age backup bundles (export_backup/import_backup, the formerBackupContainerremoved by the age flag day).oc-wallet: remove the duplicatedvault.rs; routedecrypt_signing_keythroughoc-secret.oc-policy: extend withread_secret/write_secretoperations.oc-keyagent: extend auditEventTypewithSecretRead/SecretWritten/SecretDeleted/SecretMigrated/AgeRecipientAdded/AgeReencrypted.
- Framework:
ratatui+crossterm. - Layout: search box + tree list + status bar.
- Key bindings:
j/kmove,/search,Enteropen,nnew,eedit,ddelete,ccopy (auto-clear after 40s),tTOTP,ggit,qquit. - State: an
Appstruct holdsentries/filtered/selected/mode/clipboard_clear_at/totp_codes. - The TUI shares the
oc-secretlibrary with the CLI; feature parity is enforced.
secret list/get/add/update/delete/rename(--json/--stdin).password add/get/generate.totp add/generate/uris.age init/recipient add/list/identity show/reencrypt.migrate legacy-wallets.git pull/push/log.tuilaunch.- Global
--jsonflag,--stdininput, exit-code semantics (0/1/2/3/4). - API token mode is extended to cover secret operations.
- Progressive migration:
.json→.age, with legacy files retained read-only. migrate --rollbackreverts the migration.- The
EncryptedWalletformat remains supported for compatibility.
- Phase 1: type layer +
oc-secretcrate + age integration + BDD. - Phase 2: CLI
secret/password/totpcommands + policy/audit extension. - Phase 3: migration +
oc-walletrefactor +oc-vaultgeneralization. - Phase 4: TUI (
ratatui). - Phase 5: git sync (
libgit2). - Phase 6: AI Agent extension (API token + policy + daemon).
- R56:
age/totp-rs/arboardare synchronous libraries and MUST NOT transitively pulltokio/reqwest;ratatui/crosstermare confined tooc-cli.git2(libgit2 + libssh2 + OpenSSL) is synchronous and R56-compliant, but is gated behind the optionalgitfeature ofoc-secretso that environments without a working libssh2 toolchain can still build the release binary. - R51/R52:
oc-cryptostays zero-I/O;ageMUST NOT be added tooc-crypto. - R55:
oc-keyagentremains tokio-free. - R12: The release binary MUST NOT contain TCP-specific symbols (
TcpListener,TcpStream,AF_INET). Phase 1-6 changes add only file I/O, terminal rendering, and (optionally) libgit2 sync — no direct TCP code paths. Verified vianmsymbol inspection. - Memory hardening: Signing key material (mnemonics, private keys) MUST flow through
HardenedBytes. Documented exemption:SecretPayload.secret(oc-core/src/secret.rs) remains aStringwith a customDropzeroize — this is a formally accepted deviation: the payload covers passwords/TOTP/notes whose CLI--jsonoutput contract requires plain string serialization, it never holds wallet signing keys, and serde round-trips would otherwise copy plaintext through unhardened JSON buffers anyway. Callers that need page-locking should route viaoc_crypto::HardenedBytesat point of use viaSecretPayload::secret_hardened()/into_secret_hardened()(oc-corehardenedfeature) oroc_secret::SecretEntry::decrypt_hardened(). Any future signing-key field on this type MUST useHardenedBytes.
| Crate | Feature | Default | Description |
|---|---|---|---|
oc-secret |
git |
off | Enables the oc_secret::git module (libgit2 sync). When disabled, SecretStore auto-commit is a no-op and the onecipher git subcommand is unavailable. |
oc-cli |
git |
on | Forwards to oc-secret/git. Enables the onecipher git subcommand. Disable with --no-default-features for environments without libssh2. |
oc-wallet |
rpc |
off | Enables hpx/tokio RPC client (R56: keeps oc-keyagent clean). |
oc-wallet |
sui-grpc |
off | Enables Sui gRPC verification. |
The Intent Layer provides a declarative interface for AI agents to
express signing and payment intentions (e.g. "pay 10.5 USDC to 0xABC
on Base") without constructing raw transactions. Intents flow through
three stages: Simulated (pre-flight eth_call + eth_estimateGas
to produce a human-readable summary), Confirmed (user/Passkey
approves the summary), and Executed (signed and broadcast,
optionally via the Paymaster for gasless transactions).
Key types: Intent, IntentKind (Pay / SignTransaction /
SignMessage / CrossChainTransfer), IntentStatus, IntentResult,
IntentSummary, MessageEncoding.
Key functions: simulate_intent, execute_intent.
simulate_intent and execute_intent both take an &dyn RpcClient
trait object. oc-netagent::intent stays decoupled from the Key-Agent by
depending only on this RpcClient abstraction (gas estimation, tx
construction, send_raw_transaction, receipt polling); real RPC
implementations are supplied by oc-netagent. execute_intent builds
an unsigned EIP-1559 transaction and forwards it through the RPC
client — it does not sign directly. MockRpcClient backs unit tests.
Honest status (C2): as of this revision the Intent Layer is not on the production signing path — the three production entry points (WC v2
WcMethodRouter, HTTP-RPC, and WalletSigner loopbackwallet-rpc) all bypassoc-netagent::intentand forward directly to the Key-Agent via UDSKeyAgentRequestframes, returningsigned_tx_hexfor the dApp to broadcast.simulate_intent/execute_intentare exercised only by unit tests and CLI (onecipher intent simulate/execute). The layer is currently a CLI-adjacent library, not the core execution path described above. Additionally,HpxRpcClient::native_price_usdis a stub (returns an error), sosimulate_intentfails under a real RPC client until a price feed is integrated. Wiring the Intent Layer into any hot path is tracked and must be done behind an explicit opt-in to avoid breaking existing clients.
Session keys enable delegated signing for AI agents without exposing
the master key. EVM uses ERC-7715 grantPermission on an ERC-7579
SCA; Solana uses the Session Tokens program. Per R21, the crate
defines the SessionKeyProvider trait unifying grant /
verify_active / revoke / sign_with across chains. Per R56, the
crate MUST NOT depend on tokio / reqwest / tungstenite / hyper /
async-std / smol — it uses native async fn (edition 2024) returning
runtime-agnostic Pin<Box<dyn Future>> futures; the caller
(Net-Agent) supplies the executor.
Phase 1 ships EvmSessionKeyProvider (evm.rs) and
SolanaSessionKeyProvider (solana.rs), both backed by the
MockRpcClient in rpc.rs. Phase 2 (real.rs) defines the
EvmRpcClient, EvmBundlerClient, and SolanaRpcClient traits for
injectable real providers; the real on-chain RPC implementations
(alloy / solana-client) are wired up in oc-netagent.
Key types: SessionKeyProvider, GrantReceipt, KeyScheme,
OwnerKey, PublicKey, SessionPrivateKey, SignPayload,
Signature, SolanaInstruction.
Key function: derive_session_key_id — format
sk-{chain_namespace}-0x{8-byte hash}, where the hash is the first
8 bytes of SHA-256("onecipher-session-key" || session_pubkey || chain_id).
Deviation note (R74 YAGNI): Phase 1/2 use SHA-256 for the Merkle
root and session-key ID derivation instead of keccak256. keccak256
lives in oc-netagent where the alloy dependency is available; the
real Merkle tree + ABI encoding is a Phase 2+ concern.