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
//! Error type for LUKS parsing and unlocking.
use std::io;
/// Result alias for `luks-core`.
pub type Result<T> = std::result::Result<T, LuksError>;
/// A LUKS parse or unlock failure. Every variant names the offending value so an
/// investigator can act on it (never a bare "invalid").
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum LuksError {
/// The `LUKS\xba\xbe` magic is absent — not a LUKS container.
#[error("not a LUKS container: magic is {found:02x?}, expected 4c554b53babe")]
NotLuks {
/// The first six bytes actually found.
found: [u8; 6],
},
/// The header version is neither 1 nor 2.
#[error("unsupported LUKS version {version} (only 1 and 2 are supported)")]
UnsupportedVersion {
/// The version field value.
version: u16,
},
/// The cipher/mode/hash combination has no validated decrypt path.
#[error("unsupported {what}: {value:?}")]
Unsupported {
/// Which axis is unsupported (cipher, mode, hash).
what: &'static str,
/// The offending value verbatim.
value: String,
},
/// The keyslot demands more key-derivation work than the wall-clock budget
/// allows. The iteration count comes from the header, so a crafted container
/// can ask for billions of rounds; refusing is the only way the tool stays
/// responsive on hostile input.
///
/// A *refusal*, never a silent reduction: deriving with fewer rounds than the
/// header specifies produces a different key, which would report a wrong
/// An unlock exhausted its TOTAL time budget across all keyslots.
///
/// Distinct from [`Self::DerivationBudgetExceeded`], which bounds a single
/// derivation. LUKS permits 8 keyslots and the header decides how many are
/// active and what each costs, so a crafted header can stay under the
/// per-derivation budget on every slot while the sum runs for hours. Each
/// individual check passes; nothing owns the total. This variant is that
/// total.
#[error(
"unlock exceeded its total budget of {budget_secs}s after {slots_tried} keyslot(s); \
the header's cumulative key-derivation cost is not satisfiable"
)]
UnlockBudgetExceeded {
/// How many keyslots had been attempted when the budget ran out.
slots_tried: usize,
/// The total budget for the whole unlock.
budget_secs: u64,
},
/// passphrase for a container that would in fact have opened.
#[error(
"key derivation would take about {projected_secs}s for {iterations} iterations, \
over the {budget_secs}s budget — refusing rather than clamping, because a \
reduced iteration count derives a different key"
)]
DerivationBudgetExceeded {
/// The iteration count the header asked for, verbatim.
iterations: u32,
/// Projected cost on this machine, measured from a calibration run.
projected_secs: u64,
/// The budget that was exceeded.
budget_secs: u64,
},
/// The keyslot's anti-forensic stripe count is not plausible.
///
/// `stripes` is a header field, so the container chooses it. It multiplies
/// the key size to give the key-material length the caller then allocates,
/// and it is the trip count of the AF merge loop — so an unbounded value is
/// both an allocation and a spin, on the container's say-so.
///
/// LUKS1 fixes the count at 4000 and cryptsetup writes the same for LUKS2,
/// so the ceiling sits far above anything genuine.
#[error(
"keyslot declares {stripes} anti-forensic stripes, over the {max} ceiling \
— refusing before sizing a {} byte buffer",
.stripes.saturating_mul(*.block_size)
)]
ImplausibleStripes {
/// The stripe count the header asked for, verbatim.
stripes: usize,
/// The ceiling that was exceeded.
max: usize,
/// The key size the count would have been multiplied by.
block_size: usize,
},
/// The Argon2 keyslot demands more memory than the ceiling allows.
///
/// Separate from [`Self::DerivationBudgetExceeded`] because memory cannot be
/// bounded the same way: a time cost can be measured and projected, but
/// *attempting* an oversized allocation is itself the harm — the process is
/// killed by the OS before any deadline could fire. `argon2::Params` caps
/// `m_cost` at `u32::MAX` 1 KiB blocks (4 TiB) and the header chooses the
/// value, so the ceiling has to be ours.
#[error(
"argon2 keyslot asks for {requested_kib} KiB of memory, over the \
{max_kib} KiB ceiling — refusing before allocating"
)]
DerivationMemoryExceeded {
/// The memory cost the header asked for, verbatim, in KiB blocks.
requested_kib: u32,
/// The ceiling that was exceeded, in KiB blocks.
max_kib: u32,
},
/// The header asks to derive a master key longer than the ceiling allows.
///
/// A third cost axis, independent of iterations and of Argon2 memory, and
/// bounded for the same reason as the memory one: the derived-key length
/// multiplies the work of every PBKDF2 iteration (`ceil(dkLen / hLen)` HMAC
/// blocks each), and it sizes an allocation. Crucially it also sizes the
/// CALIBRATION probe used to project the iteration cost, so leaving it
/// unbounded makes the measurement itself the denial of service — the guard
/// has to run before anything is measured or allocated.
#[error(
"header asks to derive a {requested} byte master key, over the {max} \
byte ceiling — refusing before allocating or calibrating"
)]
KeyLengthExceeded {
/// The master-key length the header asked for, verbatim, in bytes.
requested: usize,
/// The ceiling that was exceeded, in bytes.
max: usize,
},
/// The header is structurally malformed (a field runs past the buffer).
#[error("malformed LUKS header: {what} (need {need} bytes, have {got})")]
MalformedHeader {
/// What was being read.
what: &'static str,
/// Bytes needed.
need: usize,
/// Bytes available.
got: usize,
},
/// No keyslot could be unlocked with the supplied passphrase, or the derived
/// master key failed the mk-digest check (wrong passphrase).
#[error("authentication failed: no keyslot matched the passphrase")]
AuthenticationFailed,
/// The container carries no active (enabled) keyslot.
#[error("no active keyslot in the LUKS header")]
NoActiveKeyslot,
/// An I/O error reading the container.
#[error("I/O error: {0}")]
Io(#[from] io::Error),
}