HMAC SHA3 + authenticate file header with HMAC
This commit is contained in:
1 parent
b30b5d0add
commit
5e95b21fbd
6 files changed
+333
-78
No files matched your search
+101
-36
@@ -1,43 +1,50 @@
|
||||
use std::io;
|
||||
|
||||
use crate::kdf::{DERIVED_KEY_LEN, SALT_LEN};
|
||||
use crate::keccak::hmac_sha3_256;
|
||||
|
||||
use super::error::FileCryptError;
|
||||
|
||||
const MAGIC: [u8; 4] = *b"KAEF";
|
||||
const VERSION: u8 = 2;
|
||||
|
||||
pub const IV_LEN: usize = 16;
|
||||
pub const NONCE_LEN: usize = 16;
|
||||
|
||||
pub const MIN_KEY_LEN: usize = 16;
|
||||
pub const MAX_KEY_LEN: usize = DERIVED_KEY_LEN;
|
||||
|
||||
pub const MAX_KEY_LEN: usize = 64;
|
||||
pub const HEADER_AUTH_LEN: usize = 16;
|
||||
pub const HEADER_AUTH_OFFSET: usize = 64;
|
||||
pub const HEADER_LEN: usize = HEADER_AUTH_OFFSET + HEADER_AUTH_LEN;
|
||||
|
||||
pub const HEADER_LEN: usize = 64;
|
||||
|
||||
const MAGIC: [u8; 4] = *b"KAEF";
|
||||
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,
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Header {
|
||||
pub(crate) key_len: usize,
|
||||
pub(crate) iv: [u8; IV_LEN],
|
||||
pub(crate) nonce: [u8; NONCE_LEN],
|
||||
pub(crate) salt: [u8; SALT_LEN],
|
||||
pub(crate) 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];
|
||||
/// Create a new v2 header with a fixed-size Argon2-derived key.
|
||||
pub fn new(plaintext_len: u64) -> Result<Self, FileCryptError> {
|
||||
let mut iv = [0u8; IV_LEN];
|
||||
let mut nonce = [0u8; NONCE_LEN];
|
||||
let mut salt = [0u8; SALT_LEN];
|
||||
|
||||
getrandom::fill(&mut random).map_err(FileCryptError::Random)?;
|
||||
getrandom::fill(&mut iv).map_err(|e| {
|
||||
FileCryptError::Io(io::Error::other(format!("failed to generate IV: {e}")))
|
||||
})?;
|
||||
|
||||
let iv = random[0..IV_LEN].try_into().expect("IV length is constant");
|
||||
getrandom::fill(&mut nonce).map_err(|e| {
|
||||
FileCryptError::Io(io::Error::other(format!("failed to generate nonce: {e}")))
|
||||
})?;
|
||||
|
||||
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");
|
||||
getrandom::fill(&mut salt).map_err(|e| {
|
||||
FileCryptError::Io(io::Error::other(format!("failed to generate salt: {e}")))
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
key_len: DERIVED_KEY_LEN,
|
||||
@@ -48,46 +55,104 @@ impl Header {
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn to_bytes(&self) -> [u8; HEADER_LEN] {
|
||||
/// Serialize the header.
|
||||
///
|
||||
/// The authentication field at `64..80` is deliberately left zeroed.
|
||||
/// `compute_header_auth()` computes the HMAC over this representation,
|
||||
/// then the caller inserts the resulting authenticator.
|
||||
pub fn to_bytes(&self) -> [u8; HEADER_LEN] {
|
||||
let mut out = [0u8; HEADER_LEN];
|
||||
|
||||
out[0..4].copy_from_slice(&MAGIC);
|
||||
out[4] = VERSION;
|
||||
out[5] = self.key_len as u8;
|
||||
|
||||
// out[6..8] reserved, zero.
|
||||
// 6..8: reserved, must remain zero.
|
||||
|
||||
out[8..24].copy_from_slice(&self.iv);
|
||||
out[24..40].copy_from_slice(&self.nonce);
|
||||
out[40..56].copy_from_slice(&self.salt);
|
||||
out[56..64].copy_from_slice(&self.plaintext_len.to_le_bytes());
|
||||
|
||||
// 64..80: header authentication field.
|
||||
// Left as zero while computing the HMAC.
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
pub(super) fn from_bytes(bytes: &[u8; HEADER_LEN]) -> Result<Self, FileCryptError> {
|
||||
/// Parse and structurally validate a v2 header.
|
||||
///
|
||||
/// The HMAC field itself is not validated here. Authentication is
|
||||
/// performed after deriving the password-based key.
|
||||
pub fn from_bytes(bytes: &[u8; HEADER_LEN]) -> Result<Self, FileCryptError> {
|
||||
if bytes[0..4] != MAGIC {
|
||||
return Err(FileCryptError::BadMagic);
|
||||
}
|
||||
|
||||
if bytes[4] != VERSION {
|
||||
return Err(FileCryptError::UnsupportedVersion(bytes[4]));
|
||||
let version = bytes[4];
|
||||
|
||||
if version != VERSION {
|
||||
return Err(FileCryptError::UnsupportedVersion(version));
|
||||
}
|
||||
|
||||
let key_len = bytes[5] as usize;
|
||||
|
||||
if !(MIN_KEY_LEN..=MAX_KEY_LEN).contains(&key_len) || bytes[6..8] != [0, 0] {
|
||||
if !(MIN_KEY_LEN..=MAX_KEY_LEN).contains(&key_len) {
|
||||
return Err(FileCryptError::MalformedHeader);
|
||||
}
|
||||
|
||||
// Bytes 6..8 are reserved and must be zero.
|
||||
if bytes[6] != 0 || bytes[7] != 0 {
|
||||
return Err(FileCryptError::MalformedHeader);
|
||||
}
|
||||
|
||||
let iv: [u8; IV_LEN] = bytes[8..24]
|
||||
.try_into()
|
||||
.map_err(|_| FileCryptError::MalformedHeader)?;
|
||||
|
||||
let nonce: [u8; NONCE_LEN] = bytes[24..40]
|
||||
.try_into()
|
||||
.map_err(|_| FileCryptError::MalformedHeader)?;
|
||||
|
||||
let salt: [u8; SALT_LEN] = bytes[40..56]
|
||||
.try_into()
|
||||
.map_err(|_| FileCryptError::MalformedHeader)?;
|
||||
|
||||
let plaintext_len_bytes: [u8; 8] = bytes[56..64]
|
||||
.try_into()
|
||||
.map_err(|_| FileCryptError::MalformedHeader)?;
|
||||
|
||||
let plaintext_len = u64::from_le_bytes(plaintext_len_bytes);
|
||||
|
||||
// 64..80 is the stored header HMAC.
|
||||
// It must NOT be required to contain zeroes here.
|
||||
|
||||
Ok(Self {
|
||||
key_len,
|
||||
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"),
|
||||
),
|
||||
iv,
|
||||
nonce,
|
||||
salt,
|
||||
plaintext_len,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the truncated HMAC-SHA3-256 authenticator for a header.
|
||||
///
|
||||
/// The authentication field itself is replaced with zeroes before hashing.
|
||||
/// Only the first `HEADER_AUTH_LEN` bytes of the 32-byte HMAC are stored.
|
||||
pub fn compute_header_auth(
|
||||
key: &[u8; DERIVED_KEY_LEN],
|
||||
header: &[u8; HEADER_LEN],
|
||||
) -> [u8; HEADER_AUTH_LEN] {
|
||||
let mut authenticated_header = *header;
|
||||
|
||||
authenticated_header[HEADER_AUTH_OFFSET..HEADER_AUTH_OFFSET + HEADER_AUTH_LEN].fill(0);
|
||||
|
||||
let mac = hmac_sha3_256(key, &authenticated_header);
|
||||
|
||||
let mut auth = [0u8; HEADER_AUTH_LEN];
|
||||
auth.copy_from_slice(&mac[..HEADER_AUTH_LEN]);
|
||||
|
||||
auth
|
||||
}
|
||||
@@ -11,8 +11,8 @@ 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};
|
||||
use super::format::{HEADER_AUTH_LEN, HEADER_AUTH_OFFSET, Header, compute_header_auth};
|
||||
use super::{BUF_SIZE, HEADER_LEN};
|
||||
|
||||
#[cfg(test)]
|
||||
const TEST_ARGON2_PARAMS: Argon2Params = Argon2Params {
|
||||
@@ -74,6 +74,8 @@ fn at_eof<R: Read>(reader: &mut R) -> io::Result<bool> {
|
||||
/// 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.
|
||||
///
|
||||
/// A truncated HMAC-SHA3-256 authenticator is stored in the header. The HMAC covers the complete header except for the HMAC field itself.
|
||||
pub fn encrypt_stream<R: Read, W: Write>(
|
||||
mut reader: R,
|
||||
mut writer: W,
|
||||
@@ -82,16 +84,23 @@ pub fn encrypt_stream<R: Read, W: Write>(
|
||||
) -> Result<(), FileCryptError> {
|
||||
let header = Header::new(plaintext_len)?;
|
||||
|
||||
// Argon2id always produces DERIVED_KEY_LEN bytes.
|
||||
let key = derive_stream_key(password, &header.salt);
|
||||
|
||||
check_key(&key)?;
|
||||
// Header::to_bytes() leaves the authentication field zeroed.
|
||||
let mut header_bytes = header.to_bytes();
|
||||
|
||||
let header_bytes = header.to_bytes();
|
||||
// Authenticate the header before inserting the authenticator itself.
|
||||
let header_auth = compute_header_auth(&key, &header_bytes);
|
||||
|
||||
header_bytes[HEADER_AUTH_OFFSET..HEADER_AUTH_OFFSET + HEADER_AUTH_LEN]
|
||||
.copy_from_slice(&header_auth);
|
||||
|
||||
writer.write_all(&header_bytes)?;
|
||||
|
||||
let mut aead = KeccakAead::new(&header.iv, &key, &header.nonce);
|
||||
|
||||
// Authenticate the complete header, including its HMAC, as AEAD associated data.
|
||||
aead.associated_data_processing(&header_bytes);
|
||||
|
||||
let mut buf = vec![0u8; BUF_SIZE];
|
||||
@@ -127,9 +136,15 @@ 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.
|
||||
/// 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.
|
||||
/// The header HMAC is verified immediately after key derivation and
|
||||
/// BEFORE any ciphertext is read. Thus a wrong password can be rejected
|
||||
/// without processing a potentially very large encrypted file.
|
||||
///
|
||||
/// Plaintext is written before the final AEAD tag has been checked.
|
||||
/// 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,
|
||||
@@ -143,8 +158,7 @@ pub fn decrypt_stream<R: Read, W: Write>(
|
||||
|
||||
let key = derive_stream_key(password, &header.salt);
|
||||
|
||||
check_key(&key)?;
|
||||
|
||||
// The password KDF always produces a DERIVED_KEY_LEN-byte key.
|
||||
if header.key_len != key.len() {
|
||||
return Err(FileCryptError::KeyLengthMismatch {
|
||||
header: header.key_len,
|
||||
@@ -152,8 +166,25 @@ pub fn decrypt_stream<R: Read, W: Write>(
|
||||
});
|
||||
}
|
||||
|
||||
// Early authentication
|
||||
let expected_header_auth = compute_header_auth(&key, &header_bytes);
|
||||
|
||||
let stored_header_auth =
|
||||
&header_bytes[HEADER_AUTH_OFFSET..HEADER_AUTH_OFFSET + HEADER_AUTH_LEN];
|
||||
|
||||
let header_auth_matches: bool = expected_header_auth
|
||||
.as_slice()
|
||||
.ct_eq(stored_header_auth)
|
||||
.into();
|
||||
|
||||
if !header_auth_matches {
|
||||
return Err(FileCryptError::AuthenticationFailed);
|
||||
}
|
||||
|
||||
// At this point the password and all HMAC-authenticated header fields are valid. Only now do we start reading the ciphertext.
|
||||
let mut aead = KeccakAead::new(&header.iv, &key, &header.nonce);
|
||||
|
||||
// Authenticate the complete header, including the HMAC field, with AEAD.
|
||||
aead.associated_data_processing(&header_bytes);
|
||||
|
||||
let mut buf = vec![0u8; BUF_SIZE];
|
||||
|
||||
+82
-33
@@ -1,10 +1,12 @@
|
||||
use super::*;
|
||||
|
||||
use crate::file_crypt::format::{HEADER_AUTH_LEN, HEADER_AUTH_OFFSET};
|
||||
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;
|
||||
use std::io::{self, Cursor, Read};
|
||||
|
||||
const TEST_ARGON2_PARAMS: Argon2Params = Argon2Params {
|
||||
memory_kib: 32,
|
||||
@@ -88,6 +90,16 @@ mod tests {
|
||||
assert_eq!(encrypted.len(), HEADER_LEN + 100 + DERIVED_KEY_LEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_contains_authentication_tag() {
|
||||
let encrypted = enc(PASSWORD, b"hello");
|
||||
|
||||
let header_auth = &encrypted[HEADER_AUTH_OFFSET..HEADER_AUTH_OFFSET + HEADER_AUTH_LEN];
|
||||
|
||||
assert_eq!(header_auth.len(), HEADER_AUTH_LEN);
|
||||
assert_ne!(header_auth, &[0u8; HEADER_AUTH_LEN]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_one_shot_aead() {
|
||||
for n in boundary_sizes() {
|
||||
@@ -136,11 +148,8 @@ mod tests {
|
||||
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);
|
||||
@@ -154,21 +163,20 @@ mod tests {
|
||||
for pos in 0..encrypted.len() {
|
||||
for bit in [0x01u8, 0x80] {
|
||||
let mut tampered = encrypted.clone();
|
||||
|
||||
tampered[pos] ^= bit;
|
||||
|
||||
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 authentication because ciphertext/tag corruption
|
||||
* is detected by the AEAD.
|
||||
*/
|
||||
// Header structure errors are expected for some header
|
||||
// fields. Once ciphertext/tag processing is reached, any
|
||||
// modification must fail authentication.
|
||||
if pos >= HEADER_LEN {
|
||||
assert!(
|
||||
matches!(result, Err(FileCryptError::AuthenticationFailed)),
|
||||
"byte {pos}"
|
||||
"byte {pos}, bit {bit:#x}: {result:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -186,12 +194,11 @@ mod tests {
|
||||
|
||||
forged.extend_from_slice(&encrypted[HEADER_LEN + 20..]);
|
||||
|
||||
// Without modifying the authenticated length,
|
||||
// the file structure is inconsistent.
|
||||
assert!(dec(PASSWORD, &forged).is_err());
|
||||
assert!(matches!(
|
||||
dec(PASSWORD, &forged),
|
||||
Err(FileCryptError::TrailingData)
|
||||
));
|
||||
|
||||
// Changing the authenticated length must also fail because
|
||||
// the header is associated data.
|
||||
forged[56..64].copy_from_slice(&21u64.to_le_bytes());
|
||||
|
||||
assert!(matches!(
|
||||
@@ -241,14 +248,54 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_password_is_rejected_before_ciphertext_read() {
|
||||
struct HeaderOnlyReader {
|
||||
data: Vec<u8>,
|
||||
position: usize,
|
||||
}
|
||||
|
||||
impl Read for HeaderOnlyReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if self.position >= self.data.len() {
|
||||
return Err(io::Error::other(
|
||||
"ciphertext was read after header authentication",
|
||||
));
|
||||
}
|
||||
|
||||
let remaining = self.data.len() - self.position;
|
||||
let n = buf.len().min(remaining);
|
||||
|
||||
buf[..n].copy_from_slice(&self.data[self.position..self.position + n]);
|
||||
|
||||
self.position += n;
|
||||
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
let encrypted = enc(PASSWORD, &sample(BUF_SIZE * 10));
|
||||
|
||||
let reader = HeaderOnlyReader {
|
||||
data: encrypted[..HEADER_LEN].to_vec(),
|
||||
position: 0,
|
||||
};
|
||||
|
||||
let mut output = Vec::new();
|
||||
|
||||
let result = decrypt_stream(reader, &mut output, "wrong password");
|
||||
|
||||
assert!(matches!(result, Err(FileCryptError::AuthenticationFailed)));
|
||||
|
||||
assert!(output.is_empty());
|
||||
}
|
||||
|
||||
#[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!(
|
||||
@@ -298,9 +345,7 @@ mod tests {
|
||||
));
|
||||
|
||||
/*
|
||||
* Absurd declared length on a tiny file:
|
||||
* decryption must fail without trying to allocate
|
||||
* based on that value.
|
||||
* The plaintext length field is HMAC-authenticated, so changing it causes AuthenticationFailed before ciphertext processing.
|
||||
*/
|
||||
let mut huge = enc(PASSWORD, b"x");
|
||||
|
||||
@@ -308,7 +353,7 @@ mod tests {
|
||||
|
||||
assert!(matches!(
|
||||
dec(PASSWORD, &huge),
|
||||
Err(FileCryptError::Truncated)
|
||||
Err(FileCryptError::AuthenticationFailed)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -318,7 +363,6 @@ mod tests {
|
||||
|
||||
let mut tampered = encrypted.clone();
|
||||
|
||||
// Change one bit of the salt.
|
||||
tampered[40] ^= 1;
|
||||
|
||||
assert!(matches!(
|
||||
@@ -327,14 +371,26 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_field_tampering_is_rejected() {
|
||||
let encrypted = enc(PASSWORD, b"hello");
|
||||
|
||||
let mut tampered = encrypted.clone();
|
||||
|
||||
tampered[HEADER_AUTH_OFFSET] ^= 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);
|
||||
@@ -347,7 +403,6 @@ mod tests {
|
||||
|
||||
assert_eq!(fs::read(&decrypted).unwrap(), data);
|
||||
|
||||
// Corrupt one ciphertext byte.
|
||||
let mut bytes = fs::read(&encrypted).unwrap();
|
||||
|
||||
bytes[HEADER_LEN + 1000] ^= 1;
|
||||
@@ -356,27 +411,23 @@ mod tests {
|
||||
|
||||
fs::write(&corrupted, &bytes).unwrap();
|
||||
|
||||
// A failed decryption must not create
|
||||
// the output.
|
||||
let out_new = dir.path().join("out_new.bin");
|
||||
|
||||
assert!(matches!(
|
||||
decrypt_file(&corrupted, &out_new, PASSWORD,),
|
||||
decrypt_file(&corrupted, &out_new, PASSWORD),
|
||||
Err(FileCryptError::AuthenticationFailed)
|
||||
));
|
||||
|
||||
assert!(!out_new.exists());
|
||||
|
||||
// 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, PASSWORD,).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.
|
||||
let leftovers: Vec<_> = fs::read_dir(dir.path())
|
||||
.unwrap()
|
||||
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
|
||||
@@ -391,9 +442,7 @@ mod tests {
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//! HMAC-SHA3-256 implementation.
|
||||
//!
|
||||
//! HMAC construction:
|
||||
//! H(K, m) = H((K' xor opad) || H((K' xor ipad) || m))
|
||||
//!
|
||||
//! SHA3-256 has a 136-byte rate, which is the block size used here.
|
||||
|
||||
use super::sha3_256;
|
||||
|
||||
/// SHA3-256 HMAC block size in bytes.
|
||||
///
|
||||
/// SHA3-256 has a rate of 1088 bits = 136 bytes.
|
||||
pub const HMAC_SHA3_256_BLOCK_SIZE: usize = 136;
|
||||
|
||||
/// SHA3-256 HMAC output size in bytes.
|
||||
pub const HMAC_SHA3_256_OUTPUT_SIZE: usize = 32;
|
||||
|
||||
/// HMAC-SHA3-256.
|
||||
///
|
||||
/// The key is normalized according to the HMAC construction:
|
||||
/// - keys longer than the hash block size are hashed first;
|
||||
/// - shorter keys are zero-padded to the block size.
|
||||
pub fn hmac_sha3_256(key: &[u8], message: &[u8]) -> [u8; HMAC_SHA3_256_OUTPUT_SIZE] {
|
||||
let mut key_block = [0u8; HMAC_SHA3_256_BLOCK_SIZE];
|
||||
|
||||
if key.len() > HMAC_SHA3_256_BLOCK_SIZE {
|
||||
let hashed_key = sha3_256(key);
|
||||
key_block[..hashed_key.len()].copy_from_slice(&hashed_key);
|
||||
} else {
|
||||
key_block[..key.len()].copy_from_slice(key);
|
||||
}
|
||||
|
||||
let mut ipad = [0u8; HMAC_SHA3_256_BLOCK_SIZE];
|
||||
let mut opad = [0u8; HMAC_SHA3_256_BLOCK_SIZE];
|
||||
|
||||
for ((ipad_byte, opad_byte), &key_byte) in
|
||||
ipad.iter_mut().zip(opad.iter_mut()).zip(key_block.iter())
|
||||
{
|
||||
*ipad_byte = key_byte ^ 0x36;
|
||||
*opad_byte = key_byte ^ 0x5c;
|
||||
}
|
||||
|
||||
// inner = SHA3-256((K' xor ipad) || message)
|
||||
let mut inner_input = Vec::with_capacity(HMAC_SHA3_256_BLOCK_SIZE + message.len());
|
||||
inner_input.extend_from_slice(&ipad);
|
||||
inner_input.extend_from_slice(message);
|
||||
|
||||
let inner_hash = sha3_256(&inner_input);
|
||||
|
||||
// outer = SHA3-256((K' xor opad) || inner)
|
||||
let mut outer_input = Vec::with_capacity(HMAC_SHA3_256_BLOCK_SIZE + inner_hash.len());
|
||||
outer_input.extend_from_slice(&opad);
|
||||
outer_input.extend_from_slice(&inner_hash);
|
||||
|
||||
sha3_256(&outer_input)
|
||||
}
|
||||
+4
-1
@@ -1,10 +1,13 @@
|
||||
//! Keccak-f[1600], SHA-3, and SHAKE implementations.
|
||||
//! Keccak-f[1600], SHA-3, SHAKE, and HMAC implementations.
|
||||
|
||||
mod hmac;
|
||||
mod keccak;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
pub use hmac::{HMAC_SHA3_256_BLOCK_SIZE, HMAC_SHA3_256_OUTPUT_SIZE, hmac_sha3_256};
|
||||
|
||||
pub use keccak::{
|
||||
keccak, keccak_f1600, sha3_224, sha3_256, sha3_384, sha3_512, shake128, shake256,
|
||||
};
|
||||
@@ -222,3 +222,54 @@ d5a15bef186a5386c75744c0527e1faa\
|
||||
9f8726e462a12a4feb06bd8801e751e4",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_sha3_256_test_vectors() {
|
||||
let vectors = [
|
||||
(
|
||||
vec![0x0b; 20],
|
||||
b"Hi There".to_vec(),
|
||||
"ba85192310dffa96e2a3a40e69774351140bb7185e1202cdcc917589f95e16bb",
|
||||
),
|
||||
(
|
||||
b"Jefe".to_vec(),
|
||||
b"what do ya want for nothing?".to_vec(),
|
||||
"c7d4072e788877ae3596bbb0da73b887c9171f93095b294ae857fbe2645e1ba5",
|
||||
),
|
||||
(
|
||||
vec![0xaa; 20],
|
||||
vec![0xdd; 50],
|
||||
"84ec79124a27107865cedd8bd82da9965e5ed8c37b0ac98005a7f39ed58a4207",
|
||||
),
|
||||
(
|
||||
(1u8..=25).collect(),
|
||||
vec![0xcd; 50],
|
||||
"57366a45e2305321a4bc5aa5fe2ef8a921f6af8273d7fe7be6cfedb3f0aea6d7",
|
||||
),
|
||||
];
|
||||
|
||||
for (key, message, expected) in vectors {
|
||||
let actual = hmac_sha3_256(&key, &message);
|
||||
assert_hex(&actual, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_sha3_256_empty_key_and_message() {
|
||||
let actual = hmac_sha3_256(b"", b"");
|
||||
|
||||
assert_hex(
|
||||
&actual,
|
||||
"e841c164e5b4f10c9f3985587962af72fd607a951196fc92fb3a5251941784ea",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_key_is_hashed() {
|
||||
let key = vec![0x42; HMAC_SHA3_256_BLOCK_SIZE + 1];
|
||||
let message = b"test message";
|
||||
|
||||
let actual = hmac_sha3_256(&key, message);
|
||||
|
||||
assert_eq!(actual.len(), HMAC_SHA3_256_OUTPUT_SIZE);
|
||||
}
|
||||
Reference in new issue
Block a user