Skip to main content

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}