PBKDF: implement argon2id

This commit is contained in:
sam.hadow committed 2026-10-01 18:28:40 +02:00
1 parent 1fcf38db41
commit b30b5d0add
13 files changed
+1291 -148

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
# filecrypt-rs
Cli tool in Rust to encrypt and decrypt files with an Ascon-Keccak AEAD.
The PBKDF is not yet implemented and is just SHA512(password) for now.
The PBKDF used is Argon2id.
## Commands
+10 -11
View File
@@ -2,21 +2,21 @@ use std::fs::File;
use std::io::BufWriter;
use std::path::Path;
use super::BUF_SIZE;
use super::atomic::AtomicOutput;
use super::error::FileCryptError;
use super::stream::{decrypt_stream, encrypt_stream};
use super::{BUF_SIZE, check_key};
/// Encrypt the file at `input` into `output`.
///
/// A random Argon2id salt, IV, and nonce are generated and stored in the encrypted file header.
///
/// `output` is only replaced if encryption succeeds.
pub fn encrypt_file(
input: impl AsRef<Path>,
output: impl AsRef<Path>,
key: &[u8],
password: &str,
) -> Result<(), FileCryptError> {
check_key(key)?;
let input_file = File::open(input.as_ref())?;
let plaintext_len = input_file.metadata()?.len();
@@ -25,7 +25,7 @@ pub fn encrypt_file(
let mut writer = BufWriter::with_capacity(BUF_SIZE, out_file);
encrypt_stream(input_file, &mut writer, key, plaintext_len)?;
encrypt_stream(input_file, &mut writer, password, plaintext_len)?;
let out_file = writer
.into_inner()
@@ -41,22 +41,21 @@ pub fn encrypt_file(
/// Decrypt the file at `input` into `output`.
///
/// The destination is left untouched if authentication or any other part
/// of decryption fails.
/// The Argon2id salt is read from the encrypted file header.
///
/// The destination is left untouched if authentication or any other part of decryption fails.
pub fn decrypt_file(
input: impl AsRef<Path>,
output: impl AsRef<Path>,
key: &[u8],
password: &str,
) -> Result<(), FileCryptError> {
check_key(key)?;
let input_file = File::open(input.as_ref())?;
let (guard, out_file) = AtomicOutput::create(output.as_ref())?;
let mut writer = BufWriter::with_capacity(BUF_SIZE, out_file);
decrypt_stream(input_file, &mut writer, key)?;
decrypt_stream(input_file, &mut writer, password)?;
let out_file = writer
.into_inner()
+39 -9
View File
@@ -1,27 +1,53 @@
use crate::kdf::{DERIVED_KEY_LEN, SALT_LEN};
use super::error::FileCryptError;
pub const IV_LEN: usize = 16;
pub const NONCE_LEN: usize = 16;
/// The tag is as long as the key.
pub const MIN_KEY_LEN: usize = 16;
/// Must match `CAPACITY_SIZE` in `keccak_aead.rs`.
pub const MAX_KEY_LEN: usize = 24;
pub const MAX_KEY_LEN: usize = 64;
pub const HEADER_LEN: usize = 48;
pub const HEADER_LEN: usize = 64;
const MAGIC: [u8; 4] = *b"KAEF";
const VERSION: u8 = 1;
const VERSION: u8 = 2;
pub(super) struct Header {
pub(super) key_len: usize,
pub(super) iv: [u8; IV_LEN],
pub(super) nonce: [u8; NONCE_LEN],
pub(super) salt: [u8; SALT_LEN],
pub(super) plaintext_len: u64,
}
impl Header {
/// Create a new header with a fresh random IV, nonce, and KDF salt.
pub(super) fn new(plaintext_len: u64) -> Result<Self, FileCryptError> {
let mut random = [0u8; IV_LEN + NONCE_LEN + SALT_LEN];
getrandom::fill(&mut random).map_err(FileCryptError::Random)?;
let iv = random[0..IV_LEN].try_into().expect("IV length is constant");
let nonce = random[IV_LEN..IV_LEN + NONCE_LEN]
.try_into()
.expect("nonce length is constant");
let salt = random[IV_LEN + NONCE_LEN..]
.try_into()
.expect("salt length is constant");
Ok(Self {
key_len: DERIVED_KEY_LEN,
iv,
nonce,
salt,
plaintext_len,
})
}
pub(super) fn to_bytes(&self) -> [u8; HEADER_LEN] {
let mut out = [0u8; HEADER_LEN];
@@ -33,7 +59,8 @@ impl Header {
out[8..24].copy_from_slice(&self.iv);
out[24..40].copy_from_slice(&self.nonce);
out[40..48].copy_from_slice(&self.plaintext_len.to_le_bytes());
out[40..56].copy_from_slice(&self.salt);
out[56..64].copy_from_slice(&self.plaintext_len.to_le_bytes());
out
}
@@ -55,9 +82,12 @@ impl Header {
Ok(Self {
key_len,
iv: bytes[8..24].try_into().unwrap(),
nonce: bytes[24..40].try_into().unwrap(),
plaintext_len: u64::from_le_bytes(bytes[40..48].try_into().unwrap()),
iv: bytes[8..24].try_into().expect("IV length is constant"),
nonce: bytes[24..40].try_into().expect("nonce length is constant"),
salt: bytes[40..56].try_into().expect("salt length is constant"),
plaintext_len: u64::from_le_bytes(
bytes[56..64].try_into().expect("length is constant"),
),
})
}
}
+2
View File
@@ -7,6 +7,8 @@ mod stream;
#[cfg(test)]
mod tests;
pub use crate::kdf::SALT_LEN;
pub use error::FileCryptError;
pub use file::{decrypt_file, encrypt_file};
pub use format::{HEADER_LEN, MAX_KEY_LEN, MIN_KEY_LEN};
+48 -22
View File
@@ -2,12 +2,42 @@ use std::io::{self, Read, Write};
use subtle::ConstantTimeEq;
#[cfg(test)]
use crate::kdf::{Argon2Params, derive_file_key_with_params};
#[cfg(not(test))]
use crate::kdf::derive_file_key;
use crate::keccak_aead::KeccakAead;
use super::error::FileCryptError;
use super::format::Header;
use super::{BUF_SIZE, HEADER_LEN, check_key};
#[cfg(test)]
const TEST_ARGON2_PARAMS: Argon2Params = Argon2Params {
memory_kib: 32,
time_cost: 1,
parallelism: 1,
};
#[cfg(test)]
fn derive_stream_key(
password: &str,
salt: &[u8; crate::kdf::SALT_LEN],
) -> [u8; crate::kdf::DERIVED_KEY_LEN] {
derive_file_key_with_params(password, salt, TEST_ARGON2_PARAMS)
.expect("test Argon2id parameters are valid")
}
#[cfg(not(test))]
fn derive_stream_key(
password: &str,
salt: &[u8; crate::kdf::SALT_LEN],
) -> [u8; crate::kdf::DERIVED_KEY_LEN] {
derive_file_key(password, salt)
}
/// `read_exact`, but EOF becomes `on_eof` instead of an `io::Error`.
fn read_exact_or<R: Read>(
reader: &mut R,
@@ -41,34 +71,26 @@ fn at_eof<R: Read>(reader: &mut R) -> io::Result<bool> {
/// Encrypt exactly `plaintext_len` bytes from `reader` into `writer`.
///
/// The length is authenticated as part of the header.
/// A new random IV, nonce, and Argon2id salt are generated for every encrypted file.
///
/// The password is used with the header salt to derive the 64-byte KeccakAead key.
pub fn encrypt_stream<R: Read, W: Write>(
mut reader: R,
mut writer: W,
key: &[u8],
password: &str,
plaintext_len: u64,
) -> Result<(), FileCryptError> {
check_key(key)?;
let header = Header::new(plaintext_len)?;
let mut iv = [0u8; 16];
let mut nonce = [0u8; 16];
let key = derive_stream_key(password, &header.salt);
getrandom::fill(&mut iv).map_err(FileCryptError::Random)?;
getrandom::fill(&mut nonce).map_err(FileCryptError::Random)?;
let header = Header {
key_len: key.len(),
iv,
nonce,
plaintext_len,
};
check_key(&key)?;
let header_bytes = header.to_bytes();
writer.write_all(&header_bytes)?;
let mut aead = KeccakAead::new(&header.iv, key, &header.nonce);
let mut aead = KeccakAead::new(&header.iv, &key, &header.nonce);
aead.associated_data_processing(&header_bytes);
@@ -95,7 +117,7 @@ pub fn encrypt_stream<R: Read, W: Write>(
return Err(FileCryptError::InputLengthChanged);
}
let tag = aead.finalize(key);
let tag = aead.finalize(&key);
writer.write_all(&tag)?;
writer.flush()?;
@@ -105,20 +127,24 @@ pub fn encrypt_stream<R: Read, W: Write>(
/// Decrypt `reader` into `writer` and verify the authentication tag.
///
/// The Argon2id salt is read from the encrypted file's header and used together with `password` to derive the 64-byte AEAD key.
///
/// Plaintext is written before authentication succeeds. The file-level API therefore uses an atomic temporary output file.
pub fn decrypt_stream<R: Read, W: Write>(
mut reader: R,
mut writer: W,
key: &[u8],
password: &str,
) -> Result<(), FileCryptError> {
check_key(key)?;
let mut header_bytes = [0u8; HEADER_LEN];
read_exact_or(&mut reader, &mut header_bytes, FileCryptError::Truncated)?;
let header = Header::from_bytes(&header_bytes)?;
let key = derive_stream_key(password, &header.salt);
check_key(&key)?;
if header.key_len != key.len() {
return Err(FileCryptError::KeyLengthMismatch {
header: header.key_len,
@@ -126,7 +152,7 @@ pub fn decrypt_stream<R: Read, W: Write>(
});
}
let mut aead = KeccakAead::new(&header.iv, key, &header.nonce);
let mut aead = KeccakAead::new(&header.iv, &key, &header.nonce);
aead.associated_data_processing(&header_bytes);
@@ -153,7 +179,7 @@ pub fn decrypt_stream<R: Read, W: Write>(
return Err(FileCryptError::TrailingData);
}
let computed_tag = aead.finalize(key);
let computed_tag = aead.finalize(&key);
let tags_match: bool = computed_tag.as_slice().ct_eq(stored_tag.as_slice()).into();
+183 -71
View File
@@ -1,9 +1,18 @@
use super::*;
use crate::file_crypt::stream::{decrypt_stream, encrypt_stream};
use crate::kdf::{Argon2Params, DERIVED_KEY_LEN, SALT_LEN, derive_file_key_with_params};
use crate::keccak_aead::KeccakAead;
use std::fs;
use std::io::Cursor;
const KEY: [u8; 16] = [0x42; 16];
const TEST_ARGON2_PARAMS: Argon2Params = Argon2Params {
memory_kib: 32,
time_cost: 1,
parallelism: 1,
};
const PASSWORD: &str = "test password";
fn sample(n: usize) -> Vec<u8> {
(0..n)
@@ -11,13 +20,13 @@ fn sample(n: usize) -> Vec<u8> {
.collect()
}
fn enc(key: &[u8], plaintext: &[u8]) -> Vec<u8> {
fn enc(password: &str, plaintext: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
encrypt_stream(
Cursor::new(plaintext),
&mut out,
key,
password,
plaintext.len() as u64,
)
.unwrap();
@@ -25,10 +34,10 @@ fn enc(key: &[u8], plaintext: &[u8]) -> Vec<u8> {
out
}
fn dec(key: &[u8], data: &[u8]) -> Result<Vec<u8>, FileCryptError> {
fn dec(password: &str, data: &[u8]) -> Result<Vec<u8>, FileCryptError> {
let mut out = Vec::new();
decrypt_stream(Cursor::new(data), &mut out, key)?;
decrypt_stream(Cursor::new(data), &mut out, password)?;
Ok(out)
}
@@ -53,74 +62,109 @@ fn boundary_sizes() -> Vec<usize> {
#[cfg(test)]
mod tests {
use super::*;
use crate::keccak_aead::KeccakAead;
#[test]
fn roundtrip_various_sizes() {
for n in boundary_sizes() {
let plaintext = sample(n);
let encrypted = enc(&KEY, &plaintext);
assert_eq!(encrypted.len(), HEADER_LEN + n + KEY.len(), "size {n}");
assert_eq!(dec(&KEY, &encrypted).unwrap(), plaintext, "size {n}");
let encrypted = enc(PASSWORD, &plaintext);
assert_eq!(
encrypted.len(),
HEADER_LEN + n + DERIVED_KEY_LEN,
"size {n}"
);
assert_eq!(dec(PASSWORD, &encrypted).unwrap(), plaintext, "size {n}");
}
}
#[test]
fn roundtrip_all_key_lengths() {
for key_len in MIN_KEY_LEN..=MAX_KEY_LEN {
let key = vec![0xA5u8; key_len];
let plaintext = sample(100);
let encrypted = enc(&key, &plaintext);
assert_eq!(encrypted[5] as usize, key_len);
assert_eq!(encrypted.len(), HEADER_LEN + 100 + key_len);
assert_eq!(dec(&key, &encrypted).unwrap(), plaintext);
}
fn header_contains_derived_key_length() {
let encrypted = enc(PASSWORD, &sample(100));
assert_eq!(encrypted[5] as usize, DERIVED_KEY_LEN);
assert_eq!(encrypted.len(), HEADER_LEN + 100 + DERIVED_KEY_LEN);
}
/// Chunked streaming must be byte-identical to the one-shot AEAD.
#[test]
fn matches_one_shot_aead() {
for n in boundary_sizes() {
let plaintext = sample(n);
let encrypted = enc(&KEY, &plaintext);
let encrypted = enc(PASSWORD, &plaintext);
let header = &encrypted[..HEADER_LEN];
let iv = &header[8..24];
let nonce = &header[24..40];
let expected = KeccakAead::encrypt(&KEY, &plaintext, iv, header, nonce);
let salt: [u8; SALT_LEN] = header[40..56].try_into().unwrap();
let key = derive_file_key_with_params(PASSWORD, &salt, TEST_ARGON2_PARAMS).unwrap();
let expected = KeccakAead::encrypt(&key, &plaintext, iv, header, nonce);
assert_eq!(
&encrypted[HEADER_LEN..HEADER_LEN + n],
&expected.cipher[..],
"size {n}"
);
assert_eq!(&encrypted[HEADER_LEN + n..], &expected.tag[..], "size {n}");
}
}
#[test]
fn iv_and_nonce_are_random() {
let a = enc(&KEY, b"same plaintext");
let b = enc(&KEY, b"same plaintext");
fn iv_nonce_and_salt_are_random() {
let a = enc(PASSWORD, b"same plaintext");
let b = enc(PASSWORD, b"same plaintext");
assert_ne!(a[8..24], b[8..24], "IV repeated");
assert_ne!(a[24..40], b[24..40], "nonce repeated");
assert_ne!(a[40..56], b[40..56], "salt repeated");
assert_ne!(a[HEADER_LEN..], b[HEADER_LEN..]);
}
#[test]
fn different_salts_still_allow_decryption() {
let plaintext = sample(100);
let a = enc(PASSWORD, &plaintext);
let b = enc(PASSWORD, &plaintext);
// The salts must differ.
assert_ne!(a[40..56], b[40..56]);
// Both files must nevertheless decrypt correctly
// because their respective salts are stored in the header.
assert_eq!(dec(PASSWORD, &a).unwrap(), plaintext);
assert_eq!(dec(PASSWORD, &b).unwrap(), plaintext);
}
#[test]
fn every_single_bit_flip_is_rejected() {
let plaintext = sample(100);
let encrypted = enc(&KEY, &plaintext);
let encrypted = enc(PASSWORD, &plaintext);
for pos in 0..encrypted.len() {
for bit in [0x01u8, 0x80] {
let mut tampered = encrypted.clone();
tampered[pos] ^= bit;
let result = dec(&KEY, &tampered);
let result = dec(PASSWORD, &tampered);
assert!(result.is_err(), "flip at byte {pos} bit {bit:#x} accepted");
// Everything after the header must fail specifically on the tag.
/*
* Everything after the header must fail specifically
* on authentication because ciphertext/tag corruption
* is detected by the AEAD.
*/
if pos >= HEADER_LEN {
assert!(
matches!(result, Err(FileCryptError::AuthenticationFailed)),
@@ -131,32 +175,34 @@ mod tests {
}
}
/// The AEAD absorbs a partial final ciphertext block zero-padded and
/// without a length, so `C || 00` yields the same tag as `C`. The
/// authenticated length in the header must stop that.
#[test]
fn trailing_zero_byte_forgery_is_rejected() {
let plaintext = sample(20); // final chunk has 4 bytes
let encrypted = enc(&KEY, &plaintext);
let plaintext = sample(20);
let encrypted = enc(PASSWORD, &plaintext);
let mut forged = encrypted[..HEADER_LEN + 20].to_vec();
forged.push(0x00);
forged.extend_from_slice(&encrypted[HEADER_LEN + 20..]);
// Without touching the header: structural error.
assert!(dec(&KEY, &forged).is_err());
// Without modifying the authenticated length,
// the file structure is inconsistent.
assert!(dec(PASSWORD, &forged).is_err());
// Changing the authenticated length must also fail because
// the header is associated data.
forged[56..64].copy_from_slice(&21u64.to_le_bytes());
// Also bumping the length field: the header is associated data.
forged[40..48].copy_from_slice(&21u64.to_le_bytes());
assert!(matches!(
dec(&KEY, &forged),
dec(PASSWORD, &forged),
Err(FileCryptError::AuthenticationFailed)
));
}
#[test]
fn truncation_and_trailing_data() {
let encrypted = enc(&KEY, &sample(100));
let encrypted = enc(PASSWORD, &sample(100));
for cut in [
0,
@@ -167,109 +213,167 @@ mod tests {
encrypted.len() - 1,
] {
assert!(
matches!(dec(&KEY, &encrypted[..cut]), Err(FileCryptError::Truncated)),
matches!(
dec(PASSWORD, &encrypted[..cut]),
Err(FileCryptError::Truncated)
),
"cut at {cut}"
);
}
let mut extended = encrypted.clone();
extended.push(0);
assert!(matches!(
dec(&KEY, &extended),
dec(PASSWORD, &extended),
Err(FileCryptError::TrailingData)
));
}
#[test]
fn wrong_key_and_key_length() {
let encrypted = enc(&KEY, &sample(50));
fn wrong_password() {
let encrypted = enc(PASSWORD, &sample(50));
let mut other = KEY;
other[0] ^= 1;
assert!(matches!(
dec(&other, &encrypted),
dec("wrong password", &encrypted),
Err(FileCryptError::AuthenticationFailed)
));
}
#[test]
fn malformed_key_length() {
let encrypted = enc(PASSWORD, &sample(50));
let mut malformed = encrypted.clone();
// 16 is structurally valid, but does not match
// the 64-byte key generated by Argon2id.
malformed[5] = 16;
assert!(matches!(
dec(&[0x42u8; 24], &encrypted),
dec(PASSWORD, &malformed),
Err(FileCryptError::KeyLengthMismatch {
header: 16,
provided: 24
provided: 64
})
));
assert!(matches!(
dec(&[0x42u8; 8], &encrypted),
Err(FileCryptError::InvalidKeyLength(8))
super::check_key(&[0u8; 15]),
Err(FileCryptError::InvalidKeyLength(15))
));
assert!(matches!(
encrypt_stream(Cursor::new(b"x"), Vec::new(), &[0u8; 25], 1),
Err(FileCryptError::InvalidKeyLength(25))
super::check_key(&[0u8; 65]),
Err(FileCryptError::InvalidKeyLength(65))
));
}
#[test]
fn encrypt_detects_length_change() {
let too_short = encrypt_stream(Cursor::new(vec![0u8; 10]), Vec::new(), &KEY, 11);
let too_short = encrypt_stream(Cursor::new(vec![0u8; 10]), Vec::new(), PASSWORD, 11);
assert!(matches!(too_short, Err(FileCryptError::InputLengthChanged)));
let too_long = encrypt_stream(Cursor::new(vec![0u8; 12]), Vec::new(), &KEY, 11);
let too_long = encrypt_stream(Cursor::new(vec![0u8; 12]), Vec::new(), PASSWORD, 11);
assert!(matches!(too_long, Err(FileCryptError::InputLengthChanged)));
}
#[test]
fn garbage_headers() {
assert!(matches!(
dec(&KEY, &[0u8; HEADER_LEN]),
dec(PASSWORD, &[0u8; HEADER_LEN]),
Err(FileCryptError::BadMagic)
));
let mut bad_version = enc(&KEY, b"x");
bad_version[4] = 2;
let mut bad_version = enc(PASSWORD, b"x");
bad_version[4] = 3;
assert!(matches!(
dec(&KEY, &bad_version),
Err(FileCryptError::UnsupportedVersion(2))
dec(PASSWORD, &bad_version),
Err(FileCryptError::UnsupportedVersion(3))
));
// Absurd declared length on a tiny file: must fail, not allocate.
let mut huge = enc(&KEY, b"x");
huge[40..48].copy_from_slice(&u64::MAX.to_le_bytes());
assert!(matches!(dec(&KEY, &huge), Err(FileCryptError::Truncated)));
/*
* Absurd declared length on a tiny file:
* decryption must fail without trying to allocate
* based on that value.
*/
let mut huge = enc(PASSWORD, b"x");
huge[56..64].copy_from_slice(&u64::MAX.to_le_bytes());
assert!(matches!(
dec(PASSWORD, &huge),
Err(FileCryptError::Truncated)
));
}
#[test]
fn salt_is_authenticated() {
let encrypted = enc(PASSWORD, b"hello");
let mut tampered = encrypted.clone();
// Change one bit of the salt.
tampered[40] ^= 1;
assert!(matches!(
dec(PASSWORD, &tampered),
Err(FileCryptError::AuthenticationFailed)
));
}
#[test]
fn file_roundtrip_and_atomic_failure() {
let dir = tempfile::tempdir().unwrap();
let plain = dir.path().join("plain.bin");
let encrypted = dir.path().join("plain.bin.kaef");
let decrypted = dir.path().join("decrypted.bin");
let data = sample(3 * BUF_SIZE + 5);
fs::write(&plain, &data).unwrap();
encrypt_file(&plain, &encrypted, &KEY).unwrap();
decrypt_file(&encrypted, &decrypted, &KEY).unwrap();
encrypt_file(&plain, &encrypted, PASSWORD).unwrap();
decrypt_file(&encrypted, &decrypted, PASSWORD).unwrap();
assert_eq!(fs::read(&decrypted).unwrap(), data);
// Corrupt one ciphertext byte.
let mut bytes = fs::read(&encrypted).unwrap();
bytes[HEADER_LEN + 1000] ^= 1;
let corrupted = dir.path().join("corrupted.kaef");
fs::write(&corrupted, &bytes).unwrap();
// A failed decryption must not create the output
// A failed decryption must not create
// the output.
let out_new = dir.path().join("out_new.bin");
assert!(matches!(
decrypt_file(&corrupted, &out_new, &KEY),
decrypt_file(&corrupted, &out_new, PASSWORD,),
Err(FileCryptError::AuthenticationFailed)
));
assert!(!out_new.exists());
// nor clobber an existing file.
// Nor clobber an existing file.
let out_existing = dir.path().join("out_existing.bin");
fs::write(&out_existing, b"precious").unwrap();
assert!(decrypt_file(&corrupted, &out_existing, &KEY).is_err());
assert!(decrypt_file(&corrupted, &out_existing, PASSWORD,).is_err());
assert_eq!(fs::read(&out_existing).unwrap(), b"precious");
// No temporary files are left behind.
@@ -278,23 +382,31 @@ mod tests {
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.filter(|n| n.ends_with(".tmp"))
.collect();
assert!(leftovers.is_empty(), "leftover temp files: {leftovers:?}");
}
#[test]
fn empty_file_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let plain = dir.path().join("empty");
let encrypted = dir.path().join("empty.kaef");
let decrypted = dir.path().join("empty.out");
fs::write(&plain, b"").unwrap();
encrypt_file(&plain, &encrypted, &KEY).unwrap();
encrypt_file(&plain, &encrypted, PASSWORD).unwrap();
assert_eq!(
fs::metadata(&encrypted).unwrap().len() as usize,
HEADER_LEN + KEY.len()
HEADER_LEN + DERIVED_KEY_LEN
);
decrypt_file(&encrypted, &decrypted, &KEY).unwrap();
decrypt_file(&encrypted, &decrypted, PASSWORD).unwrap();
assert_eq!(fs::read(&decrypted).unwrap(), b"");
}
}
+317
View File
@@ -0,0 +1,317 @@
use super::compress::{Block, compress_block};
use super::hash::{block_to_bytes, hash_long, initial_block, initial_hash};
use super::kdf::ARGON2ID_TYPE;
use super::{Argon2Params, KdfError};
/// Core Argon2id function.
///
/// This implements Argon2id v1.3 using BLAKE2b for H and H'.
pub(super) fn argon2id(
password: &[u8],
salt: &[u8],
params: Argon2Params,
output_len: usize,
) -> Result<Vec<u8>, KdfError> {
if params.time_cost == 0 {
return Err(KdfError::InvalidTimeCost);
}
if params.parallelism == 0 {
return Err(KdfError::InvalidParallelism);
}
let parallelism = params.parallelism as usize;
let requested_memory = params.memory_kib as usize;
/*
* Argon2 requires at least 8 KiB per lane.
*/
if requested_memory < 8 * parallelism {
return Err(KdfError::InvalidMemoryCost);
}
/*
* The actual memory size is rounded down to a multiple of 4 * parallelism.
*/
let memory_kib = 4 * parallelism * (requested_memory / (4 * parallelism));
/*
* Number of blocks in each lane.
*/
let lane_length = memory_kib / parallelism;
/*
* Four segments per lane.
*/
let segment_length = lane_length / 4;
/*
* Initial hash H_0.
*/
let h0 = initial_hash(password, salt, params, memory_kib as u32, output_len);
/*
* Allocate the complete Argon2 memory matrix.
*
* Layout:
*
* lane 0: [block][block][block]...
* lane 1: [block][block][block]...
* ...
*/
let mut memory = vec![Block::ZERO; memory_kib];
/*
* Initialize the first two blocks of every lane:
*
* B[i][0] = H'(H_0 || LE32(0) || LE32(i))
* B[i][1] = H'(H_0 || LE32(1) || LE32(i))
*/
for lane in 0..parallelism {
memory[lane * lane_length] = initial_block(&h0, 0, lane);
memory[lane * lane_length + 1] = initial_block(&h0, 1, lane);
}
/*
* Fill the memory matrix.
*
* Argon2id:
*
* - pass 0, slices 0 and 1 use Argon2i style data independent addressing;
* - everything else uses Argon2d style addressing.
*/
for pass in 0..params.time_cost as usize {
for slice in 0..4 {
for lane in 0..parallelism {
process_segment(
&mut memory,
lane_length,
segment_length,
parallelism,
pass,
slice,
lane,
params.time_cost as usize,
memory_kib as u64,
);
}
}
}
/*
* Final block:
*
* C = B[0][q-1] XOR B[1][q-1] XOR ... XOR B[p-1][q-1]
*/
let mut final_block = Block::ZERO;
for lane in 0..parallelism {
let offset = lane * lane_length + lane_length - 1;
for (dst, src) in final_block.0.iter_mut().zip(memory[offset].0.iter()) {
*dst ^= *src;
}
}
/*
* Final Argon2 output:
*
* H'(C, output_len)
*/
let final_bytes = block_to_bytes(&final_block);
Ok(hash_long(&final_bytes, output_len))
}
/// Process one Argon2 segment.
fn process_segment(
memory: &mut [Block],
lane_length: usize,
segment_length: usize,
parallelism: usize,
pass: usize,
slice: usize,
lane: usize,
total_time: usize,
memory_kib: u64,
) {
/*
* Argon2id uses data-independent addressing only during the first two slices of the first pass.
*/
let data_independent = pass == 0 && slice < 2;
let mut address_block = Block::ZERO;
let mut input_block = Block::ZERO;
/*
* Position within this segment.
*/
let mut index = 0usize;
if data_independent {
input_block.0[0] = pass as u64;
input_block.0[1] = lane as u64;
input_block.0[2] = slice as u64;
input_block.0[3] = memory_kib;
input_block.0[4] = total_time as u64;
input_block.0[5] = ARGON2ID_TYPE as u64;
/*
* In the first segment, blocks 0 and 1 already exist, so generation starts at block 2.
*/
if pass == 0 && slice == 0 {
input_block.0[6] += 1;
address_block = compress_block(&input_block, &Block::ZERO, None);
address_block = compress_block(&address_block, &Block::ZERO, None);
index = 2;
}
}
while index < segment_length {
let current_offset = lane * lane_length + slice * segment_length + index;
/*
* Previous block in this lane.
*
* At the beginning of a lane, the previous block is the final block of the same lane.
*/
let previous_offset = if slice == 0 && index == 0 {
current_offset + lane_length - 1
} else {
current_offset - 1
};
/*
* Obtain J_1/J_2.
*/
let pseudo_random = if data_independent {
/*
* Generate a new address block every 128 positions.
*/
if index.is_multiple_of(128) {
input_block.0[6] += 1;
address_block = compress_block(&input_block, &Block::ZERO, None);
address_block = compress_block(&address_block, &Block::ZERO, None);
}
address_block.0[index % 128]
} else {
/*
* Argon2d addressing uses the first word of the previous block.
*/
memory[previous_offset].0[0]
};
/*
* Select the reference block.
*/
let reference_offset = index_alpha(
pseudo_random,
lane_length,
segment_length,
parallelism,
pass,
slice,
lane,
index,
);
/*
* Later passes XOR the newly generated block with the block that was already present at this position.
*/
let previous_block = memory[previous_offset];
let reference_block = memory[reference_offset];
let old_block = if pass == 0 {
None
} else {
Some(&memory[current_offset])
};
let new_block = compress_block(&previous_block, &reference_block, old_block);
memory[current_offset] = new_block;
index += 1;
}
}
/// Select the reference block for an Argon2 block.
///
/// This implements the Argon2 mapping from J_1/J_2 to the reference lane and reference position.
fn index_alpha(
random: u64,
lane_length: usize,
segment_length: usize,
parallelism: usize,
pass: usize,
slice: usize,
lane: usize,
index: usize,
) -> usize {
/*
* J_2 selects the reference lane.
*/
let mut reference_lane = ((random >> 32) as usize) % parallelism;
/*
* During the first slice of the first pass, references must remain in the same lane.
*/
if pass == 0 && slice == 0 {
reference_lane = lane;
}
/*
* Compute the size of the allowed reference area.
*/
let mut reference_area_size = 3 * segment_length;
let mut start_position = ((slice + 1) % 4) * segment_length;
if lane == reference_lane {
reference_area_size += index;
}
if pass == 0 {
reference_area_size = slice * segment_length;
start_position = 0;
if slice == 0 || lane == reference_lane {
reference_area_size += index;
}
}
/*
* The current block cannot reference itself.
*/
if index == 0 || lane == reference_lane {
reference_area_size -= 1;
}
/*
* Non-uniform mapping:
*
* x = J_1^2 / 2^32
* y = |W| * x / 2^32
*/
let mut x = (random as u32) as u64;
x = (x * x) >> 32;
let y = (x * reference_area_size as u64) >> 32;
/*
* Map into the selected lane.
*/
let relative_position = (start_position + reference_area_size - (y as usize + 1)) % lane_length;
reference_lane * lane_length + relative_position
}
+134
View File
@@ -0,0 +1,134 @@
/// One Argon2 memory block is 1024 bytes = 128 × u64.
pub(super) const BLOCK_WORDS: usize = 128;
/// One Argon2 memory block is 1024 bytes.
pub(super) const BLOCK_SIZE: usize = 1024;
/// A single 1024-byte Argon2 memory block.
#[derive(Clone, Copy)]
pub(super) struct Block(pub(super) [u64; BLOCK_WORDS]);
impl Block {
pub(super) const ZERO: Self = Self([0u64; BLOCK_WORDS]);
}
/// Argon2 compression function G.
///
/// R = X XOR Y
/// Z = P(R)
/// output = Z XOR R
///
/// On passes after the first, the old contents of the destination block are also XORed into the result.
pub(super) fn compress_block(previous: &Block, reference: &Block, old: Option<&Block>) -> Block {
let mut r = [0u64; BLOCK_WORDS];
for (dst, (&a, &b)) in r.iter_mut().zip(previous.0.iter().zip(reference.0.iter())) {
*dst = a ^ b;
}
/*
* P(R), first over rows and then over columns.
*/
let mut z = r;
/*
* Row-wise permutation.
*/
for row in 0..8 {
let start = row * 16;
let mut words = [0u64; 16];
words.copy_from_slice(&z[start..start + 16]);
round(&mut words);
z[start..start + 16].copy_from_slice(&words);
}
/*
* Column-wise permutation.
*
* Each permutation operates on two adjacent 64-bit
* columns from each of the eight rows.
*/
for column_pair in 0..8 {
let mut words = [0u64; 16];
for row in 0..8 {
let source = row * 16 + column_pair * 2;
words[row * 2] = z[source];
words[row * 2 + 1] = z[source + 1];
}
round(&mut words);
for row in 0..8 {
let destination = row * 16 + column_pair * 2;
z[destination] = words[row * 2];
z[destination + 1] = words[row * 2 + 1];
}
}
let mut output = [0u64; BLOCK_WORDS];
for i in 0..BLOCK_WORDS {
output[i] = z[i] ^ r[i] ^ old.map_or(0, |block| block.0[i]);
}
Block(output)
}
/// The Argon2/BLAKE2b quarter-round.
///
/// This is the BlaMka operation used by Argon2.
#[inline(always)]
fn g(v: &mut [u64; 16], a: usize, b: usize, c: usize, d: usize) {
v[a] = blamka(v[a], v[b]);
v[d] = (v[d] ^ v[a]).rotate_right(32);
v[c] = blamka(v[c], v[d]);
v[b] = (v[b] ^ v[c]).rotate_right(24);
v[a] = blamka(v[a], v[b]);
v[d] = (v[d] ^ v[a]).rotate_right(16);
v[c] = blamka(v[c], v[d]);
v[b] = (v[b] ^ v[c]).rotate_right(63);
}
/// BlaMka addition:
///
/// a + b + 2 * low32(a) * low32(b) mod 2^64.
#[inline(always)]
fn blamka(a: u64, b: u64) -> u64 {
let product = 2u64
.wrapping_mul(a & 0xffff_ffff)
.wrapping_mul(b & 0xffff_ffff);
a.wrapping_add(b).wrapping_add(product)
}
/// One BLAKE2b style 16 word round used by Argon2.
#[inline(always)]
fn round(v: &mut [u64; 16]) {
// Column step.
g(v, 0, 4, 8, 12);
g(v, 1, 5, 9, 13);
g(v, 2, 6, 10, 14);
g(v, 3, 7, 11, 15);
// Diagonal step.
g(v, 0, 5, 10, 15);
g(v, 1, 6, 11, 12);
g(v, 2, 7, 8, 13);
g(v, 3, 4, 9, 14);
}
+164
View File
@@ -0,0 +1,164 @@
use crate::blake2b::Blake2b;
use super::Argon2Params;
use super::compress::{BLOCK_SIZE, BLOCK_WORDS, Block};
use super::kdf::{ARGON2_VERSION, ARGON2ID_TYPE};
/// Compute the Argon2 initial hash H_0.
///
/// For our password KDF, the optional secret and associated-data
/// inputs are both empty, but their zero lengths are still encoded
/// because they are part of the Argon2 parameter structure.
pub(super) fn initial_hash(
password: &[u8],
salt: &[u8],
params: Argon2Params,
memory_kib: u32,
output_len: usize,
) -> [u8; 64] {
let mut input = Vec::with_capacity(6 * 4 + 4 + password.len() + 4 + salt.len() + 4 + 4);
// p
append_u32(&mut input, params.parallelism);
// T
append_u32(&mut input, output_len as u32);
// m'
append_u32(&mut input, memory_kib);
// t
append_u32(&mut input, params.time_cost);
// version
append_u32(&mut input, ARGON2_VERSION);
// type = Argon2id
append_u32(&mut input, ARGON2ID_TYPE);
// |P| and P
append_u32(&mut input, password.len() as u32);
input.extend_from_slice(password);
// |S| and S
append_u32(&mut input, salt.len() as u32);
input.extend_from_slice(salt);
// |K| = 0
append_u32(&mut input, 0);
// |X| = 0
append_u32(&mut input, 0);
let digest = Blake2b::hash(&input);
let mut output = [0u8; 64];
output.copy_from_slice(&digest);
output
}
/// Compute one of the two initial memory blocks.
pub(super) fn initial_block(h0: &[u8; 64], index: usize, lane: usize) -> Block {
let mut input = Vec::with_capacity(72);
input.extend_from_slice(h0);
append_u32(&mut input, index as u32);
append_u32(&mut input, lane as u32);
hash_long_block(&input)
}
/// Argon2 variable-length hash H'.
///
/// This is the H' construction specified by Argon2.
/// It ultimately uses the project's BLAKE2b implementation.
pub(super) fn hash_long(input: &[u8], output_len: usize) -> Vec<u8> {
assert!(output_len >= 1);
let mut prefixed = Vec::with_capacity(4 + input.len());
append_u32(&mut prefixed, output_len as u32);
prefixed.extend_from_slice(input);
/*
* For T <= 64:
*
* H'(T || A) = BLAKE2b-T(T || A)
*/
if output_len <= 64 {
return Blake2b::hash_with_output_length(&prefixed, output_len)
.expect("output length <= 64");
}
/*
* For T > 64:
*
* r = ceil(T / 32) - 2
*
* V_1 = H^64(T || A)
* V_i = H^64(V_{i-1})
* final = H^(T - 32*r)(V_r)
*/
let r = output_len.div_ceil(32) - 2;
let mut output = Vec::with_capacity(output_len);
let mut v = Blake2b::hash(&prefixed);
output.extend_from_slice(&v[..32]);
for _ in 1..r {
v = Blake2b::hash(&v);
output.extend_from_slice(&v[..32]);
}
let final_len = output_len - 32 * r;
v = Blake2b::hash_with_output_length(&v, final_len).expect("final H' output length is valid");
output.extend_from_slice(&v);
debug_assert_eq!(output.len(), output_len);
output
}
/// Compute H'(input, 1024).
fn hash_long_block(input: &[u8]) -> Block {
let digest = hash_long(input, BLOCK_SIZE);
let mut words = [0u64; BLOCK_WORDS];
for (i, word) in words.iter_mut().enumerate() {
let start = i * 8;
*word = u64::from_le_bytes(
digest[start..start + 8]
.try_into()
.expect("Argon2 block is 1024 bytes"),
);
}
Block(words)
}
/// Serialize an Argon2 block in little-endian order.
pub(super) fn block_to_bytes(block: &Block) -> Vec<u8> {
let mut output = Vec::with_capacity(BLOCK_SIZE);
for word in block.0 {
output.extend_from_slice(&word.to_le_bytes());
}
output
}
/// Append a little-endian u32.
#[inline]
fn append_u32(output: &mut Vec<u8>, value: u32) {
output.extend_from_slice(&value.to_le_bytes());
}
+88 -13
View File
@@ -1,24 +1,99 @@
//! Password-to-key derivation.
//! PBKDF using Argon2id.
//!
//! This currently uses SHA3-512 directly
//! Argon2id version 1.3 using the project's BLAKE2b implementation.
//!
//! This is not a proper password-based KDF. There is currently no salt,
//! work factor, memory hardness, or iteration count.
//! The password is encoded as UTF-8 and combined with a random,
//! per file salt. The salt is not secret and must be stored alongside
//! the encrypted file so that the same key can be derived during
//! decryption.
//!
//! Default parameters:
//! - Memory: 64 MiB
//! - Passes: 3
//! - Parallelism: 4 lanes
//! - Output: 64 bytes
//!
//! These defaults follow the memory-constrained recommendation from
//! RFC 9106. The implementation itself is single-threaded; the
//! `parallelism` parameter is still part of the Argon2 computation.
use crate::keccak::sha3_512;
use super::argon2::argon2id;
/// Length of the key produced for `KeccakAead`.
pub const DERIVED_KEY_LEN: usize = 64;
/// Derive a 64-byte encryption key from a password.
/// Argon2 salt length.
///
/// The password is encoded as UTF-8 and hashed using the project's
/// SHA3-512 implementation.
pub fn derive_file_key(password: &str) -> [u8; DERIVED_KEY_LEN] {
let digest = sha3_512(password.as_bytes());
/// RFC 9106 recommends a 16-byte salt.
pub const SALT_LEN: usize = 16;
let mut key = [0u8; DERIVED_KEY_LEN];
key.copy_from_slice(&digest[..DERIVED_KEY_LEN]);
/// Default Argon2id memory cost in KiB.
///
/// 64 MiB.
pub const ARGON2_MEMORY_KIB: u32 = 64 * 1024;
key
/// Default Argon2id time cost.
pub const ARGON2_TIME_COST: u32 = 3;
/// Default Argon2id parallelism.
pub const ARGON2_PARALLELISM: u32 = 4;
/// Argon2 version 1.3.
pub(super) const ARGON2_VERSION: u32 = 0x13;
/// Argon2id type identifier.
pub(super) const ARGON2ID_TYPE: u32 = 2;
/// Argon2 parameter set.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Argon2Params {
/// Memory cost in KiB.
pub memory_kib: u32,
/// Number of passes.
pub time_cost: u32,
/// Number of lanes.
pub parallelism: u32,
}
impl Default for Argon2Params {
fn default() -> Self {
Self {
memory_kib: ARGON2_MEMORY_KIB,
time_cost: ARGON2_TIME_COST,
parallelism: ARGON2_PARALLELISM,
}
}
}
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KdfError {
InvalidMemoryCost,
InvalidTimeCost,
InvalidParallelism,
}
/// Derive a 64-byte encryption key from a password and a 16-byte salt.
///
/// The salt is randomly generated for each encrypted file and stored in its header. It is not secret.
pub fn derive_file_key(password: &str, salt: &[u8; SALT_LEN]) -> [u8; DERIVED_KEY_LEN] {
derive_file_key_with_params(password, salt, Argon2Params::default())
.expect("default Argon2 parameters are valid")
}
/// Derive a 64-byte key with explicit Argon2 parameters.
///
/// This is mainly useful for testing and benchmarking.
pub fn derive_file_key_with_params(
password: &str,
salt: &[u8; SALT_LEN],
params: Argon2Params,
) -> Result<[u8; DERIVED_KEY_LEN], KdfError> {
let digest = argon2id(password.as_bytes(), salt, params, DERIVED_KEY_LEN)?;
Ok(digest
.try_into()
.expect("Argon2 output length must be DERIVED_KEY_LEN"))
}
+7 -1
View File
@@ -1,6 +1,12 @@
mod argon2;
mod compress;
mod hash;
mod kdf;
#[cfg(test)]
mod test;
pub use kdf::derive_file_key;
pub use kdf::{
ARGON2_MEMORY_KIB, ARGON2_PARALLELISM, ARGON2_TIME_COST, Argon2Params, DERIVED_KEY_LEN,
KdfError, SALT_LEN, derive_file_key, derive_file_key_with_params,
};
+296 -13
View File
@@ -1,26 +1,309 @@
use super::*;
use crate::kdf::kdf::DERIVED_KEY_LEN;
#[test]
fn key_has_expected_length() {
assert_eq!(DERIVED_KEY_LEN, 64);
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
let key = derive_file_key("test password");
assert_eq!(key.len(), DERIVED_KEY_LEN);
fn salt() -> [u8; SALT_LEN] {
*b"0123456789abcdef"
}
#[test]
fn same_password_same_key() {
let a = derive_file_key("correct horse battery staple");
let b = derive_file_key("correct horse battery staple");
fn argon2id_known_vector() {
/*
* Password: "abc"
* Salt: "0123456789abcdef"
* Memory: 32 KiB
* Passes: 3
* Parallelism: 4
* Output: 64 bytes
* Version: 1.3
* Type: Argon2id
*/
let key = derive_file_key_with_params(
"abc",
&salt(),
Argon2Params {
memory_kib: 32,
time_cost: 3,
parallelism: 4,
},
)
.unwrap();
assert_eq!(a, b);
assert_eq!(
hex(&key),
"db750d0e8a56f66f4b70c0b24ee453c03f35c90edfa90715b203396b0be4bd4c\
ed0884204bbb1e5221db12de6554cdb3b7624f9bbf3336b7797a90871be1f11a"
.replace(' ', "")
.replace('\n', "")
);
}
#[test]
fn argon2id_empty_password() {
let key = derive_file_key_with_params(
"",
&salt(),
Argon2Params {
memory_kib: 32,
time_cost: 1,
parallelism: 1,
},
)
.unwrap();
assert_eq!(
hex(&key),
"9a32bc5205d518e7692d11451a825efd8c6a6dcb5cfaa1d511c08f3961a701ff\
a0e9bfc5163d4e90af944ec81a2fbc37afc2e6ed04807e96f66cd7737cb037c4"
.replace(' ', "")
.replace('\n', "")
);
}
#[test]
fn different_passwords_produce_different_keys() {
let a = derive_file_key("password1");
let b = derive_file_key("password2");
let salt = salt();
assert_ne!(a, b);
let key1 = derive_file_key_with_params(
"password1",
&salt,
Argon2Params {
memory_kib: 32,
time_cost: 1,
parallelism: 1,
},
)
.unwrap();
let key2 = derive_file_key_with_params(
"password2",
&salt,
Argon2Params {
memory_kib: 32,
time_cost: 1,
parallelism: 1,
},
)
.unwrap();
assert_ne!(key1, key2);
}
#[test]
fn different_salts_produce_different_keys() {
let salt1 = [0x01u8; SALT_LEN];
let salt2 = [0x02u8; SALT_LEN];
let key1 = derive_file_key_with_params(
"password",
&salt1,
Argon2Params {
memory_kib: 32,
time_cost: 1,
parallelism: 1,
},
)
.unwrap();
let key2 = derive_file_key_with_params(
"password",
&salt2,
Argon2Params {
memory_kib: 32,
time_cost: 1,
parallelism: 1,
},
)
.unwrap();
assert_ne!(key1, key2);
}
#[test]
fn same_password_and_salt_are_deterministic() {
let salt = salt();
let params = Argon2Params {
memory_kib: 32,
time_cost: 1,
parallelism: 1,
};
let key1 = derive_file_key_with_params("password", &salt, params).unwrap();
let key2 = derive_file_key_with_params("password", &salt, params).unwrap();
assert_eq!(key1, key2);
}
#[test]
fn unicode_password_is_supported() {
let salt = salt();
let key = derive_file_key_with_params(
"mot de passe 🔐",
&salt,
Argon2Params {
memory_kib: 32,
time_cost: 1,
parallelism: 1,
},
)
.unwrap();
assert_eq!(key.len(), DERIVED_KEY_LEN);
}
#[test]
fn output_is_always_64_bytes() {
let salt = salt();
let key = derive_file_key_with_params(
"password",
&salt,
Argon2Params {
memory_kib: 32,
time_cost: 1,
parallelism: 1,
},
)
.unwrap();
assert_eq!(key.len(), DERIVED_KEY_LEN);
assert_eq!(DERIVED_KEY_LEN, 64);
}
#[test]
fn invalid_time_cost() {
let result = derive_file_key_with_params(
"password",
&salt(),
Argon2Params {
memory_kib: 32,
time_cost: 0,
parallelism: 1,
},
);
assert_eq!(result.unwrap_err(), KdfError::InvalidTimeCost);
}
#[test]
fn invalid_parallelism() {
let result = derive_file_key_with_params(
"password",
&salt(),
Argon2Params {
memory_kib: 32,
time_cost: 1,
parallelism: 0,
},
);
assert_eq!(result.unwrap_err(), KdfError::InvalidParallelism);
}
#[test]
fn invalid_memory_cost() {
let result = derive_file_key_with_params(
"password",
&salt(),
Argon2Params {
memory_kib: 31,
time_cost: 1,
parallelism: 4,
},
);
assert_eq!(result.unwrap_err(), KdfError::InvalidMemoryCost);
}
#[test]
fn minimum_memory_cost() {
/*
* Argon2 requires at least 8 KiB per lane.
*
* With p = 1, 8 KiB is therefore the minimum valid
* memory cost.
*/
let result = derive_file_key_with_params(
"password",
&salt(),
Argon2Params {
memory_kib: 8,
time_cost: 1,
parallelism: 1,
},
);
assert!(result.is_ok());
}
#[test]
fn memory_cost_is_rounded_down() {
/*
* m' must be a multiple of 4*p.
*
* 33 KiB with p=1 is therefore rounded to 32 KiB.
*/
let rounded = derive_file_key_with_params(
"password",
&salt(),
Argon2Params {
memory_kib: 33,
time_cost: 1,
parallelism: 1,
},
)
.unwrap();
let exact = derive_file_key_with_params(
"password",
&salt(),
Argon2Params {
memory_kib: 32,
time_cost: 1,
parallelism: 1,
},
)
.unwrap();
assert_eq!(rounded, exact);
}
#[test]
fn default_parameters_produce_64_byte_key() {
/*
* This deliberately runs the real production parameters:
*
* 64 MiB, 3 passes, 4 lanes.
*
*/
let key = derive_file_key("password", &salt());
assert_eq!(key.len(), DERIVED_KEY_LEN);
}
#[test]
fn default_parameters_match_reference() {
/*
* Production parameters:
* m = 65536 KiB
* t = 3
* p = 4
* T = 64
*
* This vector was independently generated with a reference Argon2id implementation.
*/
let key = derive_file_key("password", &salt());
assert_eq!(
hex(&key),
"ceb3d36b4076a38bd44d73139c58916ed33708a99e2941137c64ef51d38d3195\
8e2dd94fcbbce2b62aeb049ab43781dc0351fcf3071e7b3cb3031fe0a71cffc9"
.replace(' ', "")
.replace('\n', "")
);
}
+2 -7
View File
@@ -10,7 +10,6 @@ mod keccak;
mod keccak_aead;
use file_crypt::{decrypt_file, encrypt_file};
use kdf::derive_file_key;
/// Encrypt and decrypt files using KeccakAead.
#[derive(Parser, Debug)]
@@ -66,9 +65,7 @@ fn main() -> Result<(), Box<dyn Error>> {
output,
password,
} => {
let key = derive_file_key(&password);
encrypt_file(&input, &output, &key)?;
encrypt_file(&input, &output, &password)?;
println!("Encrypted '{}' -> '{}'", input.display(), output.display());
}
@@ -78,9 +75,7 @@ fn main() -> Result<(), Box<dyn Error>> {
output,
password,
} => {
let key = derive_file_key(&password);
decrypt_file(&input, &output, &key)?;
decrypt_file(&input, &output, &password)?;
println!("Decrypted '{}' -> '{}'", input.display(), output.display());
}