HMAC SHA3 + authenticate file header with HMAC

This commit is contained in:
sam.hadow committed 2026-10-02 18:11:33 +02:00
1 parent b30b5d0add
commit 5e95b21fbd
6 files changed
+333 -78

No files matched your search

+101 -36
View File
@@ -1,43 +1,50 @@
use std::io;
use crate::kdf::{DERIVED_KEY_LEN, SALT_LEN}; use crate::kdf::{DERIVED_KEY_LEN, SALT_LEN};
use crate::keccak::hmac_sha3_256;
use super::error::FileCryptError; use super::error::FileCryptError;
const MAGIC: [u8; 4] = *b"KAEF";
const VERSION: u8 = 2;
pub const IV_LEN: usize = 16; pub const IV_LEN: usize = 16;
pub const NONCE_LEN: usize = 16; pub const NONCE_LEN: usize = 16;
pub const MIN_KEY_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; #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Header {
const MAGIC: [u8; 4] = *b"KAEF"; pub(crate) key_len: usize,
const VERSION: u8 = 2; pub(crate) iv: [u8; IV_LEN],
pub(crate) nonce: [u8; NONCE_LEN],
pub(super) struct Header { pub(crate) salt: [u8; SALT_LEN],
pub(super) key_len: usize, pub(crate) plaintext_len: u64,
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 { impl Header {
/// Create a new header with a fresh random IV, nonce, and KDF salt. /// Create a new v2 header with a fixed-size Argon2-derived key.
pub(super) fn new(plaintext_len: u64) -> Result<Self, FileCryptError> { pub fn new(plaintext_len: u64) -> Result<Self, FileCryptError> {
let mut random = [0u8; IV_LEN + NONCE_LEN + SALT_LEN]; 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] getrandom::fill(&mut salt).map_err(|e| {
.try_into() FileCryptError::Io(io::Error::other(format!("failed to generate salt: {e}")))
.expect("nonce length is constant"); })?;
let salt = random[IV_LEN + NONCE_LEN..]
.try_into()
.expect("salt length is constant");
Ok(Self { Ok(Self {
key_len: DERIVED_KEY_LEN, 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]; let mut out = [0u8; HEADER_LEN];
out[0..4].copy_from_slice(&MAGIC); out[0..4].copy_from_slice(&MAGIC);
out[4] = VERSION; out[4] = VERSION;
out[5] = self.key_len as u8; 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[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..56].copy_from_slice(&self.salt); out[40..56].copy_from_slice(&self.salt);
out[56..64].copy_from_slice(&self.plaintext_len.to_le_bytes()); 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 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 { if bytes[0..4] != MAGIC {
return Err(FileCryptError::BadMagic); return Err(FileCryptError::BadMagic);
} }
if bytes[4] != VERSION { let version = bytes[4];
return Err(FileCryptError::UnsupportedVersion(bytes[4]));
if version != VERSION {
return Err(FileCryptError::UnsupportedVersion(version));
} }
let key_len = bytes[5] as usize; 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); 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 { Ok(Self {
key_len, key_len,
iv: bytes[8..24].try_into().expect("IV length is constant"), iv,
nonce: bytes[24..40].try_into().expect("nonce length is constant"), nonce,
salt: bytes[40..56].try_into().expect("salt length is constant"), salt,
plaintext_len: u64::from_le_bytes( plaintext_len,
bytes[56..64].try_into().expect("length is constant"),
),
}) })
} }
} }
/// 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
}
+39 -8
View File
@@ -11,8 +11,8 @@ 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_AUTH_LEN, HEADER_AUTH_OFFSET, Header, compute_header_auth};
use super::{BUF_SIZE, HEADER_LEN, check_key}; use super::{BUF_SIZE, HEADER_LEN};
#[cfg(test)] #[cfg(test)]
const TEST_ARGON2_PARAMS: Argon2Params = Argon2Params { 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. /// 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. /// 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>( pub fn encrypt_stream<R: Read, W: Write>(
mut reader: R, mut reader: R,
mut writer: W, mut writer: W,
@@ -82,16 +84,23 @@ pub fn encrypt_stream<R: Read, W: Write>(
) -> Result<(), FileCryptError> { ) -> Result<(), FileCryptError> {
let header = Header::new(plaintext_len)?; let header = Header::new(plaintext_len)?;
// Argon2id always produces DERIVED_KEY_LEN bytes.
let key = derive_stream_key(password, &header.salt); 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)?; 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);
// Authenticate the complete header, including its HMAC, as AEAD associated data.
aead.associated_data_processing(&header_bytes); aead.associated_data_processing(&header_bytes);
let mut buf = vec![0u8; BUF_SIZE]; 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. /// 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>( pub fn decrypt_stream<R: Read, W: Write>(
mut reader: R, mut reader: R,
mut writer: W, mut writer: W,
@@ -143,8 +158,7 @@ pub fn decrypt_stream<R: Read, W: Write>(
let key = derive_stream_key(password, &header.salt); 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() { if header.key_len != key.len() {
return Err(FileCryptError::KeyLengthMismatch { return Err(FileCryptError::KeyLengthMismatch {
header: header.key_len, 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); 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); aead.associated_data_processing(&header_bytes);
let mut buf = vec![0u8; BUF_SIZE]; let mut buf = vec![0u8; BUF_SIZE];
+82 -33
View File
@@ -1,10 +1,12 @@
use super::*; use super::*;
use crate::file_crypt::format::{HEADER_AUTH_LEN, HEADER_AUTH_OFFSET};
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::kdf::{Argon2Params, DERIVED_KEY_LEN, SALT_LEN, derive_file_key_with_params};
use crate::keccak_aead::KeccakAead; use crate::keccak_aead::KeccakAead;
use std::fs; use std::fs;
use std::io::Cursor; use std::io::{self, Cursor, Read};
const TEST_ARGON2_PARAMS: Argon2Params = Argon2Params { const TEST_ARGON2_PARAMS: Argon2Params = Argon2Params {
memory_kib: 32, memory_kib: 32,
@@ -88,6 +90,16 @@ mod tests {
assert_eq!(encrypted.len(), HEADER_LEN + 100 + DERIVED_KEY_LEN); 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] #[test]
fn matches_one_shot_aead() { fn matches_one_shot_aead() {
for n in boundary_sizes() { for n in boundary_sizes() {
@@ -136,11 +148,8 @@ mod tests {
let a = enc(PASSWORD, &plaintext); let a = enc(PASSWORD, &plaintext);
let b = enc(PASSWORD, &plaintext); let b = enc(PASSWORD, &plaintext);
// The salts must differ.
assert_ne!(a[40..56], b[40..56]); 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, &a).unwrap(), plaintext);
assert_eq!(dec(PASSWORD, &b).unwrap(), plaintext); assert_eq!(dec(PASSWORD, &b).unwrap(), plaintext);
@@ -154,21 +163,20 @@ mod tests {
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(PASSWORD, &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");
/* // Header structure errors are expected for some header
* Everything after the header must fail specifically // fields. Once ciphertext/tag processing is reached, any
* on authentication because ciphertext/tag corruption // modification must fail authentication.
* 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)),
"byte {pos}" "byte {pos}, bit {bit:#x}: {result:?}"
); );
} }
} }
@@ -186,12 +194,11 @@ mod tests {
forged.extend_from_slice(&encrypted[HEADER_LEN + 20..]); forged.extend_from_slice(&encrypted[HEADER_LEN + 20..]);
// Without modifying the authenticated length, assert!(matches!(
// the file structure is inconsistent. dec(PASSWORD, &forged),
assert!(dec(PASSWORD, &forged).is_err()); 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()); forged[56..64].copy_from_slice(&21u64.to_le_bytes());
assert!(matches!( 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] #[test]
fn malformed_key_length() { fn malformed_key_length() {
let encrypted = enc(PASSWORD, &sample(50)); let encrypted = enc(PASSWORD, &sample(50));
let mut malformed = encrypted.clone(); let mut malformed = encrypted.clone();
// 16 is structurally valid, but does not match
// the 64-byte key generated by Argon2id.
malformed[5] = 16; malformed[5] = 16;
assert!(matches!( assert!(matches!(
@@ -298,9 +345,7 @@ mod tests {
)); ));
/* /*
* Absurd declared length on a tiny file: * The plaintext length field is HMAC-authenticated, so changing it causes AuthenticationFailed before ciphertext processing.
* decryption must fail without trying to allocate
* based on that value.
*/ */
let mut huge = enc(PASSWORD, b"x"); let mut huge = enc(PASSWORD, b"x");
@@ -308,7 +353,7 @@ mod tests {
assert!(matches!( assert!(matches!(
dec(PASSWORD, &huge), dec(PASSWORD, &huge),
Err(FileCryptError::Truncated) Err(FileCryptError::AuthenticationFailed)
)); ));
} }
@@ -318,7 +363,6 @@ mod tests {
let mut tampered = encrypted.clone(); let mut tampered = encrypted.clone();
// Change one bit of the salt.
tampered[40] ^= 1; tampered[40] ^= 1;
assert!(matches!( 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] #[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);
@@ -347,7 +403,6 @@ mod tests {
assert_eq!(fs::read(&decrypted).unwrap(), data); assert_eq!(fs::read(&decrypted).unwrap(), data);
// 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;
@@ -356,27 +411,23 @@ mod tests {
fs::write(&corrupted, &bytes).unwrap(); fs::write(&corrupted, &bytes).unwrap();
// 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, PASSWORD,), 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.
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, PASSWORD,).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.
let leftovers: Vec<_> = fs::read_dir(dir.path()) let leftovers: Vec<_> = fs::read_dir(dir.path())
.unwrap() .unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned()) .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
@@ -391,9 +442,7 @@ mod tests {
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();
+56
View File
@@ -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
View File
@@ -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; mod keccak;
#[cfg(test)] #[cfg(test)]
mod test; mod test;
pub use hmac::{HMAC_SHA3_256_BLOCK_SIZE, HMAC_SHA3_256_OUTPUT_SIZE, hmac_sha3_256};
pub use keccak::{ pub use keccak::{
keccak, keccak_f1600, sha3_224, sha3_256, sha3_384, sha3_512, shake128, shake256, keccak, keccak_f1600, sha3_224, sha3_256, sha3_384, sha3_512, shake128, shake256,
}; };
+51
View File
@@ -222,3 +222,54 @@ d5a15bef186a5386c75744c0527e1faa\
9f8726e462a12a4feb06bd8801e751e4", 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);
}