dig_keystore/error.rs
1//! Error types for `dig-keystore`.
2//!
3//! All fallible public operations return [`Result<T>`] (= `Result<T, KeystoreError>`).
4//! The variants are designed to let callers distinguish:
5//!
6//! - **User error** — wrong password, wrong scheme type parameter → reshow a prompt.
7//! - **Corruption / tampering** — CRC mismatch, auth-tag failure → refuse and
8//! alert the operator (possible attack).
9//! - **Configuration error** — KDF params out of bounds, unsupported algorithm.
10//! - **I/O error** — underlying backend could not read / write.
11//!
12//! Each variant carries enough context to be actionable. `Arc<std::io::Error>`
13//! is used for the backend case so `KeystoreError` can implement `Clone`
14//! (useful when passing errors through async channels or broadcasting via
15//! `watch::Sender`).
16
17use std::sync::Arc;
18use thiserror::Error;
19
20/// Result alias used throughout the crate.
21pub type Result<T> = std::result::Result<T, KeystoreError>;
22
23/// Errors produced by keystore operations.
24///
25/// The enum is [`Clone`] so errors can be fanned out through broadcast channels
26/// or bubbled through async traits. The only non-Clone primitive (`std::io::Error`)
27/// is wrapped in `Arc` to preserve clonability.
28#[derive(Error, Debug, Clone)]
29pub enum KeystoreError {
30 /// An underlying backend I/O operation failed.
31 ///
32 /// This is the catch-all for filesystem errors from [`crate::FileBackend`]
33 /// as well as any future backend (OS keyring / HSM). The wrapped
34 /// [`std::io::Error`] preserves the original [`ErrorKind`](std::io::ErrorKind)
35 /// for callers who want to distinguish e.g. `NotFound` from `PermissionDenied`.
36 #[error("backend I/O error: {0}")]
37 Backend(#[source] Arc<std::io::Error>),
38
39 /// The file's magic prefix did not match any known scheme.
40 ///
41 /// First 6 bytes of a keystore file carry `DIGVK1`, `DIGLW1`, etc. If the
42 /// caller pointed at a non-keystore file (or a future-version file this
43 /// build doesn't understand), decode fails here before any cryptography.
44 #[error("unknown magic; not a DIG keystore file (saw {saw:?})")]
45 UnknownMagic {
46 /// The magic bytes that were actually read.
47 saw: [u8; 6],
48 },
49
50 /// The file's format version is newer or older than this library understands.
51 ///
52 /// Format version is stored as a big-endian `u16` right after the magic.
53 /// This library recognizes [`crate::FORMAT_VERSION_V1`] only.
54 #[error("unsupported format version {found}")]
55 UnsupportedFormat {
56 /// The format version byte read from the file.
57 found: u16,
58 },
59
60 /// The file's key-scheme id does not match the type parameter used to open it.
61 ///
62 /// If the caller opens `Keystore::<BlsSigning>::load(...)` but the file on
63 /// disk is `L1WalletBls` (scheme id `0x0003`), we refuse. This guards
64 /// against accidentally interpreting wallet master seeds as validator
65 /// signing seeds, which would produce perfectly-valid-looking BLS
66 /// signatures that bind to the wrong domain.
67 #[error(
68 "key scheme mismatch: expected {expected:#06x} ({expected_name:?}), file is {found:#06x}"
69 )]
70 SchemeMismatch {
71 /// The scheme expected by the caller (`K::SCHEME_ID`).
72 expected: u16,
73 /// Human-readable expected name (e.g., `"BlsSigning"`).
74 expected_name: &'static str,
75 /// The scheme id actually stored in the file.
76 found: u16,
77 },
78
79 /// The CRC32 checksum at the end of the file did not match.
80 ///
81 /// CRC is computed over every byte of the file except the trailing 4. A
82 /// mismatch indicates disk corruption, partial write, or deliberate
83 /// tampering. It is NOT a cryptographic integrity check (AES-GCM's tag
84 /// is) — CRC is only a fast-fail so we don't burn ~0.5 s on Argon2 for a
85 /// file that's clearly garbage.
86 #[error("CRC32 check failed (stored {stored:#010x}, computed {computed:#010x})")]
87 CrcMismatch {
88 /// The CRC32 read from the file.
89 stored: u32,
90 /// The CRC32 computed over the preceding bytes.
91 computed: u32,
92 },
93
94 /// AES-GCM authentication tag failed.
95 ///
96 /// This is the single error produced for any cryptographic decryption
97 /// failure: wrong password, tampered ciphertext, tampered header (AAD
98 /// mismatch), or truncated payload. We intentionally do NOT distinguish
99 /// these variants at the error level to avoid side-channel leaks.
100 #[error("AES-GCM authentication failed (wrong password or tampered file)")]
101 DecryptFailed,
102
103 /// Argon2 or AES-GCM rejected the provided parameters.
104 ///
105 /// Thrown when [`crate::KdfParams`] has out-of-bounds values (e.g.,
106 /// `memory_kib < 8192`) or when the underlying `argon2` crate returns an
107 /// error (rare — usually only on invalid output size).
108 #[error("invalid KDF params: {0}")]
109 InvalidKdfParams(&'static str),
110
111 /// The file advertised an unsupported KDF algorithm.
112 ///
113 /// Currently only `0x01 = Argon2id` is recognized. Non-`0x01` values are
114 /// reserved for future algorithms (scrypt, bcrypt, balloon).
115 #[error("unsupported KDF id {0:#04x}")]
116 UnsupportedKdf(u8),
117
118 /// The file advertised an unsupported symmetric cipher.
119 ///
120 /// Currently only `0x01 = AES-256-GCM` is recognized. Non-`0x01` values
121 /// are reserved for e.g. ChaCha20-Poly1305.
122 #[error("unsupported cipher id {0:#04x}")]
123 UnsupportedCipher(u8),
124
125 /// `Keystore::create` was called for a path that already exists.
126 ///
127 /// Deliberate: overwriting a keystore file is almost always an operator
128 /// error. Callers that really want to replace a keystore should
129 /// [`crate::Keystore::delete`] first, or simply [`crate::Keystore::change_password`]
130 /// + [`crate::Keystore::rotate_kdf`] which rotate in place.
131 #[error("key path already exists: {0:?}")]
132 AlreadyExists(String),
133
134 /// The decrypted plaintext has the wrong length for the key scheme.
135 ///
136 /// Each [`crate::KeyScheme`] declares a fixed [`SECRET_LEN`](crate::KeyScheme::SECRET_LEN).
137 /// If `unlock` decrypts successfully but the plaintext length disagrees
138 /// with the scheme (e.g., file was encrypted under v1 with a 32-byte seed
139 /// and this build expects 48), we reject. Normally impossible once the
140 /// scheme id check has passed; included for defence in depth.
141 #[error("invalid plaintext length: expected {expected}, got {got}")]
142 InvalidPlaintext {
143 /// Expected byte length.
144 expected: usize,
145 /// Actual byte length read.
146 got: usize,
147 },
148
149 /// The provided seed bytes were malformed (e.g., not a valid BLS seed).
150 ///
151 /// Rarely thrown — `chia-bls::SecretKey::from_seed` accepts any byte
152 /// length — but reserved for schemes where the raw bytes must pass a
153 /// scheme-specific validity check (e.g., `secp256k1` scalar bounds).
154 #[error("invalid seed bytes: {0}")]
155 InvalidSeed(String),
156
157 /// The file's length header claims a payload larger than the file bytes.
158 ///
159 /// Indicates a truncated file (disk full mid-write, network transfer cut,
160 /// etc). Should be rare since we write files atomically via rename, but
161 /// guard anyway.
162 #[error(
163 "file truncated (header claims {claimed} byte payload, only {available} bytes available)"
164 )]
165 Truncated {
166 /// Bytes claimed by the header.
167 claimed: usize,
168 /// Bytes actually available.
169 available: usize,
170 },
171}
172
173impl From<std::io::Error> for KeystoreError {
174 /// Wrap an I/O error as a backend error.
175 ///
176 /// Used liberally through the `?` operator in [`crate::FileBackend`] and
177 /// other `std::io`-backed code paths.
178 fn from(err: std::io::Error) -> Self {
179 KeystoreError::Backend(Arc::new(err))
180 }
181}