use aes::cipher::KeyInit;
use aes::{Aes128, Aes256};
use xts_mode::Xts128;
use std::time::{Duration, Instant};
use crate::error::{LuksError, Result};
pub const DERIVATION_BUDGET: Duration = Duration::from_secs(30);
pub const UNLOCK_BUDGET: Duration = Duration::from_secs(90);
const CALIBRATION_ITERS: u32 = 20_000;
fn pbkdf2_into(
hash_spec: &str,
password: &[u8],
salt: &[u8],
iters: u32,
out: &mut [u8],
) -> Result<()> {
match hash_spec {
"sha1" => pbkdf2::pbkdf2_hmac::<sha1::Sha1>(password, salt, iters, out),
"sha256" => pbkdf2::pbkdf2_hmac::<sha2::Sha256>(password, salt, iters, out),
"sha512" => pbkdf2::pbkdf2_hmac::<sha2::Sha512>(password, salt, iters, out),
other => {
return Err(LuksError::Unsupported {
what: "hash",
value: other.to_string(),
})
}
}
Ok(())
}
pub fn derive_key_within(
hash_spec: &str,
password: &[u8],
salt: &[u8],
iterations: u32,
key_len: usize,
budget: Duration,
) -> Result<Vec<u8>> {
if key_len > MAX_KEY_BYTES {
return Err(LuksError::KeyLengthExceeded {
requested: key_len,
max: MAX_KEY_BYTES,
});
}
let mut out = vec![0u8; key_len];
let iters = iterations.max(1);
if iters > CALIBRATION_ITERS {
let mut probe = vec![0u8; key_len];
let started = Instant::now();
pbkdf2_into(hash_spec, password, salt, CALIBRATION_ITERS, &mut probe)?;
let measured = started.elapsed();
let projected_nanos =
measured.as_nanos().saturating_mul(u128::from(iters)) / u128::from(CALIBRATION_ITERS);
let projected = Duration::from_nanos(u64::try_from(projected_nanos).unwrap_or(u64::MAX));
if projected > budget {
return Err(LuksError::DerivationBudgetExceeded {
iterations: iters,
projected_secs: projected.as_secs(),
budget_secs: budget.as_secs(),
});
}
}
pbkdf2_into(hash_spec, password, salt, iters, &mut out)?;
Ok(out)
}
pub struct Argon2Params<'a> {
pub kind: &'a str,
pub time: u32,
pub memory: u32,
pub cpus: u32,
pub salt: &'a [u8],
}
const MAX_ARGON2_MEMORY_KIB: u32 = 4 * 1024 * 1024;
const MAX_KEY_BYTES: usize = 1024;
pub fn derive_key_argon2_within(
p: &Argon2Params,
password: &[u8],
key_len: usize,
budget: Duration,
) -> Result<Vec<u8>> {
use argon2::{Algorithm, Argon2, Params, Version};
let algo = match p.kind {
"argon2i" => Algorithm::Argon2i,
"argon2id" => Algorithm::Argon2id,
other => {
return Err(LuksError::Unsupported {
what: "argon2 variant",
value: other.to_string(),
})
}
};
if key_len > MAX_KEY_BYTES {
return Err(LuksError::KeyLengthExceeded {
requested: key_len,
max: MAX_KEY_BYTES,
});
}
if p.memory > MAX_ARGON2_MEMORY_KIB {
return Err(LuksError::DerivationMemoryExceeded {
requested_kib: p.memory,
max_kib: MAX_ARGON2_MEMORY_KIB,
});
}
let run = |time: u32, out: &mut [u8]| -> Result<()> {
let params = Params::new(p.memory, time, p.cpus, Some(key_len)).map_err(|e| {
LuksError::Unsupported {
what: "argon2 params",
value: e.to_string(),
}
})?;
Argon2::new(algo, Version::V0x13, params)
.hash_password_into(password, p.salt, out)
.map_err(|e| LuksError::Unsupported {
what: "argon2",
value: e.to_string(),
})
};
if p.time > 1 {
let mut probe = vec![0u8; key_len];
let started = Instant::now();
run(1, &mut probe)?;
let measured = started.elapsed();
let projected_nanos = measured.as_nanos().saturating_mul(u128::from(p.time));
let projected = Duration::from_nanos(u64::try_from(projected_nanos).unwrap_or(u64::MAX));
if projected > budget {
return Err(LuksError::DerivationBudgetExceeded {
iterations: p.time,
projected_secs: projected.as_secs(),
budget_secs: budget.as_secs(),
});
}
}
let mut out = vec![0u8; key_len];
run(p.time, &mut out)?;
Ok(out)
}
pub fn xts_decrypt_area(
cipher_mode: &str,
key: &[u8],
buffer: &mut [u8],
unit_size: usize,
base_sector: u128,
) -> Result<()> {
if cipher_mode != "xts-plain64" {
return Err(LuksError::Unsupported {
what: "cipher mode",
value: cipher_mode.to_string(),
});
}
let step = (unit_size / 512).max(1) as u128;
match key.len() {
32 => {
let (k1, k2) = key.split_at(16);
let xts = Xts128::<Aes128>::new(Aes128::new(k1.into()), Aes128::new(k2.into()));
decrypt_units(&xts, buffer, unit_size, base_sector, step);
}
64 => {
let (k1, k2) = key.split_at(32);
let xts = Xts128::<Aes256>::new(Aes256::new(k1.into()), Aes256::new(k2.into()));
decrypt_units(&xts, buffer, unit_size, base_sector, step);
}
n => {
return Err(LuksError::Unsupported {
what: "xts key size",
value: format!("{n} bytes"),
})
}
}
Ok(())
}
fn decrypt_units<C>(xts: &Xts128<C>, buffer: &mut [u8], unit_size: usize, base: u128, step: u128)
where
C: aes::cipher::BlockCipher + aes::cipher::BlockEncrypt + aes::cipher::BlockDecrypt,
{
for (u, chunk) in buffer.chunks_mut(unit_size).enumerate() {
if chunk.len() < 16 {
continue; }
let tweak = (base + u as u128 * step).to_le_bytes();
xts.decrypt_sector(chunk, tweak);
}
}
#[cfg(test)]
mod tests {
use super::*;
use xts_mode::get_tweak_default;
#[test]
fn absurd_iteration_count_is_refused_rather_than_run() {
use std::sync::mpsc;
use std::time::Duration;
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let r = derive_key_within(
"sha1",
b"luks-TEST",
b"salt",
u32::MAX,
32,
DERIVATION_BUDGET,
);
let budgeted = matches!(
r,
Err(LuksError::DerivationBudgetExceeded { iterations, .. })
if iterations == u32::MAX
);
let _ = tx.send(budgeted);
});
let refused = rx
.recv_timeout(Duration::from_secs(60))
.expect("derive_key did not return within 60s for u32::MAX iterations — unbounded");
assert!(
refused,
"u32::MAX iterations must be refused with DerivationBudgetExceeded"
);
}
#[test]
fn a_realistic_iteration_count_still_derives() {
let k = derive_key_within(
"sha256",
b"password",
b"salt",
200_000,
32,
DERIVATION_BUDGET,
)
.unwrap();
assert_eq!(k.len(), 32);
let again = derive_key_within(
"sha256",
b"password",
b"salt",
200_000,
32,
DERIVATION_BUDGET,
)
.unwrap();
assert_eq!(k, again);
}
#[test]
fn derive_key_matches_known_pbkdf2_sha256() {
let k =
derive_key_within("sha256", b"password", b"salt", 1, 32, DERIVATION_BUDGET).unwrap();
assert_eq!(
hex(&k),
"120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b"
);
}
#[test]
fn xts_area_roundtrip_512_units() {
let key = [0x24u8; 64];
let mut buf = vec![0u8; 1024];
for (i, b) in buf.iter_mut().enumerate() {
*b = (i as u8) ^ 0x3c;
}
let plain = buf.clone();
let (k1, k2) = key.split_at(32);
let xts = Xts128::<Aes256>::new(Aes256::new(k1.into()), Aes256::new(k2.into()));
xts.encrypt_area(&mut buf, 512, 5, get_tweak_default);
xts_decrypt_area("xts-plain64", &key, &mut buf, 512, 5).unwrap();
assert_eq!(buf, plain);
}
#[test]
fn xts_area_4096_unit_uses_512_based_tweak() {
let key = [0x51u8; 64];
let mut buf = vec![7u8; 4096];
let plain = buf.clone();
let (k1, k2) = key.split_at(32);
let xts = Xts128::<Aes256>::new(Aes256::new(k1.into()), Aes256::new(k2.into()));
xts.encrypt_sector(&mut buf, 8u128.to_le_bytes());
xts_decrypt_area("xts-plain64", &key, &mut buf, 4096, 8).unwrap();
assert_eq!(buf, plain);
}
#[test]
fn xts_rejects_bad_mode_and_keysize() {
let mut buf = [0u8; 512];
assert!(matches!(
xts_decrypt_area("cbc-essiv", &[0u8; 64], &mut buf, 512, 0),
Err(LuksError::Unsupported {
what: "cipher mode",
..
})
));
assert!(matches!(
xts_decrypt_area("xts-plain64", &[0u8; 48], &mut buf, 512, 0),
Err(LuksError::Unsupported {
what: "xts key size",
..
})
));
}
#[test]
fn derive_key_rejects_unknown_hash() {
assert!(matches!(
derive_key_within("md5", b"x", b"y", 1, 16, DERIVATION_BUDGET),
Err(LuksError::Unsupported { what: "hash", .. })
));
}
#[test]
fn absurd_argon2_memory_is_refused_before_allocating() {
let p = Argon2Params {
kind: "argon2id",
time: 1,
memory: u32::MAX, cpus: 1,
salt: &[0x11u8; 16],
};
let err = derive_key_argon2_within(&p, b"pw", 64, DERIVATION_BUDGET)
.expect_err("a 4 TiB memory cost must be refused, not attempted");
assert!(
matches!(err, LuksError::DerivationMemoryExceeded { .. }),
"refused for the wrong reason: {err}"
);
let msg = err.to_string();
assert!(
msg.contains(&u32::MAX.to_string()),
"value not named: {msg}"
);
}
#[test]
fn absurd_argon2_time_is_refused_rather_than_run() {
use std::sync::mpsc;
use std::time::Duration;
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let p = Argon2Params {
kind: "argon2id",
time: u32::MAX,
memory: 64,
cpus: 1,
salt: &[0x11u8; 16],
};
let budgeted = matches!(
derive_key_argon2_within(&p, b"pw", 64, DERIVATION_BUDGET),
Err(LuksError::DerivationBudgetExceeded { .. })
);
let _ = tx.send(budgeted);
});
let refused = rx
.recv_timeout(Duration::from_secs(60))
.expect("derive_key_argon2 did not return within 60s for u32::MAX time cost");
assert!(refused, "u32::MAX time cost must be refused");
}
#[test]
fn a_realistic_argon2_cost_still_derives() {
let p = Argon2Params {
kind: "argon2id",
time: 4,
memory: 65_536, cpus: 1,
salt: &[0x11u8; 16],
};
let k = derive_key_argon2_within(&p, b"pw", 64, DERIVATION_BUDGET).unwrap();
assert_eq!(k.len(), 64);
assert_eq!(
k,
derive_key_argon2_within(&p, b"pw", 64, DERIVATION_BUDGET).unwrap()
);
}
#[test]
fn argon2id_derives_and_rejects_unknown() {
let p = Argon2Params {
kind: "argon2id",
time: 1,
memory: 32,
cpus: 1,
salt: &[0x11u8; 16],
};
let k = derive_key_argon2_within(&p, b"pw", 64, DERIVATION_BUDGET).unwrap();
assert_eq!(k.len(), 64);
let bad = Argon2Params {
kind: "scrypt",
..p
};
assert!(matches!(
derive_key_argon2_within(&bad, b"pw", 64, DERIVATION_BUDGET),
Err(LuksError::Unsupported {
what: "argon2 variant",
..
})
));
}
#[test]
fn xts_area_roundtrip_128bit_key() {
let key = [0x91u8; 32];
let mut buf = vec![0u8; 512];
for (i, b) in buf.iter_mut().enumerate() {
*b = (i as u8).wrapping_add(1);
}
let plain = buf.clone();
let (k1, k2) = key.split_at(16);
let xts = Xts128::<Aes128>::new(Aes128::new(k1.into()), Aes128::new(k2.into()));
xts.encrypt_area(&mut buf, 512, 0, get_tweak_default);
xts_decrypt_area("xts-plain64", &key, &mut buf, 512, 0).unwrap();
assert_eq!(buf, plain);
}
#[test]
fn argon2_rejects_invalid_params() {
let p = Argon2Params {
kind: "argon2id",
time: 1,
memory: 0,
cpus: 1,
salt: &[0x11u8; 16],
};
assert!(matches!(
derive_key_argon2_within(&p, b"pw", 32, DERIVATION_BUDGET),
Err(LuksError::Unsupported {
what: "argon2 params",
..
})
));
}
#[test]
fn argon2_rejects_short_salt() {
let p = Argon2Params {
kind: "argon2id",
time: 1,
memory: 32,
cpus: 1,
salt: &[0u8; 4],
};
assert!(matches!(
derive_key_argon2_within(&p, b"pw", 32, DERIVATION_BUDGET),
Err(LuksError::Unsupported { what: "argon2", .. })
));
}
fn hex(b: &[u8]) -> String {
use std::fmt::Write;
b.iter().fold(String::new(), |mut s, x| {
let _ = write!(s, "{x:02x}");
s
})
}
#[test]
fn derive_key_refuses_an_implausible_key_length_before_calibrating() {
let started = Instant::now();
let err = derive_key_within(
"sha256",
b"luks-TEST",
&[0u8; 32],
50_000,
0x1010_1010, DERIVATION_BUDGET,
)
.expect_err("an implausible master-key length must be refused");
let elapsed = started.elapsed();
assert!(
matches!(err, LuksError::KeyLengthExceeded { .. }),
"expected KeyLengthExceeded, got {err:?}"
);
assert!(
elapsed < Duration::from_secs(2),
"refused only after doing the work ({elapsed:?}) — the guard must \
run before the calibration probe allocates and derives"
);
}
#[test]
fn derive_key_argon2_refuses_an_implausible_key_length() {
let p = Argon2Params {
kind: "argon2id",
time: 1,
memory: 32,
cpus: 1,
salt: &[0u8; 16],
};
assert!(matches!(
derive_key_argon2_within(&p, b"pw", 0x1010_1010, DERIVATION_BUDGET),
Err(LuksError::KeyLengthExceeded { .. })
));
}
#[test]
fn real_master_key_lengths_are_still_accepted() {
for len in [16usize, 32, 64] {
assert!(
derive_key_within("sha256", b"pw", &[0u8; 32], 1, len, DERIVATION_BUDGET).is_ok(),
"a {len}-byte master key is ordinary and must still derive"
);
}
}
}