Skip to main content

jacs_core/
errors.rs

1//! `jacs-core` error type.
2//!
3//! `CoreError` is the protocol-layer error enum surfaced by every operation
4//! in `jacs-core` (sign, verify, envelope decrypt, agreement quorum, schema
5//! lookup, …). It is **deliberately** independent of `jacs::JacsError` so
6//! `jacs-core` can compile for `wasm32-unknown-unknown` without dragging in
7//! native-only crates. The native facade converts via `From<CoreError> for
8//! jacs::JacsError` (lives in `jacs/src/error.rs`).
9//!
10//! ## Serialization contract
11//!
12//! Every `CoreError` serializes as
13//!
14//! ```json
15//! { "code": "VariantName", "message": "human readable text", "details": { … } }
16//! ```
17//!
18//! `details` is only present when the variant carries structured fields
19//! (e.g. `AlgorithmMismatch { expected, actual }`); single-`String`
20//! variants omit it. The shape is stable — `jacs-wasm` exposes it as
21//! `JacsWasmError` to browser callers, and the `code` discriminator is
22//! load-bearing for client-side error handling. See PRD §3.1.
23
24use serde::Serialize;
25use serde::ser::SerializeStruct;
26use thiserror::Error;
27
28/// Protocol-layer error.
29///
30/// One variant per failure mode. Variant names are stable wire identifiers
31/// (they appear as the `code` field of the serialized error); do not rename
32/// without bumping the JACS WASM contract.
33#[derive(Debug, Error, Clone, PartialEq, Eq)]
34pub enum CoreError {
35    /// The supplied password does not unlock the encrypted private key.
36    #[error("invalid password")]
37    InvalidPassword,
38
39    /// The supplied password is structurally invalid (e.g. empty).
40    #[error("invalid password format: {0}")]
41    InvalidPasswordFormat(String),
42
43    /// The agent has been locked via `clear_secrets`; sign operations are
44    /// rejected until re-unlocked. Verification still works.
45    #[error("agent is locked; call unlock before signing")]
46    Locked,
47
48    /// The caller asked for one algorithm but the loaded material is a
49    /// different one.
50    #[error("algorithm mismatch: expected {expected}, got {actual}")]
51    AlgorithmMismatch {
52        /// The algorithm the caller requested.
53        expected: String,
54        /// The algorithm actually present on the loaded material.
55        actual: String,
56    },
57
58    /// The algorithm identifier or envelope magic is unknown / reserved.
59    #[error("unsupported algorithm: {0}")]
60    UnsupportedAlgorithm(String),
61
62    /// The JACS document is structurally invalid (missing field, wrong
63    /// type, malformed canonical bytes, …).
64    #[error("malformed document: {0}")]
65    MalformedDocument(String),
66
67    /// A key blob is malformed (wrong length, bad PKCS#8 structure, …).
68    #[error("malformed key: {0}")]
69    MalformedKey(String),
70
71    /// The encrypted-key envelope is structurally invalid (short, bad
72    /// JSON, missing field, …). Distinct from `InvalidPassword`.
73    #[error("malformed envelope: {0}")]
74    MalformedEnvelope(String),
75
76    /// The cryptographic signature did not verify against the supplied
77    /// public key + payload.
78    #[error("signature invalid: {0}")]
79    SignatureInvalid(String),
80
81    /// AEAD encryption failed.
82    #[error("encryption failed: {0}")]
83    EncryptionFailed(String),
84
85    /// AEAD decryption failed (tag mismatch, KDF error, …). Distinct from
86    /// `InvalidPassword`, which is the specific case where the password
87    /// itself was wrong.
88    #[error("decryption failed: {0}")]
89    DecryptionFailed(String),
90
91    /// JSON schema validation failed.
92    #[error("schema invalid: {0}")]
93    SchemaInvalid(String),
94
95    /// Multi-party agreement quorum / payload check failed.
96    #[error("agreement failed: {0}")]
97    AgreementFailed(String),
98}
99
100impl CoreError {
101    /// Stable wire identifier for this error (the `code` field of the
102    /// serialized payload). Match this in client code instead of comparing
103    /// to the localized `message`.
104    pub fn code(&self) -> &'static str {
105        match self {
106            CoreError::InvalidPassword => "InvalidPassword",
107            CoreError::InvalidPasswordFormat(_) => "InvalidPasswordFormat",
108            CoreError::Locked => "Locked",
109            CoreError::AlgorithmMismatch { .. } => "AlgorithmMismatch",
110            CoreError::UnsupportedAlgorithm(_) => "UnsupportedAlgorithm",
111            CoreError::MalformedDocument(_) => "MalformedDocument",
112            CoreError::MalformedKey(_) => "MalformedKey",
113            CoreError::MalformedEnvelope(_) => "MalformedEnvelope",
114            CoreError::SignatureInvalid(_) => "SignatureInvalid",
115            CoreError::EncryptionFailed(_) => "EncryptionFailed",
116            CoreError::DecryptionFailed(_) => "DecryptionFailed",
117            CoreError::SchemaInvalid(_) => "SchemaInvalid",
118            CoreError::AgreementFailed(_) => "AgreementFailed",
119        }
120    }
121}
122
123impl Serialize for CoreError {
124    /// Stable JSON shape: `{ code, message, details? }`. `details` is only
125    /// present when the variant has structured fields beyond a single
126    /// human-readable string. This shape is the wire contract — see module
127    /// docs.
128    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
129    where
130        S: serde::Serializer,
131    {
132        let details = match self {
133            CoreError::AlgorithmMismatch { expected, actual } => Some(serde_json::json!({
134                "expected": expected,
135                "actual": actual,
136            })),
137            // Single-`String` variants do not need `details` — the message
138            // already carries the only payload.
139            _ => None,
140        };
141
142        let mut s =
143            serializer.serialize_struct("CoreError", if details.is_some() { 3 } else { 2 })?;
144        s.serialize_field("code", self.code())?;
145        s.serialize_field("message", &self.to_string())?;
146        if let Some(d) = details {
147            s.serialize_field("details", &d)?;
148        }
149        s.end()
150    }
151}