1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
use thiserror::Error;
/// Errors returned by all pqfile operations.
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum PqfileError {
/// Underlying I/O error.
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
/// File does not begin with the `PQFL` magic bytes.
#[error("invalid magic bytes: expected PQFL")]
InvalidMagic,
/// Version byte in the header is not recognized.
#[error("unsupported version: {0:#04x}")]
UnsupportedVersion(u8),
/// KEM variant field in the header is not recognized.
#[error("unsupported KEM variant: {0}")]
UnsupportedKem(u16),
/// The decryption key uses a different KEM variant than the file was encrypted with.
#[error("KEM variant mismatch: key is {key}, file uses {file}")]
KemVariantMismatch {
/// KEM variant identifier of the supplied private key.
key: u16,
/// KEM variant identifier recorded in the file header.
file: u16,
},
/// An encryption or key-encapsulation operation failed.
#[error("encryption failure")]
EncryptionFailure,
/// AEAD authentication tag did not verify; ciphertext is corrupt or tampered.
#[error("decryption failure: authentication tag mismatch")]
DecryptionFailure,
/// PEM data could not be parsed.
#[error("invalid PEM: {0}")]
InvalidPem(String),
/// Key material has an unexpected byte length.
#[error("invalid key length: expected {expected}, got {got}")]
InvalidKeyLength {
/// Expected byte length.
expected: usize,
/// Actual byte length encountered.
got: usize,
},
/// Output file already exists and `--force` was not specified.
#[error("output file already exists (use --force to overwrite): {0}")]
OutputExists(std::path::PathBuf),
/// Passphrase did not decrypt the key, or the encrypted key body is corrupt.
#[error("wrong passphrase or corrupted key")]
WrongPassphrase,
/// Operation requires a passphrase but none was supplied.
#[error("private key is passphrase-protected; provide a passphrase to decrypt it")]
PassphraseRequired,
/// New-passphrase confirmation did not match.
#[error("passphrases do not match")]
PassphraseMismatch,
/// Signature bytes are malformed (wrong length or structure).
#[error("invalid signature: malformed bytes")]
InvalidSignature,
/// ML-DSA signature is well-formed but does not verify against the data.
#[error("signature verification failed")]
SignatureVerificationFailed,
/// None of the recipient slots in a v4/v7/v8/v9 file matched the supplied private key.
#[error("no matching recipient found: {slots_tried} slot(s) tried with the given private key")]
NoMatchingRecipient {
/// Number of recipient slots that were attempted during decryption.
slots_tried: usize,
},
/// The key has been explicitly revoked.
#[error("key has been revoked (fingerprint: {fingerprint}): {reason}")]
KeyRevoked {
/// Colon-separated hex fingerprint of the revoked key.
fingerprint: String,
/// Human-readable reason supplied at revocation time.
reason: String,
},
/// v6 compressed files require the `zstd` feature, which is absent in this build.
#[error("compressed .pqf files (v6) are not supported in this build")]
CompressionNotSupported,
/// The private key was encrypted with legacy Argon2id parameters (p=1, pqfile < 4.0).
/// Run `pqfile repassphrase --from-legacy` to upgrade it before use.
#[error(
"private key uses legacy Argon2id parameters (p=1); \
run `pqfile repassphrase --from-legacy` to upgrade it to p=4 before use"
)]
LegacyKeyFormat,
/// Shamir share fingerprints did not agree; shares may be from different keys.
#[error("key share reconstruction failed: fingerprint mismatch, ensure you have the correct shares for this key")]
ShareVerificationFailed,
/// Argon2id parameters in a v10 passphrase-only file exceed the configured ceiling.
///
/// A crafted file could specify extreme Argon2 parameters to force excessive memory or
/// CPU use on decryption. The ceiling is checked before the KDF runs. Raise
/// `--max-kdf-mem` / `--max-kdf-time` flags to accept a higher cost, or reject the file.
#[error(
"KDF parameters in file (m={m_kib} KiB, t={t}) exceed ceiling \
(max_m={max_m_kib} KiB, max_t={max_t})"
)]
KdfLimitExceeded {
/// Memory cost (KiB) requested by the file.
m_kib: u32,
/// Time cost (iterations) requested by the file.
t: u32,
/// Maximum memory cost (KiB) the caller will allow.
max_m_kib: u32,
/// Maximum time cost the caller will allow.
max_t: u32,
},
/// The `.pqf` stream ended without a terminal chunk (is_last flag never seen).
/// The file was most likely truncated during download or transfer; re-downloading
/// is the appropriate recovery action. Distinguished from [`DecryptionFailure`]
/// which indicates bytes are present but the authentication tag is wrong.
#[error("stream truncated: file ended before the final authenticated chunk was seen; re-download and try again")]
Truncated,
/// The v10 file was encrypted with a keyfile second factor but none was supplied.
#[error(
"this file requires a keyfile: it was encrypted with a keyfile second factor; \
pass --keyfile <PATH> with the same keyfile used at encryption"
)]
KeyfileRequired,
/// A keyfile was supplied but the v10 file was not encrypted with one.
/// Failing loudly here avoids silently deriving a wrong key (which would
/// surface as an opaque authentication failure) and tells the caller the
/// keyfile is not protecting this file.
#[error("file was not encrypted with a keyfile; remove --keyfile and retry")]
KeyfileNotRequired,
/// The v10 header carries feature flag bits this build does not understand.
/// The file was likely produced by a newer pqfile; upgrade to decrypt it.
#[error(
"unsupported header flags: {0:#04x}; file was likely written by a newer pqfile version"
)]
UnsupportedHeaderFlags(u8),
/// The v10 file was encrypted with a FIDO2 hardware token second factor but
/// none was supplied (or a keyfile was supplied instead).
#[error(
"this file requires a FIDO2 security key: it was encrypted with a hardware token second \
factor; pass --fido2 <ENROLLMENT_FILE> with the same enrollment used at encryption"
)]
Fido2Required,
/// A FIDO2 token was supplied but the v10 file was not encrypted with one
/// (or was encrypted with a keyfile instead). Mirrors [`KeyfileNotRequired`]:
/// failing loudly here avoids silently deriving a wrong key.
///
/// [`KeyfileNotRequired`]: PqfileError::KeyfileNotRequired
#[error("file was not encrypted with a FIDO2 second factor; remove --fido2 and retry")]
Fido2NotRequired,
}
impl PqfileError {
/// Stable numeric code for this error, as emitted in the CLI's `--json`
/// error output and documented in `docs/ERROR_CODES.md`.
///
/// Codes are append-only: new variants are always assigned new codes, and
/// existing codes are never reused or reassigned. The match below is
/// deliberately exhaustive (no `_` arm) so adding a variant without
/// assigning it a code is a compile error rather than a silent fallthrough.
#[must_use]
pub fn code(&self) -> u32 {
match self {
PqfileError::Io(_) => 1,
PqfileError::InvalidMagic => 2,
PqfileError::UnsupportedVersion(_) => 3,
PqfileError::UnsupportedKem(_) => 4,
PqfileError::KemVariantMismatch { .. } => 5,
PqfileError::EncryptionFailure => 6,
PqfileError::DecryptionFailure => 7,
PqfileError::InvalidPem(_) => 8,
PqfileError::InvalidKeyLength { .. } => 9,
PqfileError::OutputExists(_) => 10,
PqfileError::WrongPassphrase => 11,
PqfileError::PassphraseRequired => 12,
PqfileError::PassphraseMismatch => 13,
PqfileError::InvalidSignature => 14,
PqfileError::SignatureVerificationFailed => 15,
PqfileError::NoMatchingRecipient { .. } => 16,
PqfileError::KeyRevoked { .. } => 17,
PqfileError::CompressionNotSupported => 18,
PqfileError::LegacyKeyFormat => 19,
PqfileError::ShareVerificationFailed => 20,
PqfileError::Truncated => 21,
PqfileError::KdfLimitExceeded { .. } => 22,
PqfileError::KeyfileRequired => 23,
PqfileError::KeyfileNotRequired => 24,
PqfileError::UnsupportedHeaderFlags(_) => 25,
PqfileError::Fido2Required => 26,
PqfileError::Fido2NotRequired => 27,
}
}
}