diff --git a/Cargo.lock b/Cargo.lock index 284bfd2ec..ee876fd36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1018,6 +1018,14 @@ dependencies = [ [[package]] name = "pkcs12" version = "0.0.0" +dependencies = [ + "cfg-if", + "digest", + "hex-literal", + "sha2", + "whirlpool", + "zeroize", +] [[package]] name = "pkcs5" @@ -1853,6 +1861,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "whirlpool" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1ae50671d985c15b3214c7d969b8b520759fb3c8682444bec15ef775335a05c" +dependencies = [ + "digest", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/pkcs12/Cargo.toml b/pkcs12/Cargo.toml index 379fbde59..937fcf674 100644 --- a/pkcs12/Cargo.toml +++ b/pkcs12/Cargo.toml @@ -14,6 +14,20 @@ readme = "README.md" edition = "2021" rust-version = "1.65" +[dependencies] +cfg-if = "1.0.0" +digest = { version = "0.10.7", features=["alloc"], optional = true } +zeroize = "1.6.0" + +[dev-dependencies] +hex-literal = "0.4" +sha2 = "0.10.7" +whirlpool = "0.10.4" + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] + +[features] +kdf = ["dep:digest"] + diff --git a/pkcs12/src/kdf.rs b/pkcs12/src/kdf.rs new file mode 100644 index 000000000..e411324f6 --- /dev/null +++ b/pkcs12/src/kdf.rs @@ -0,0 +1,164 @@ +//! Implementation of the key derivation function +//! [RFC 7292 Appendix B](https://datatracker.ietf.org/doc/html/rfc7292#appendix-B) + +use alloc::{vec, vec::Vec}; +use digest::{core_api::BlockSizeUser, Digest, FixedOutputReset, OutputSizeUser, Update}; +use zeroize::{Zeroize, Zeroizing}; + +/// Transform a utf-8 string in a unicode (utf16) string as binary array. +/// The Utf16 code points are stored in big endian format with two trailing zero bytes. +fn str_to_unicode(utf8_str: &str) -> Vec { + let mut utf16_bytes = Vec::with_capacity(utf8_str.len() * 2 + 2); + for code_point in utf8_str.encode_utf16().chain(Some(0)) { + utf16_bytes.extend(code_point.to_be_bytes()); + } + utf16_bytes +} + +/// Specify the usage type of the generated key +/// This allows to derive distinct encryption keys, IVs and MAC from the same password or text +/// string. +pub enum Pkcs12KeyType { + /// Use key for encryption + EncryptionKey = 1, + /// Use key as initial vector + Iv = 2, + /// Use key as MAC + Mac = 3, +} + +/// Derives `key` of type `id` from `pass` and `salt` with length `key_len` using `rounds` +/// iterations of the algorithm +/// `pass` must be a utf8 string. +/// ```rust +/// let key = pkcs12::kdf::derive_key_utf8::("top-secret", &[0x1, 0x2, 0x3, 0x4], +/// pkcs12::kdf::Pkcs12KeyType::EncryptionKey, 1000, 32); +/// ``` +pub fn derive_key_utf8( + pass: &str, + salt: &[u8], + id: Pkcs12KeyType, + rounds: i32, + key_len: usize, +) -> Vec +where + D: Digest + FixedOutputReset + BlockSizeUser, +{ + let pass_utf16 = Zeroizing::new(str_to_unicode(pass)); + derive_key::(&pass_utf16, salt, id, rounds, key_len) +} + +/// Derives `key` of type `id` from `pass` and `salt` with length `key_len` using `rounds` +/// iterations of the algorithm +/// `pass` must be a unicode (utf16) byte array in big endian order without order mark and with two +/// terminating zero bytes. +/// ```rust +/// let key = pkcs12::kdf::derive_key_utf8::("top-secret", &[0x1, 0x2, 0x3, 0x4], +/// pkcs12::kdf::Pkcs12KeyType::EncryptionKey, 1000, 32); +/// ``` +pub fn derive_key( + pass: &[u8], + salt: &[u8], + id: Pkcs12KeyType, + rounds: i32, + key_len: usize, +) -> Vec +where + D: Digest + FixedOutputReset + BlockSizeUser, +{ + let mut digest = D::new(); + let output_size = ::output_size(); + let block_size = D::block_size(); + + // In the following, the numbered comments relate directly to the algorithm + // described in RFC 7292, Appendix B.2. Actual variable names may differ. + // Comments of the RFC are in enclosed in [] + // + // 1. Construct a string, D (the "diversifier"), by concatenating v/8 + // copies of ID, where v is the block size in bits. + let id_block = match id { + Pkcs12KeyType::EncryptionKey => vec![1u8; block_size], + Pkcs12KeyType::Iv => vec![2u8; block_size], + Pkcs12KeyType::Mac => vec![3u8; block_size], + }; + + let slen = block_size * ((salt.len() + block_size - 1) / block_size); + let plen = block_size * ((pass.len() + block_size - 1) / block_size); + let ilen = slen + plen; + let mut init_key = vec![0u8; ilen]; + // 2. Concatenate copies of the salt together to create a string S of + // length v(ceiling(s/v)) bits (the final copy of the salt may be + // truncated to create S). Note that if the salt is the empty + // string, then so is S. + for i in 0..slen { + init_key[i] = salt[i % salt.len()]; + } + + // 3. Concatenate copies of the password together to create a string P + // of length v(ceiling(p/v)) bits (the final copy of the password + // may be truncated to create P). Note that if the password is the + // empty string, then so is P. + for i in 0..plen { + init_key[slen + i] = pass[i % pass.len()]; + } + + // 4. Set I=S||P to be the concatenation of S and P. + // [already done in `init_key`] + + let mut m = key_len; + let mut n = 0; + let mut out = vec![0u8; key_len]; + // 5. Set c=ceiling(n/u) + // 6. For i=1, 2, ..., c, do the following: + // [ Instead of following this approach, we use an infinite loop and + // use the break condition below, if we have produced n bytes for the key] + loop { + // 6. A. Set A2=H^r(D||I). (i.e., the r-th hash of D||1, + // H(H(H(... H(D||I)))) + ::update(&mut digest, &id_block); + ::update(&mut digest, &init_key); + let mut result = digest.finalize_fixed_reset(); + for _ in 1..rounds { + ::update(&mut digest, &result[0..output_size]); + result = digest.finalize_fixed_reset(); + } + + // 7. Concateate A_1, A_2, ..., A_c together to form a pseudorandom + // bit string, A. + // [ Instead of storing all Ais and concatenating later, we concatenate + // them immediately ] + let new_bytes_num = m.min(output_size); + out[n..n + new_bytes_num].copy_from_slice(&result[0..new_bytes_num]); + n += new_bytes_num; + if m <= new_bytes_num { + break; + } + m -= new_bytes_num; + + // 6. B. Concatenate copies of Ai to create a string B of length v + // bits (the final copy of Ai may be truncated to create B). + // [ we achieve this on thy fly with the expression `result[k % output_size]` below] + + // 6. C. Treating I as a concatenation I_0, I_1, ..., I_(k-1) of v-bit + // blocks, where k=ceiling(s/v)+ceiling(p/v), modify I by + // setting I_j=(I_j+B+1) mod 2^v for each j. + let mut j = 0; + while j < ilen { + let mut c = 1_u16; + let mut k = block_size - 1; + loop { + c += init_key[k + j] as u16 + result[k % output_size] as u16; + init_key[j + k] = (c & 0x00ff) as u8; + c >>= 8; + if k == 0 { + break; + } + k -= 1; + } + j += block_size; + } + } + init_key.zeroize(); + // 8. Use the first n bits of A as the output of this entire process. + out +} diff --git a/pkcs12/src/lib.rs b/pkcs12/src/lib.rs index 7d36dd6f3..29abfabb4 100644 --- a/pkcs12/src/lib.rs +++ b/pkcs12/src/lib.rs @@ -14,4 +14,10 @@ unused_qualifications )] -//! TODO: PKCS#12 crate +//! TODO: complete PKCS#12 crate + +#[cfg(feature = "kdf")] +extern crate alloc; + +#[cfg(feature = "kdf")] +pub mod kdf; diff --git a/pkcs12/tests/kdf.rs b/pkcs12/tests/kdf.rs new file mode 100644 index 000000000..2b40efeb4 --- /dev/null +++ b/pkcs12/tests/kdf.rs @@ -0,0 +1,163 @@ +#![cfg(feature = "kdf")] +/// Test cases for the key derivation functions. +/// All test cases have been verified against openssl's method `PKCS12_key_gen_utf8`. +/// See https://github.com/xemwebe/test_pkcs12_kdf for a sample program. +/// +use hex_literal::hex; +use pkcs12::kdf::{derive_key_utf8, Pkcs12KeyType}; + +#[test] +fn pkcs12_key_derive_sha256() { + const PASS_SHORT: &str = "ge@äheim"; + const SALT_INC: [u8; 8] = [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]; + + assert_eq!( + derive_key_utf8::( + PASS_SHORT, + &SALT_INC, + Pkcs12KeyType::EncryptionKey, + 100, + 32 + ), + hex!("fae4d4957a3cc781e1180b9d4fb79c1e0c8579b746a3177e5b0768a3118bf863") + ); + + assert_eq!( + derive_key_utf8::(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Iv, 100, 32), + hex!("e5ff813bc6547de5155b14d2fada85b3201a977349db6e26ccc998d9e8f83d6c") + ); + + assert_eq!( + derive_key_utf8::(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Mac, 100, 32), + hex!("136355ed9434516682534f46d63956db5ff06b844702c2c1f3b46321e2524a4d") + ); + + assert_eq!( + derive_key_utf8::( + PASS_SHORT, + &SALT_INC, + Pkcs12KeyType::EncryptionKey, + 100, + 20 + ), + hex!("fae4d4957a3cc781e1180b9d4fb79c1e0c8579b7") + ); + + assert_eq!( + derive_key_utf8::(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Iv, 100, 20), + hex!("e5ff813bc6547de5155b14d2fada85b3201a9773") + ); + + assert_eq!( + derive_key_utf8::(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Mac, 100, 20), + hex!("136355ed9434516682534f46d63956db5ff06b84") + ); + + assert_eq!( + derive_key_utf8::( + PASS_SHORT, + &SALT_INC, + Pkcs12KeyType::EncryptionKey, + 100, + 12 + ), + hex!("fae4d4957a3cc781e1180b9d") + ); + + assert_eq!( + derive_key_utf8::(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Iv, 100, 12), + hex!("e5ff813bc6547de5155b14d2") + ); + + assert_eq!( + derive_key_utf8::(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Mac, 100, 12), + hex!("136355ed9434516682534f46") + ); + + assert_eq!( + derive_key_utf8::( + PASS_SHORT, + &SALT_INC, + Pkcs12KeyType::EncryptionKey, + 1000, + 32 + ), + hex!("2b95a0569b63f641fae1efca32e84db3699ab74540628ba66283b58cf5400527") + ); + + assert_eq!( + derive_key_utf8::(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Iv, 1000, 32), + hex!("6472c0ebad3fab4123e8b5ed7834de21eeb20187b3eff78a7d1cdffa4034851d") + ); + + assert_eq!( + derive_key_utf8::(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Mac, 1000, 32), + hex!("3f9113f05c30a996c4a516409bdac9d065f44296ccd52bb75de3fcfdbe2bf130") + ); + + assert_eq!( + derive_key_utf8::(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Mac, 1000, 32), + hex!("3f9113f05c30a996c4a516409bdac9d065f44296ccd52bb75de3fcfdbe2bf130") + ); + + assert_eq!( + derive_key_utf8::(PASS_SHORT, &SALT_INC, Pkcs12KeyType::EncryptionKey, 1000, 100), + hex!("2b95a0569b63f641fae1efca32e84db3699ab74540628ba66283b58cf5400527d8d0ebe2ccbf768c51c4d8fbd1bb156be06c1c59cbb69e44052ffc37376fdb47b2de7f9e543de9d096d8e5474b220410ff1c5d8bb7e5bc0f61baeaa12fd0da1d7a970172") + ); + + assert_eq!( + derive_key_utf8::(PASS_SHORT, &SALT_INC, Pkcs12KeyType::EncryptionKey, 1000, 200), + hex!("2b95a0569b63f641fae1efca32e84db3699ab74540628ba66283b58cf5400527d8d0ebe2ccbf768c51c4d8fbd1bb156be06c1c59cbb69e44052ffc37376fdb47b2de7f9e543de9d096d8e5474b220410ff1c5d8bb7e5bc0f61baeaa12fd0da1d7a9701729cea6014d7fe62a2ed926dc36b61307f119d64edbceb5a9c58133bbf75ba0bef000a1a5180e4b1de7d89c89528bcb7899a1e46fd4da0d9de8f8e65e8d0d775e33d1247e76d596a34303161b219f39afda448bf518a2835fc5e28f0b55a1b6137a2c70cf7") + ); +} + +#[test] +fn pkcs12_key_derive_sha512() { + const PASS_SHORT: &str = "ge@äheim"; + const SALT_INC: [u8; 8] = [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]; + + assert_eq!( + derive_key_utf8::( + PASS_SHORT, + &SALT_INC, + Pkcs12KeyType::EncryptionKey, + 100, + 32 + ), + hex!("b14a9f01bfd9dce4c9d66d2fe9937e5fd9f1afa59e370a6fa4fc81c1cc8ec8ee") + ); +} + +#[test] +fn pkcs12_key_derive_whirlpool() { + const PASS_SHORT: &str = "ge@äheim"; + const SALT_INC: [u8; 8] = [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]; + + assert_eq!( + derive_key_utf8::( + PASS_SHORT, + &SALT_INC, + Pkcs12KeyType::EncryptionKey, + 100, + 32 + ), + hex!("3324282adb468bff0734d3b7e399094ec8500cb5b0a3604055da107577aaf766") + ); +} + +#[test] +fn pkcs12_key_derive_special_chars() { + const PASS_SHORT: &str = "🔥"; + const SALT_INC: [u8; 8] = [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]; + + assert_eq!( + derive_key_utf8::( + PASS_SHORT, + &SALT_INC, + Pkcs12KeyType::EncryptionKey, + 100, + 32 + ), + hex!("d01e72a940b4b1a7a5707fc8264a60cb7606ff9051dedff90930687d2513c006") + ); +}