use aes::{Aes128, Aes192, Aes256};
use cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
use ctr::Ctr128BE;
#[cfg(feature = "kms")]
use crate::proto::hdfs::{CipherSuiteProto, FileEncryptionInfoProto};
#[cfg(feature = "kms")]
use crate::{HdfsError, Result};
type Aes128Ctr = Ctr128BE<Aes128>;
type Aes192Ctr = Ctr128BE<Aes192>;
type Aes256Ctr = Ctr128BE<Aes256>;
#[cfg(feature = "kms")]
#[derive(Debug, Clone)]
pub(crate) struct DataEncryptionKey {
pub material: Vec<u8>,
pub iv: Vec<u8>,
}
#[derive(Debug, Clone)]
pub(crate) struct FileCryptoCodec {
key: Vec<u8>,
iv: [u8; 16],
}
impl FileCryptoCodec {
#[cfg(feature = "kms")]
pub(crate) fn new(info: &FileEncryptionInfoProto, dek: DataEncryptionKey) -> Result<Self> {
match CipherSuiteProto::try_from(info.suite) {
Ok(CipherSuiteProto::AesCtrNopadding) => {}
Ok(CipherSuiteProto::Sm4CtrNopadding) => {
return Err(HdfsError::UnsupportedFeature(
"SM4 cipher suite for HDFS encryption".to_string(),
));
}
_ => {
return Err(HdfsError::OperationFailed(format!(
"Unknown HDFS cipher suite {}",
info.suite
)));
}
}
if !matches!(dek.material.len(), 16 | 24 | 32) {
return Err(HdfsError::OperationFailed(format!(
"KMS returned DEK of unexpected length {}",
dek.material.len()
)));
}
if dek.iv.len() != 16 {
return Err(HdfsError::OperationFailed(format!(
"KMS returned IV of unexpected length {}",
dek.iv.len()
)));
}
let mut iv = [0u8; 16];
iv.copy_from_slice(&dek.iv);
Ok(Self {
key: dek.material,
iv,
})
}
pub(crate) fn make_cipher(&self) -> FileCipher {
match self.key.len() {
16 => FileCipher::Aes128(new_ctr(&self.key, &self.iv)),
24 => FileCipher::Aes192(new_ctr(&self.key, &self.iv)),
32 => FileCipher::Aes256(new_ctr(&self.key, &self.iv)),
_ => unreachable!("validated in new"),
}
}
}
fn new_ctr<C: KeyIvInit>(key: &[u8], iv: &[u8]) -> C {
C::new_from_slices(key, iv).expect("validated key and iv lengths")
}
pub(crate) enum FileCipher {
Aes128(Aes128Ctr),
Aes192(Aes192Ctr),
Aes256(Aes256Ctr),
}
impl FileCipher {
pub(crate) fn apply(&mut self, file_offset: u64, buf: &mut [u8]) {
match self {
FileCipher::Aes128(c) => apply_ctr(c, file_offset, buf),
FileCipher::Aes192(c) => apply_ctr(c, file_offset, buf),
FileCipher::Aes256(c) => apply_ctr(c, file_offset, buf),
}
}
}
fn apply_ctr<C: StreamCipher + StreamCipherSeek>(cipher: &mut C, file_offset: u64, buf: &mut [u8]) {
cipher.seek(file_offset);
cipher.apply_keystream(buf);
}
#[cfg(all(test, feature = "kms"))]
mod tests {
use super::*;
fn dek(material: &[u8], iv: &[u8]) -> DataEncryptionKey {
DataEncryptionKey {
material: material.to_vec(),
iv: iv.to_vec(),
}
}
fn aes_ctr_info() -> FileEncryptionInfoProto {
FileEncryptionInfoProto {
suite: CipherSuiteProto::AesCtrNopadding as i32,
crypto_protocol_version: 2,
key: vec![],
iv: vec![],
key_name: String::new(),
ez_key_version_name: String::new(),
}
}
#[test]
fn round_trip_at_offset_zero() {
let mut cipher = FileCryptoCodec::new(&aes_ctr_info(), dek(&[0x11; 16], &[0x22; 16]))
.unwrap()
.make_cipher();
let plaintext = b"the quick brown fox jumps over the lazy dog".to_vec();
let mut buf = plaintext.clone();
cipher.apply(0, &mut buf);
assert_ne!(buf, plaintext);
cipher.apply(0, &mut buf);
assert_eq!(buf, plaintext);
}
#[test]
fn decrypts_partial_range_independently() {
let mut cipher = FileCryptoCodec::new(&aes_ctr_info(), dek(&[0x42; 32], &[0xAA; 16]))
.unwrap()
.make_cipher();
let plaintext: Vec<u8> = (0..512u32).map(|i| i as u8).collect();
let mut whole = plaintext.clone();
cipher.apply(0, &mut whole);
let start = 37usize;
let end = 401usize;
let mut slice = whole[start..end].to_vec();
cipher.apply(start as u64, &mut slice);
assert_eq!(slice, &plaintext[start..end]);
}
#[test]
fn matches_reference_aes128_ctr_vector() {
let key: [u8; 16] = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10,
];
let iv: [u8; 16] = [
0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99,
];
let plaintext = b"HDFS encryption test vector!";
let mut cipher = FileCryptoCodec::new(&aes_ctr_info(), dek(&key, &iv))
.unwrap()
.make_cipher();
let mut ours = plaintext.to_vec();
cipher.apply(0, &mut ours);
let mut reference = Aes128Ctr::new_from_slices(&key, &iv).expect("valid lengths");
let mut expected = plaintext.to_vec();
reference.apply_keystream(&mut expected);
assert_eq!(ours, expected);
assert_ne!(ours, plaintext);
}
#[test]
fn rejects_sm4_cipher_suite() {
let mut info = aes_ctr_info();
info.suite = CipherSuiteProto::Sm4CtrNopadding as i32;
let err = FileCryptoCodec::new(&info, dek(&[0; 16], &[0; 16])).unwrap_err();
assert!(matches!(err, HdfsError::UnsupportedFeature(_)));
}
#[test]
fn rejects_bad_key_or_iv_length() {
let info = aes_ctr_info();
assert!(FileCryptoCodec::new(&info, dek(&[0; 17], &[0; 16])).is_err());
assert!(FileCryptoCodec::new(&info, dek(&[0; 16], &[0; 15])).is_err());
}
#[test]
fn supports_aes192_and_aes256() {
for keylen in [16usize, 24, 32] {
let mut cipher =
FileCryptoCodec::new(&aes_ctr_info(), dek(&vec![0x33u8; keylen], &[0x77; 16]))
.unwrap()
.make_cipher();
let plaintext = vec![0u8; 100];
let mut buf = plaintext.clone();
cipher.apply(7, &mut buf); assert_ne!(buf, plaintext);
cipher.apply(7, &mut buf);
assert_eq!(buf, plaintext);
}
}
}