Skip to main content

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}