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}