luks/error.rs
1//! Error type for LUKS parsing and unlocking.
2
3use std::io;
4
5/// Result alias for `luks-core`.
6pub type Result<T> = std::result::Result<T, LuksError>;
7
8/// A LUKS parse or unlock failure. Every variant names the offending value so an
9/// investigator can act on it (never a bare "invalid").
10#[derive(Debug, thiserror::Error)]
11#[non_exhaustive]
12pub enum LuksError {
13 /// The `LUKS\xba\xbe` magic is absent — not a LUKS container.
14 #[error("not a LUKS container: magic is {found:02x?}, expected 4c554b53babe")]
15 NotLuks {
16 /// The first six bytes actually found.
17 found: [u8; 6],
18 },
19
20 /// The header version is neither 1 nor 2.
21 #[error("unsupported LUKS version {version} (only 1 and 2 are supported)")]
22 UnsupportedVersion {
23 /// The version field value.
24 version: u16,
25 },
26
27 /// The cipher/mode/hash combination has no validated decrypt path.
28 #[error("unsupported {what}: {value:?}")]
29 Unsupported {
30 /// Which axis is unsupported (cipher, mode, hash).
31 what: &'static str,
32 /// The offending value verbatim.
33 value: String,
34 },
35
36 /// The keyslot demands more key-derivation work than the wall-clock budget
37 /// allows. The iteration count comes from the header, so a crafted container
38 /// can ask for billions of rounds; refusing is the only way the tool stays
39 /// responsive on hostile input.
40 ///
41 /// A *refusal*, never a silent reduction: deriving with fewer rounds than the
42 /// header specifies produces a different key, which would report a wrong
43 /// An unlock exhausted its TOTAL time budget across all keyslots.
44 ///
45 /// Distinct from [`Self::DerivationBudgetExceeded`], which bounds a single
46 /// derivation. LUKS permits 8 keyslots and the header decides how many are
47 /// active and what each costs, so a crafted header can stay under the
48 /// per-derivation budget on every slot while the sum runs for hours. Each
49 /// individual check passes; nothing owns the total. This variant is that
50 /// total.
51 #[error(
52 "unlock exceeded its total budget of {budget_secs}s after {slots_tried} keyslot(s); \
53 the header's cumulative key-derivation cost is not satisfiable"
54 )]
55 UnlockBudgetExceeded {
56 /// How many keyslots had been attempted when the budget ran out.
57 slots_tried: usize,
58 /// The total budget for the whole unlock.
59 budget_secs: u64,
60 },
61
62 /// passphrase for a container that would in fact have opened.
63 #[error(
64 "key derivation would take about {projected_secs}s for {iterations} iterations, \
65 over the {budget_secs}s budget — refusing rather than clamping, because a \
66 reduced iteration count derives a different key"
67 )]
68 DerivationBudgetExceeded {
69 /// The iteration count the header asked for, verbatim.
70 iterations: u32,
71 /// Projected cost on this machine, measured from a calibration run.
72 projected_secs: u64,
73 /// The budget that was exceeded.
74 budget_secs: u64,
75 },
76
77 /// The keyslot's anti-forensic stripe count is not plausible.
78 ///
79 /// `stripes` is a header field, so the container chooses it. It multiplies
80 /// the key size to give the key-material length the caller then allocates,
81 /// and it is the trip count of the AF merge loop — so an unbounded value is
82 /// both an allocation and a spin, on the container's say-so.
83 ///
84 /// LUKS1 fixes the count at 4000 and cryptsetup writes the same for LUKS2,
85 /// so the ceiling sits far above anything genuine.
86 #[error(
87 "keyslot declares {stripes} anti-forensic stripes, over the {max} ceiling \
88 — refusing before sizing a {} byte buffer",
89 .stripes.saturating_mul(*.block_size)
90 )]
91 ImplausibleStripes {
92 /// The stripe count the header asked for, verbatim.
93 stripes: usize,
94 /// The ceiling that was exceeded.
95 max: usize,
96 /// The key size the count would have been multiplied by.
97 block_size: usize,
98 },
99
100 /// The Argon2 keyslot demands more memory than the ceiling allows.
101 ///
102 /// Separate from [`Self::DerivationBudgetExceeded`] because memory cannot be
103 /// bounded the same way: a time cost can be measured and projected, but
104 /// *attempting* an oversized allocation is itself the harm — the process is
105 /// killed by the OS before any deadline could fire. `argon2::Params` caps
106 /// `m_cost` at `u32::MAX` 1 KiB blocks (4 TiB) and the header chooses the
107 /// value, so the ceiling has to be ours.
108 #[error(
109 "argon2 keyslot asks for {requested_kib} KiB of memory, over the \
110 {max_kib} KiB ceiling — refusing before allocating"
111 )]
112 DerivationMemoryExceeded {
113 /// The memory cost the header asked for, verbatim, in KiB blocks.
114 requested_kib: u32,
115 /// The ceiling that was exceeded, in KiB blocks.
116 max_kib: u32,
117 },
118
119 /// The header asks to derive a master key longer than the ceiling allows.
120 ///
121 /// A third cost axis, independent of iterations and of Argon2 memory, and
122 /// bounded for the same reason as the memory one: the derived-key length
123 /// multiplies the work of every PBKDF2 iteration (`ceil(dkLen / hLen)` HMAC
124 /// blocks each), and it sizes an allocation. Crucially it also sizes the
125 /// CALIBRATION probe used to project the iteration cost, so leaving it
126 /// unbounded makes the measurement itself the denial of service — the guard
127 /// has to run before anything is measured or allocated.
128 #[error(
129 "header asks to derive a {requested} byte master key, over the {max} \
130 byte ceiling — refusing before allocating or calibrating"
131 )]
132 KeyLengthExceeded {
133 /// The master-key length the header asked for, verbatim, in bytes.
134 requested: usize,
135 /// The ceiling that was exceeded, in bytes.
136 max: usize,
137 },
138
139 /// The header is structurally malformed (a field runs past the buffer).
140 #[error("malformed LUKS header: {what} (need {need} bytes, have {got})")]
141 MalformedHeader {
142 /// What was being read.
143 what: &'static str,
144 /// Bytes needed.
145 need: usize,
146 /// Bytes available.
147 got: usize,
148 },
149
150 /// No keyslot could be unlocked with the supplied passphrase, or the derived
151 /// master key failed the mk-digest check (wrong passphrase).
152 #[error("authentication failed: no keyslot matched the passphrase")]
153 AuthenticationFailed,
154
155 /// The container carries no active (enabled) keyslot.
156 #[error("no active keyslot in the LUKS header")]
157 NoActiveKeyslot,
158
159 /// An I/O error reading the container.
160 #[error("I/O error: {0}")]
161 Io(#[from] io::Error),
162}