PBKDF: implement argon2id
This commit is contained in:
1 parent
1fcf38db41
commit
b30b5d0add
13 files changed
+1291
-148
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
# filecrypt-rs
|
# filecrypt-rs
|
||||||
|
|
||||||
Cli tool in Rust to encrypt and decrypt files with an Ascon-Keccak AEAD.
|
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
|
## Commands
|
||||||
|
|
||||||
|
|||||||
+10
-11
@@ -2,21 +2,21 @@ use std::fs::File;
|
|||||||
use std::io::BufWriter;
|
use std::io::BufWriter;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use super::BUF_SIZE;
|
||||||
use super::atomic::AtomicOutput;
|
use super::atomic::AtomicOutput;
|
||||||
use super::error::FileCryptError;
|
use super::error::FileCryptError;
|
||||||
use super::stream::{decrypt_stream, encrypt_stream};
|
use super::stream::{decrypt_stream, encrypt_stream};
|
||||||
use super::{BUF_SIZE, check_key};
|
|
||||||
|
|
||||||
/// Encrypt the file at `input` into `output`.
|
/// 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.
|
/// `output` is only replaced if encryption succeeds.
|
||||||
pub fn encrypt_file(
|
pub fn encrypt_file(
|
||||||
input: impl AsRef<Path>,
|
input: impl AsRef<Path>,
|
||||||
output: impl AsRef<Path>,
|
output: impl AsRef<Path>,
|
||||||
key: &[u8],
|
password: &str,
|
||||||
) -> Result<(), FileCryptError> {
|
) -> Result<(), FileCryptError> {
|
||||||
check_key(key)?;
|
|
||||||
|
|
||||||
let input_file = File::open(input.as_ref())?;
|
let input_file = File::open(input.as_ref())?;
|
||||||
|
|
||||||
let plaintext_len = input_file.metadata()?.len();
|
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);
|
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
|
let out_file = writer
|
||||||
.into_inner()
|
.into_inner()
|
||||||
@@ -41,22 +41,21 @@ pub fn encrypt_file(
|
|||||||
|
|
||||||
/// Decrypt the file at `input` into `output`.
|
/// Decrypt the file at `input` into `output`.
|
||||||
///
|
///
|
||||||
/// The destination is left untouched if authentication or any other part
|
/// The Argon2id salt is read from the encrypted file header.
|
||||||
/// of decryption fails.
|
///
|
||||||
|
/// The destination is left untouched if authentication or any other part of decryption fails.
|
||||||
pub fn decrypt_file(
|
pub fn decrypt_file(
|
||||||
input: impl AsRef<Path>,
|
input: impl AsRef<Path>,
|
||||||
output: impl AsRef<Path>,
|
output: impl AsRef<Path>,
|
||||||
key: &[u8],
|
password: &str,
|
||||||
) -> Result<(), FileCryptError> {
|
) -> Result<(), FileCryptError> {
|
||||||
check_key(key)?;
|
|
||||||
|
|
||||||
let input_file = File::open(input.as_ref())?;
|
let input_file = File::open(input.as_ref())?;
|
||||||
|
|
||||||
let (guard, out_file) = AtomicOutput::create(output.as_ref())?;
|
let (guard, out_file) = AtomicOutput::create(output.as_ref())?;
|
||||||
|
|
||||||
let mut writer = BufWriter::with_capacity(BUF_SIZE, out_file);
|
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
|
let out_file = writer
|
||||||
.into_inner()
|
.into_inner()
|
||||||
|
|||||||
@@ -1,27 +1,53 @@
|
|||||||
|
use crate::kdf::{DERIVED_KEY_LEN, SALT_LEN};
|
||||||
|
|
||||||
use super::error::FileCryptError;
|
use super::error::FileCryptError;
|
||||||
|
|
||||||
pub const IV_LEN: usize = 16;
|
pub const IV_LEN: usize = 16;
|
||||||
pub const NONCE_LEN: usize = 16;
|
pub const NONCE_LEN: usize = 16;
|
||||||
|
|
||||||
/// The tag is as long as the key.
|
|
||||||
pub const MIN_KEY_LEN: usize = 16;
|
pub const MIN_KEY_LEN: usize = 16;
|
||||||
|
|
||||||
/// Must match `CAPACITY_SIZE` in `keccak_aead.rs`.
|
pub const MAX_KEY_LEN: usize = 64;
|
||||||
pub const MAX_KEY_LEN: usize = 24;
|
|
||||||
|
|
||||||
pub const HEADER_LEN: usize = 48;
|
pub const HEADER_LEN: usize = 64;
|
||||||
|
|
||||||
const MAGIC: [u8; 4] = *b"KAEF";
|
const MAGIC: [u8; 4] = *b"KAEF";
|
||||||
const VERSION: u8 = 1;
|
const VERSION: u8 = 2;
|
||||||
|
|
||||||
pub(super) struct Header {
|
pub(super) struct Header {
|
||||||
pub(super) key_len: usize,
|
pub(super) key_len: usize,
|
||||||
pub(super) iv: [u8; IV_LEN],
|
pub(super) iv: [u8; IV_LEN],
|
||||||
pub(super) nonce: [u8; NONCE_LEN],
|
pub(super) nonce: [u8; NONCE_LEN],
|
||||||
|
pub(super) salt: [u8; SALT_LEN],
|
||||||
pub(super) plaintext_len: u64,
|
pub(super) plaintext_len: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Header {
|
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] {
|
pub(super) fn to_bytes(&self) -> [u8; HEADER_LEN] {
|
||||||
let mut out = [0u8; HEADER_LEN];
|
let mut out = [0u8; HEADER_LEN];
|
||||||
|
|
||||||
@@ -33,7 +59,8 @@ impl Header {
|
|||||||
|
|
||||||
out[8..24].copy_from_slice(&self.iv);
|
out[8..24].copy_from_slice(&self.iv);
|
||||||
out[24..40].copy_from_slice(&self.nonce);
|
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
|
out
|
||||||
}
|
}
|
||||||
@@ -55,9 +82,12 @@ impl Header {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
key_len,
|
key_len,
|
||||||
iv: bytes[8..24].try_into().unwrap(),
|
iv: bytes[8..24].try_into().expect("IV length is constant"),
|
||||||
nonce: bytes[24..40].try_into().unwrap(),
|
nonce: bytes[24..40].try_into().expect("nonce length is constant"),
|
||||||
plaintext_len: u64::from_le_bytes(bytes[40..48].try_into().unwrap()),
|
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"),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@ mod stream;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
pub use crate::kdf::SALT_LEN;
|
||||||
|
|
||||||
pub use error::FileCryptError;
|
pub use error::FileCryptError;
|
||||||
pub use file::{decrypt_file, encrypt_file};
|
pub use file::{decrypt_file, encrypt_file};
|
||||||
pub use format::{HEADER_LEN, MAX_KEY_LEN, MIN_KEY_LEN};
|
pub use format::{HEADER_LEN, MAX_KEY_LEN, MIN_KEY_LEN};
|
||||||
|
|||||||
+48
-22
@@ -2,12 +2,42 @@ use std::io::{self, Read, Write};
|
|||||||
|
|
||||||
use subtle::ConstantTimeEq;
|
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 crate::keccak_aead::KeccakAead;
|
||||||
|
|
||||||
use super::error::FileCryptError;
|
use super::error::FileCryptError;
|
||||||
use super::format::Header;
|
use super::format::Header;
|
||||||
use super::{BUF_SIZE, HEADER_LEN, check_key};
|
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`.
|
/// `read_exact`, but EOF becomes `on_eof` instead of an `io::Error`.
|
||||||
fn read_exact_or<R: Read>(
|
fn read_exact_or<R: Read>(
|
||||||
reader: &mut R,
|
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`.
|
/// 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>(
|
pub fn encrypt_stream<R: Read, W: Write>(
|
||||||
mut reader: R,
|
mut reader: R,
|
||||||
mut writer: W,
|
mut writer: W,
|
||||||
key: &[u8],
|
password: &str,
|
||||||
plaintext_len: u64,
|
plaintext_len: u64,
|
||||||
) -> Result<(), FileCryptError> {
|
) -> Result<(), FileCryptError> {
|
||||||
check_key(key)?;
|
let header = Header::new(plaintext_len)?;
|
||||||
|
|
||||||
let mut iv = [0u8; 16];
|
let key = derive_stream_key(password, &header.salt);
|
||||||
let mut nonce = [0u8; 16];
|
|
||||||
|
|
||||||
getrandom::fill(&mut iv).map_err(FileCryptError::Random)?;
|
check_key(&key)?;
|
||||||
|
|
||||||
getrandom::fill(&mut nonce).map_err(FileCryptError::Random)?;
|
|
||||||
|
|
||||||
let header = Header {
|
|
||||||
key_len: key.len(),
|
|
||||||
iv,
|
|
||||||
nonce,
|
|
||||||
plaintext_len,
|
|
||||||
};
|
|
||||||
|
|
||||||
let header_bytes = header.to_bytes();
|
let header_bytes = header.to_bytes();
|
||||||
|
|
||||||
writer.write_all(&header_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);
|
aead.associated_data_processing(&header_bytes);
|
||||||
|
|
||||||
@@ -95,7 +117,7 @@ pub fn encrypt_stream<R: Read, W: Write>(
|
|||||||
return Err(FileCryptError::InputLengthChanged);
|
return Err(FileCryptError::InputLengthChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tag = aead.finalize(key);
|
let tag = aead.finalize(&key);
|
||||||
|
|
||||||
writer.write_all(&tag)?;
|
writer.write_all(&tag)?;
|
||||||
writer.flush()?;
|
writer.flush()?;
|
||||||
@@ -105,20 +127,24 @@ pub fn encrypt_stream<R: Read, W: Write>(
|
|||||||
|
|
||||||
/// Decrypt `reader` into `writer` and verify the authentication tag.
|
/// 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.
|
/// 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>(
|
pub fn decrypt_stream<R: Read, W: Write>(
|
||||||
mut reader: R,
|
mut reader: R,
|
||||||
mut writer: W,
|
mut writer: W,
|
||||||
key: &[u8],
|
password: &str,
|
||||||
) -> Result<(), FileCryptError> {
|
) -> Result<(), FileCryptError> {
|
||||||
check_key(key)?;
|
|
||||||
|
|
||||||
let mut header_bytes = [0u8; HEADER_LEN];
|
let mut header_bytes = [0u8; HEADER_LEN];
|
||||||
|
|
||||||
read_exact_or(&mut reader, &mut header_bytes, FileCryptError::Truncated)?;
|
read_exact_or(&mut reader, &mut header_bytes, FileCryptError::Truncated)?;
|
||||||
|
|
||||||
let header = Header::from_bytes(&header_bytes)?;
|
let header = Header::from_bytes(&header_bytes)?;
|
||||||
|
|
||||||
|
let key = derive_stream_key(password, &header.salt);
|
||||||
|
|
||||||
|
check_key(&key)?;
|
||||||
|
|
||||||
if header.key_len != key.len() {
|
if header.key_len != key.len() {
|
||||||
return Err(FileCryptError::KeyLengthMismatch {
|
return Err(FileCryptError::KeyLengthMismatch {
|
||||||
header: header.key_len,
|
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);
|
aead.associated_data_processing(&header_bytes);
|
||||||
|
|
||||||
@@ -153,7 +179,7 @@ pub fn decrypt_stream<R: Read, W: Write>(
|
|||||||
return Err(FileCryptError::TrailingData);
|
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();
|
let tags_match: bool = computed_tag.as_slice().ct_eq(stored_tag.as_slice()).into();
|
||||||
|
|
||||||
|
|||||||
+183
-71
@@ -1,9 +1,18 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::file_crypt::stream::{decrypt_stream, encrypt_stream};
|
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::fs;
|
||||||
use std::io::Cursor;
|
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> {
|
fn sample(n: usize) -> Vec<u8> {
|
||||||
(0..n)
|
(0..n)
|
||||||
@@ -11,13 +20,13 @@ fn sample(n: usize) -> Vec<u8> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enc(key: &[u8], plaintext: &[u8]) -> Vec<u8> {
|
fn enc(password: &str, plaintext: &[u8]) -> Vec<u8> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
|
|
||||||
encrypt_stream(
|
encrypt_stream(
|
||||||
Cursor::new(plaintext),
|
Cursor::new(plaintext),
|
||||||
&mut out,
|
&mut out,
|
||||||
key,
|
password,
|
||||||
plaintext.len() as u64,
|
plaintext.len() as u64,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -25,10 +34,10 @@ fn enc(key: &[u8], plaintext: &[u8]) -> Vec<u8> {
|
|||||||
out
|
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();
|
let mut out = Vec::new();
|
||||||
|
|
||||||
decrypt_stream(Cursor::new(data), &mut out, key)?;
|
decrypt_stream(Cursor::new(data), &mut out, password)?;
|
||||||
|
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
@@ -53,74 +62,109 @@ fn boundary_sizes() -> Vec<usize> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::keccak_aead::KeccakAead;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn roundtrip_various_sizes() {
|
fn roundtrip_various_sizes() {
|
||||||
for n in boundary_sizes() {
|
for n in boundary_sizes() {
|
||||||
let plaintext = sample(n);
|
let plaintext = sample(n);
|
||||||
let encrypted = enc(&KEY, &plaintext);
|
let encrypted = enc(PASSWORD, &plaintext);
|
||||||
assert_eq!(encrypted.len(), HEADER_LEN + n + KEY.len(), "size {n}");
|
|
||||||
assert_eq!(dec(&KEY, &encrypted).unwrap(), plaintext, "size {n}");
|
assert_eq!(
|
||||||
|
encrypted.len(),
|
||||||
|
HEADER_LEN + n + DERIVED_KEY_LEN,
|
||||||
|
"size {n}"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(dec(PASSWORD, &encrypted).unwrap(), plaintext, "size {n}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn roundtrip_all_key_lengths() {
|
fn header_contains_derived_key_length() {
|
||||||
for key_len in MIN_KEY_LEN..=MAX_KEY_LEN {
|
let encrypted = enc(PASSWORD, &sample(100));
|
||||||
let key = vec![0xA5u8; key_len];
|
|
||||||
let plaintext = sample(100);
|
assert_eq!(encrypted[5] as usize, DERIVED_KEY_LEN);
|
||||||
let encrypted = enc(&key, &plaintext);
|
|
||||||
assert_eq!(encrypted[5] as usize, key_len);
|
assert_eq!(encrypted.len(), HEADER_LEN + 100 + DERIVED_KEY_LEN);
|
||||||
assert_eq!(encrypted.len(), HEADER_LEN + 100 + key_len);
|
|
||||||
assert_eq!(dec(&key, &encrypted).unwrap(), plaintext);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Chunked streaming must be byte-identical to the one-shot AEAD.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn matches_one_shot_aead() {
|
fn matches_one_shot_aead() {
|
||||||
for n in boundary_sizes() {
|
for n in boundary_sizes() {
|
||||||
let plaintext = sample(n);
|
let plaintext = sample(n);
|
||||||
let encrypted = enc(&KEY, &plaintext);
|
let encrypted = enc(PASSWORD, &plaintext);
|
||||||
|
|
||||||
let header = &encrypted[..HEADER_LEN];
|
let header = &encrypted[..HEADER_LEN];
|
||||||
|
|
||||||
let iv = &header[8..24];
|
let iv = &header[8..24];
|
||||||
let nonce = &header[24..40];
|
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!(
|
assert_eq!(
|
||||||
&encrypted[HEADER_LEN..HEADER_LEN + n],
|
&encrypted[HEADER_LEN..HEADER_LEN + n],
|
||||||
&expected.cipher[..],
|
&expected.cipher[..],
|
||||||
"size {n}"
|
"size {n}"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(&encrypted[HEADER_LEN + n..], &expected.tag[..], "size {n}");
|
assert_eq!(&encrypted[HEADER_LEN + n..], &expected.tag[..], "size {n}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iv_and_nonce_are_random() {
|
fn iv_nonce_and_salt_are_random() {
|
||||||
let a = enc(&KEY, b"same plaintext");
|
let a = enc(PASSWORD, b"same plaintext");
|
||||||
let b = enc(&KEY, b"same plaintext");
|
let b = enc(PASSWORD, b"same plaintext");
|
||||||
|
|
||||||
assert_ne!(a[8..24], b[8..24], "IV repeated");
|
assert_ne!(a[8..24], b[8..24], "IV repeated");
|
||||||
|
|
||||||
assert_ne!(a[24..40], b[24..40], "nonce 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..]);
|
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]
|
#[test]
|
||||||
fn every_single_bit_flip_is_rejected() {
|
fn every_single_bit_flip_is_rejected() {
|
||||||
let plaintext = sample(100);
|
let plaintext = sample(100);
|
||||||
let encrypted = enc(&KEY, &plaintext);
|
let encrypted = enc(PASSWORD, &plaintext);
|
||||||
|
|
||||||
for pos in 0..encrypted.len() {
|
for pos in 0..encrypted.len() {
|
||||||
for bit in [0x01u8, 0x80] {
|
for bit in [0x01u8, 0x80] {
|
||||||
let mut tampered = encrypted.clone();
|
let mut tampered = encrypted.clone();
|
||||||
tampered[pos] ^= bit;
|
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");
|
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 {
|
if pos >= HEADER_LEN {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(result, Err(FileCryptError::AuthenticationFailed)),
|
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]
|
#[test]
|
||||||
fn trailing_zero_byte_forgery_is_rejected() {
|
fn trailing_zero_byte_forgery_is_rejected() {
|
||||||
let plaintext = sample(20); // final chunk has 4 bytes
|
let plaintext = sample(20);
|
||||||
let encrypted = enc(&KEY, &plaintext);
|
let encrypted = enc(PASSWORD, &plaintext);
|
||||||
|
|
||||||
let mut forged = encrypted[..HEADER_LEN + 20].to_vec();
|
let mut forged = encrypted[..HEADER_LEN + 20].to_vec();
|
||||||
|
|
||||||
forged.push(0x00);
|
forged.push(0x00);
|
||||||
|
|
||||||
forged.extend_from_slice(&encrypted[HEADER_LEN + 20..]);
|
forged.extend_from_slice(&encrypted[HEADER_LEN + 20..]);
|
||||||
|
|
||||||
// Without touching the header: structural error.
|
// Without modifying the authenticated length,
|
||||||
assert!(dec(&KEY, &forged).is_err());
|
// 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!(
|
assert!(matches!(
|
||||||
dec(&KEY, &forged),
|
dec(PASSWORD, &forged),
|
||||||
Err(FileCryptError::AuthenticationFailed)
|
Err(FileCryptError::AuthenticationFailed)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn truncation_and_trailing_data() {
|
fn truncation_and_trailing_data() {
|
||||||
let encrypted = enc(&KEY, &sample(100));
|
let encrypted = enc(PASSWORD, &sample(100));
|
||||||
|
|
||||||
for cut in [
|
for cut in [
|
||||||
0,
|
0,
|
||||||
@@ -167,109 +213,167 @@ mod tests {
|
|||||||
encrypted.len() - 1,
|
encrypted.len() - 1,
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(dec(&KEY, &encrypted[..cut]), Err(FileCryptError::Truncated)),
|
matches!(
|
||||||
|
dec(PASSWORD, &encrypted[..cut]),
|
||||||
|
Err(FileCryptError::Truncated)
|
||||||
|
),
|
||||||
"cut at {cut}"
|
"cut at {cut}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut extended = encrypted.clone();
|
let mut extended = encrypted.clone();
|
||||||
|
|
||||||
extended.push(0);
|
extended.push(0);
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
dec(&KEY, &extended),
|
dec(PASSWORD, &extended),
|
||||||
Err(FileCryptError::TrailingData)
|
Err(FileCryptError::TrailingData)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wrong_key_and_key_length() {
|
fn wrong_password() {
|
||||||
let encrypted = enc(&KEY, &sample(50));
|
let encrypted = enc(PASSWORD, &sample(50));
|
||||||
|
|
||||||
let mut other = KEY;
|
|
||||||
other[0] ^= 1;
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
dec(&other, &encrypted),
|
dec("wrong password", &encrypted),
|
||||||
Err(FileCryptError::AuthenticationFailed)
|
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!(
|
assert!(matches!(
|
||||||
dec(&[0x42u8; 24], &encrypted),
|
dec(PASSWORD, &malformed),
|
||||||
Err(FileCryptError::KeyLengthMismatch {
|
Err(FileCryptError::KeyLengthMismatch {
|
||||||
header: 16,
|
header: 16,
|
||||||
provided: 24
|
provided: 64
|
||||||
})
|
})
|
||||||
));
|
));
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
dec(&[0x42u8; 8], &encrypted),
|
super::check_key(&[0u8; 15]),
|
||||||
Err(FileCryptError::InvalidKeyLength(8))
|
Err(FileCryptError::InvalidKeyLength(15))
|
||||||
));
|
));
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
encrypt_stream(Cursor::new(b"x"), Vec::new(), &[0u8; 25], 1),
|
super::check_key(&[0u8; 65]),
|
||||||
Err(FileCryptError::InvalidKeyLength(25))
|
Err(FileCryptError::InvalidKeyLength(65))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn encrypt_detects_length_change() {
|
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)));
|
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)));
|
assert!(matches!(too_long, Err(FileCryptError::InputLengthChanged)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn garbage_headers() {
|
fn garbage_headers() {
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
dec(&KEY, &[0u8; HEADER_LEN]),
|
dec(PASSWORD, &[0u8; HEADER_LEN]),
|
||||||
Err(FileCryptError::BadMagic)
|
Err(FileCryptError::BadMagic)
|
||||||
));
|
));
|
||||||
|
|
||||||
let mut bad_version = enc(&KEY, b"x");
|
let mut bad_version = enc(PASSWORD, b"x");
|
||||||
bad_version[4] = 2;
|
|
||||||
|
bad_version[4] = 3;
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
dec(&KEY, &bad_version),
|
dec(PASSWORD, &bad_version),
|
||||||
Err(FileCryptError::UnsupportedVersion(2))
|
Err(FileCryptError::UnsupportedVersion(3))
|
||||||
));
|
));
|
||||||
|
|
||||||
// Absurd declared length on a tiny file: must fail, not allocate.
|
/*
|
||||||
let mut huge = enc(&KEY, b"x");
|
* Absurd declared length on a tiny file:
|
||||||
huge[40..48].copy_from_slice(&u64::MAX.to_le_bytes());
|
* decryption must fail without trying to allocate
|
||||||
assert!(matches!(dec(&KEY, &huge), Err(FileCryptError::Truncated)));
|
* 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]
|
#[test]
|
||||||
fn file_roundtrip_and_atomic_failure() {
|
fn file_roundtrip_and_atomic_failure() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
let plain = dir.path().join("plain.bin");
|
let plain = dir.path().join("plain.bin");
|
||||||
|
|
||||||
let encrypted = dir.path().join("plain.bin.kaef");
|
let encrypted = dir.path().join("plain.bin.kaef");
|
||||||
|
|
||||||
let decrypted = dir.path().join("decrypted.bin");
|
let decrypted = dir.path().join("decrypted.bin");
|
||||||
|
|
||||||
let data = sample(3 * BUF_SIZE + 5);
|
let data = sample(3 * BUF_SIZE + 5);
|
||||||
|
|
||||||
fs::write(&plain, &data).unwrap();
|
fs::write(&plain, &data).unwrap();
|
||||||
|
|
||||||
encrypt_file(&plain, &encrypted, &KEY).unwrap();
|
encrypt_file(&plain, &encrypted, PASSWORD).unwrap();
|
||||||
decrypt_file(&encrypted, &decrypted, &KEY).unwrap();
|
|
||||||
|
decrypt_file(&encrypted, &decrypted, PASSWORD).unwrap();
|
||||||
|
|
||||||
assert_eq!(fs::read(&decrypted).unwrap(), data);
|
assert_eq!(fs::read(&decrypted).unwrap(), data);
|
||||||
|
|
||||||
// Corrupt one ciphertext byte.
|
// Corrupt one ciphertext byte.
|
||||||
let mut bytes = fs::read(&encrypted).unwrap();
|
let mut bytes = fs::read(&encrypted).unwrap();
|
||||||
|
|
||||||
bytes[HEADER_LEN + 1000] ^= 1;
|
bytes[HEADER_LEN + 1000] ^= 1;
|
||||||
|
|
||||||
let corrupted = dir.path().join("corrupted.kaef");
|
let corrupted = dir.path().join("corrupted.kaef");
|
||||||
|
|
||||||
fs::write(&corrupted, &bytes).unwrap();
|
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");
|
let out_new = dir.path().join("out_new.bin");
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
decrypt_file(&corrupted, &out_new, &KEY),
|
decrypt_file(&corrupted, &out_new, PASSWORD,),
|
||||||
Err(FileCryptError::AuthenticationFailed)
|
Err(FileCryptError::AuthenticationFailed)
|
||||||
));
|
));
|
||||||
|
|
||||||
assert!(!out_new.exists());
|
assert!(!out_new.exists());
|
||||||
|
|
||||||
// nor clobber an existing file.
|
// Nor clobber an existing file.
|
||||||
let out_existing = dir.path().join("out_existing.bin");
|
let out_existing = dir.path().join("out_existing.bin");
|
||||||
|
|
||||||
fs::write(&out_existing, b"precious").unwrap();
|
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");
|
assert_eq!(fs::read(&out_existing).unwrap(), b"precious");
|
||||||
|
|
||||||
// No temporary files are left behind.
|
// No temporary files are left behind.
|
||||||
@@ -278,23 +382,31 @@ mod tests {
|
|||||||
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
|
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
|
||||||
.filter(|n| n.ends_with(".tmp"))
|
.filter(|n| n.ends_with(".tmp"))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
assert!(leftovers.is_empty(), "leftover temp files: {leftovers:?}");
|
assert!(leftovers.is_empty(), "leftover temp files: {leftovers:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_file_roundtrip() {
|
fn empty_file_roundtrip() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
let plain = dir.path().join("empty");
|
let plain = dir.path().join("empty");
|
||||||
|
|
||||||
let encrypted = dir.path().join("empty.kaef");
|
let encrypted = dir.path().join("empty.kaef");
|
||||||
|
|
||||||
let decrypted = dir.path().join("empty.out");
|
let decrypted = dir.path().join("empty.out");
|
||||||
|
|
||||||
fs::write(&plain, b"").unwrap();
|
fs::write(&plain, b"").unwrap();
|
||||||
|
|
||||||
encrypt_file(&plain, &encrypted, &KEY).unwrap();
|
encrypt_file(&plain, &encrypted, PASSWORD).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fs::metadata(&encrypted).unwrap().len() as usize,
|
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"");
|
assert_eq!(fs::read(&decrypted).unwrap(), b"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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,
|
//! The password is encoded as UTF-8 and combined with a random,
|
||||||
//! work factor, memory hardness, or iteration count.
|
//! 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`.
|
/// Length of the key produced for `KeccakAead`.
|
||||||
pub const DERIVED_KEY_LEN: usize = 64;
|
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
|
/// RFC 9106 recommends a 16-byte salt.
|
||||||
/// SHA3-512 implementation.
|
pub const SALT_LEN: usize = 16;
|
||||||
pub fn derive_file_key(password: &str) -> [u8; DERIVED_KEY_LEN] {
|
|
||||||
let digest = sha3_512(password.as_bytes());
|
|
||||||
|
|
||||||
let mut key = [0u8; DERIVED_KEY_LEN];
|
/// Default Argon2id memory cost in KiB.
|
||||||
key.copy_from_slice(&digest[..DERIVED_KEY_LEN]);
|
///
|
||||||
|
/// 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
@@ -1,6 +1,12 @@
|
|||||||
|
mod argon2;
|
||||||
|
mod compress;
|
||||||
|
mod hash;
|
||||||
mod kdf;
|
mod kdf;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod 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
@@ -1,26 +1,309 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::kdf::kdf::DERIVED_KEY_LEN;
|
|
||||||
|
|
||||||
#[test]
|
fn hex(bytes: &[u8]) -> String {
|
||||||
fn key_has_expected_length() {
|
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||||
assert_eq!(DERIVED_KEY_LEN, 64);
|
}
|
||||||
|
|
||||||
let key = derive_file_key("test password");
|
fn salt() -> [u8; SALT_LEN] {
|
||||||
assert_eq!(key.len(), DERIVED_KEY_LEN);
|
*b"0123456789abcdef"
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn same_password_same_key() {
|
fn argon2id_known_vector() {
|
||||||
let a = derive_file_key("correct horse battery staple");
|
/*
|
||||||
let b = derive_file_key("correct horse battery staple");
|
* 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]
|
#[test]
|
||||||
fn different_passwords_produce_different_keys() {
|
fn different_passwords_produce_different_keys() {
|
||||||
let a = derive_file_key("password1");
|
let salt = salt();
|
||||||
let b = derive_file_key("password2");
|
|
||||||
|
|
||||||
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
@@ -10,7 +10,6 @@ mod keccak;
|
|||||||
mod keccak_aead;
|
mod keccak_aead;
|
||||||
|
|
||||||
use file_crypt::{decrypt_file, encrypt_file};
|
use file_crypt::{decrypt_file, encrypt_file};
|
||||||
use kdf::derive_file_key;
|
|
||||||
|
|
||||||
/// Encrypt and decrypt files using KeccakAead.
|
/// Encrypt and decrypt files using KeccakAead.
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -66,9 +65,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
output,
|
output,
|
||||||
password,
|
password,
|
||||||
} => {
|
} => {
|
||||||
let key = derive_file_key(&password);
|
encrypt_file(&input, &output, &password)?;
|
||||||
|
|
||||||
encrypt_file(&input, &output, &key)?;
|
|
||||||
|
|
||||||
println!("Encrypted '{}' -> '{}'", input.display(), output.display());
|
println!("Encrypted '{}' -> '{}'", input.display(), output.display());
|
||||||
}
|
}
|
||||||
@@ -78,9 +75,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
output,
|
output,
|
||||||
password,
|
password,
|
||||||
} => {
|
} => {
|
||||||
let key = derive_file_key(&password);
|
decrypt_file(&input, &output, &password)?;
|
||||||
|
|
||||||
decrypt_file(&input, &output, &key)?;
|
|
||||||
|
|
||||||
println!("Decrypted '{}' -> '{}'", input.display(), output.display());
|
println!("Decrypted '{}' -> '{}'", input.display(), output.display());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user