jacs_core/material.rs
1//! `AgentMaterial` and `UnlockSecret`: the inputs to `CoreAgent::from_encrypted_material`.
2//!
3//! `AgentMaterial` is the serializable bundle a browser caller (or any
4//! storage-agnostic loader) holds for an agent: the agent's JACS document,
5//! its config, its public key, the encrypted private-key envelope, and
6//! the signing algorithm. It is the over-the-wire shape for
7//! `localStore.saveEncryptedAgent` / `loadEncryptedAgent` in
8//! `jacs-wasm::local_store`.
9//!
10//! `UnlockSecret` is the password / raw-key choice the caller passes when
11//! constructing a `CoreAgent`. `Password` runs the encrypted envelope through
12//! the `envelope::decrypt_private_key` sniffer (V2 Argon2id JSON plus legacy
13//! PBKDF2 raw binary). `RawPrivateKey` skips decryption entirely, used
14//! internally by `CoreAgent::ephemeral` and by callers who already hold the
15//! decrypted bytes.
16//!
17//! See PRD §4.2.
18
19use crate::sign::SigningAlgorithm;
20use secrecy::SecretBox;
21use serde::{Deserialize, Serialize};
22use serde_json::Value;
23
24/// Persisted bundle for an encrypted JACS agent.
25///
26/// The shape is JSON-friendly so it can be written to `localStorage` (via
27/// `jacs-wasm::local_store::save_encrypted_agent`) as a single string blob
28/// without further unpacking. The two `Vec<u8>` fields are
29/// base64-serialized by `serde_json` when the bundle is JSON-encoded
30/// (via the default `serde(with = …)` path below).
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct AgentMaterial {
33 /// The agent's `jacs.config.json` contents (parsed as JSON).
34 pub config: Value,
35 /// The agent's JACS document (the `agent.json` body, including
36 /// `jacsId`, `jacsVersion`, `jacsSignature`, etc.).
37 pub agent: Value,
38 /// Raw public-key bytes (algorithm-specific encoding — Ed25519 is the
39 /// 32-byte verifying key, pq2025 is the 2592-byte ML-DSA-87 public
40 /// key).
41 #[serde(with = "base64_bytes")]
42 pub public_key: Vec<u8>,
43 /// The encrypted private-key envelope. Either the V2 Argon2id JSON
44 /// envelope (current writer) or the legacy raw-binary PBKDF2 envelope
45 /// (legacy reader). `envelope::decrypt_private_key` sniffs which one
46 /// it is.
47 #[serde(with = "base64_bytes")]
48 pub encrypted_private_key: Vec<u8>,
49 /// The signing algorithm tied to the keys above.
50 pub algorithm: SigningAlgorithm,
51}
52
53/// Caller's choice for how to unlock the encrypted private key.
54///
55/// Borrowing here lets the caller keep ownership of the password
56/// string / raw-key buffer. The lifetime of the underlying secret is
57/// the caller's concern; `CoreAgent::from_encrypted_material` only
58/// reads from it during construction.
59pub enum UnlockSecret<'a> {
60 /// Run the password through the envelope decryptor. The password
61 /// itself is borrowed — it is never copied into the resulting
62 /// `CoreAgent`, only the decrypted private key bytes are (and
63 /// those are wrapped + zeroized).
64 Password(&'a str),
65 /// Skip decryption. The provided bytes are interpreted directly as
66 /// the algorithm-specific raw private key (Ed25519 PKCS#8 or raw
67 /// 32-byte scalar; pq2025 ML-DSA-87 4896-byte private key). Used
68 /// by `CoreAgent::ephemeral` and by callers who already hold the
69 /// decrypted bytes (for example after running a custom key store).
70 RawPrivateKey(SecretBox<Vec<u8>>),
71}
72
73// -----------------------------------------------------------------------------
74// Internal: base64 helper for `Vec<u8>` fields so the JSON form is small and
75// human-readable. Mirrors how the native side encodes binary fields in
76// configs / agent JSON documents.
77// -----------------------------------------------------------------------------
78
79mod base64_bytes {
80 use base64::{Engine as _, engine::general_purpose::STANDARD};
81 use serde::{Deserialize, Deserializer, Serializer};
82
83 pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
84 serializer.serialize_str(&STANDARD.encode(bytes))
85 }
86
87 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
88 let encoded = String::deserialize(deserializer)?;
89 STANDARD
90 .decode(encoded.as_bytes())
91 .map_err(serde::de::Error::custom)
92 }
93}