I added some tests. Don’t know that they’re exhaustive or conventional to Rust but I feel like they do the job and solve the problems I can foresee - right this second.
Anyway, just kinda a cool reference for what testing in rust looks like (a thing I also haven’t done a ton of before, was afraid of, and possibly caused me to be a lot more cautious with the code I actually write, but also possible - and more likely - that that is irrational, especially since I’ve already caught a bunch of stuff with tests and benchmarks).
Like yesterday the benchmarks for this crate, are here: https://problemchild.engineering.com/benchmarks/crypto/2023-08-13/report.
crypto/Cargo.toml
[package]
name = "crypto"
version = "0.1.0"
edition = "2021"
[dependencies]
hex = "0.4.3"
sha2 = "0.10.7"
aes = "0.8.3"
chacha20poly1305 = "0.10.1"
rand_core = "0.6.4"
ed25519-dalek = { version = "2.0.0-rc.3", features = ["rand_core"] }
x25519-dalek = { version = "2.0.0-rc.3", features = ["static_secrets"] }
[dev-dependencies]
lazy_static = "1.4.0"
criterion = { version = "0.4", features = ["html_reports"] }
[[bench]]
name = "lib"
harness = false
crypto/src/lib.rs
use aes::{
cipher::{BlockDecrypt, BlockEncrypt},
Aes256,
};
use sha2::{Digest, Sha256};
use chacha20poly1305::aead::{
generic_array::GenericArray, Aead, AeadCore, KeyInit as ChaChaKeyInit,
};
use ed25519_dalek::{Signer, SigningKey, Verifier};
pub fn hash_to_32_bytes(val: String) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(val);
let result = hasher.finalize();
let out: [u8; 32] = result
.try_into()
.expect("failed to convert hashed value to fixed bytes");
out
}
pub fn hash_to_string(val: String) -> String {
hex::encode(hash_to_32_bytes(val))
}
pub fn decode_32_byte_key_from_string(val: String) -> [u8; 32] {
let decoded_val = hex::decode(val).expect("failed to decode val from");
let decoded_val_as_bytes: &[u8] = &decoded_val;
let decoded_val_as_fixed_bytes: [u8; 32] = decoded_val_as_bytes
.try_into()
.expect("failed to convert decoded val into fixed bytes");
decoded_val_as_fixed_bytes
}
pub fn encode_32_byte_key_to_string(val: [u8; 32]) -> String {
hex::encode(val)
}
#[cfg(test)]
mod hash_and_encoding_tests {
use super::*;
#[test]
fn test_hash() -> Result<(), String> {
let val = "testing hash and encoding".to_string();
let val2 = "testing hash and encoding".to_string();
let bytes_hash = hash_to_32_bytes(val.clone());
let bytes_hash2 = hash_to_32_bytes(val2.clone());
assert_eq!(bytes_hash, bytes_hash2);
let str_hash = hash_to_string(val.clone());
let str_hash2 = hash_to_string(val2.clone());
assert_eq!(str_hash, str_hash2);
let str_hash_as_bytes = decode_32_byte_key_from_string(str_hash);
assert_eq!(str_hash_as_bytes, bytes_hash);
Ok(())
}
#[test]
fn test_encoding() -> Result<(), String> {
let (priv_key, _) = generate_exchange_keys();
let priv_key_as_string = encode_32_byte_key_to_string(priv_key);
let priv_key_as_bytes = decode_32_byte_key_from_string(priv_key_as_string);
assert_eq!(priv_key, priv_key_as_bytes);
Ok(())
}
}
pub fn block_encrypt_key(key: [u8; 32], content: [u8; 32]) -> [u8; 32] {
let cipher = Aes256::new(GenericArray::from_slice(&key));
let block_one: [u8; 16] = content[0..16]
.try_into()
.expect("failed to convert block one to fixed bytes");
let block_two: [u8; 16] = content[16..32]
.try_into()
.expect("failed to convert block two to fixed bytes");
let block_one_as_generic_array = GenericArray::from(block_one);
let block_two_as_generic_array = GenericArray::from(block_two);
cipher.encrypt_blocks(&mut [block_one_as_generic_array, block_two_as_generic_array]);
let mut combined_array: [u8; 32] = [0u8; 32];
combined_array[..16].copy_from_slice(block_one_as_generic_array.as_slice());
combined_array[16..].copy_from_slice(block_two_as_generic_array.as_slice());
combined_array
}
pub fn block_decrypt_key(key: [u8; 32], encrypted_content: [u8; 32]) -> [u8; 32] {
let cipher = Aes256::new(GenericArray::from_slice(&key));
let block_one: [u8; 16] = encrypted_content[0..16]
.try_into()
.expect("failed to convert block one to fixed bytes");
let block_two: [u8; 16] = encrypted_content[16..32]
.try_into()
.expect("failed to convert block two to fixed bytes");
let block_one_as_generic_array = GenericArray::from(block_one);
let block_two_as_generic_array = GenericArray::from(block_two);
cipher.decrypt_blocks(&mut [block_one_as_generic_array, block_two_as_generic_array]);
let mut combined_array: [u8; 32] = [0; 32];
combined_array[..16].copy_from_slice(block_one_as_generic_array.as_slice());
combined_array[16..].copy_from_slice(block_two_as_generic_array.as_slice());
combined_array
}
#[cfg(test)]
mod block_encryption_tests {
use super::*;
#[test]
fn test_block_encryption() -> Result<(), String> {
let (priv_exchange_key, pub_exchange_key) = generate_exchange_keys();
let encrypted_key = block_encrypt_key(pub_exchange_key, priv_exchange_key);
let decrypted_key = block_decrypt_key(pub_exchange_key, encrypted_key);
assert_eq!(decrypted_key, priv_exchange_key);
Ok(())
}
}
pub fn generate_signing_keys() -> ([u8; 32], [u8; 32]) {
let mut rng = rand_core::OsRng;
let signing_key: SigningKey = SigningKey::generate(&mut rng);
(
signing_key.to_bytes(),
signing_key.verifying_key().to_bytes(),
)
}
pub fn sign_content(content: Vec<u8>, signing_key: [u8; 32]) -> [u8; 64] {
let signing_key = ed25519_dalek::SigningKey::from_bytes(&signing_key);
let signature = signing_key.sign(&content);
signature.to_bytes()
}
pub fn verify_signature(
signature: [u8; 64],
verifying_key: [u8; 32],
content: Vec<u8>,
) -> Result<(), String> {
let signature = ed25519_dalek::Signature::from_bytes(&signature);
let verifying_key = match ed25519_dalek::VerifyingKey::from_bytes(&verifying_key) {
Ok(vk) => vk,
Err(err) => {
return Err(format!(
"failed to generate verifying key from pub signing key: {}",
err.to_string()
))
}
};
match verifying_key.verify(&content, &signature) {
Ok(_) => Ok(()),
Err(err) => {
return Err(format!(
"failed to verify signature for content: {}",
err.to_string()
))
}
}
}
#[cfg(test)]
mod signing_and_verification_tests {
use super::*;
#[test]
fn test_signing_and_verification() -> Result<(), String> {
let (signing_key, verifying_key) = generate_signing_keys();
let content = vec![0u8; 1024];
let signature = sign_content(content.clone(), signing_key);
let signature_verified = verify_signature(signature, verifying_key, content).is_ok();
assert_eq!(signature_verified, true);
Ok(())
}
}
pub fn generate_exchange_keys() -> ([u8; 32], [u8; 32]) {
let priv_key = x25519_dalek::StaticSecret::random_from_rng(rand_core::OsRng);
let pub_key = x25519_dalek::PublicKey::from(&priv_key);
(*priv_key.as_bytes(), *pub_key.as_bytes())
}
pub fn encrypt_and_sign_content(
signing_key: [u8; 32],
content: Vec<u8>,
receiver_pub_exchange_keys: Vec<u8>,
// returns (nonce, key_sets packed together all as one line, encrypted_content, sender_public_key, encrypted_content_signature)
) -> Result<([u8; 24], Vec<u8>, Vec<u8>, [u8; 32], [u8; 64]), String> {
let signing_key = ed25519_dalek::SigningKey::from_bytes(&signing_key);
let private_content_key =
chacha20poly1305::XChaCha20Poly1305::generate_key(&mut rand_core::OsRng);
let content_cipher = chacha20poly1305::XChaCha20Poly1305::new(&private_content_key);
let nonce = chacha20poly1305::XChaCha20Poly1305::generate_nonce(&mut rand_core::OsRng);
let mut content_signature = signing_key.clone().sign(&content).to_vec();
let verifying_key = signing_key.verifying_key();
content_signature.extend(verifying_key.as_bytes().to_vec());
content_signature.extend(content);
let encrypted_content = match content_cipher.encrypt(&nonce, content_signature.as_ref()) {
Ok(ec) => ec,
Err(err) => return Err(format!("failed to encrypt content: {}", err.to_string())),
};
// 256 bit [u8; 32] receiver_pub_exchange_key
// 256 bit [u8; 32] encrypted_content_key
let mut key_sets: Vec<u8> = vec![];
let (sender_priv_key, sender_public_key) = generate_exchange_keys();
for receiver_pub_exchange_key in receiver_pub_exchange_keys.chunks(32) {
let sender_priv_exchange_key = x25519_dalek::StaticSecret::from(sender_priv_key);
let receiver_pub_exchange_key_as_fixed_bytes: [u8; 32] = receiver_pub_exchange_key
.try_into()
.expect("failed to convert receiver pub exchange key to fixed bytes");
let shared_secret = sender_priv_exchange_key
.diffie_hellman(&x25519_dalek::PublicKey::from(
receiver_pub_exchange_key_as_fixed_bytes,
))
.to_bytes();
let encrypted_content_key_as_bytes = private_content_key.as_slice();
let encrypted_content_key_as_fixed_bytes: [u8; 32] = encrypted_content_key_as_bytes
.try_into()
.expect("failed to convert encrypted content key to fixed bytes");
let encrypted_content_key =
block_encrypt_key(shared_secret, encrypted_content_key_as_fixed_bytes);
key_sets.extend(receiver_pub_exchange_key.iter());
key_sets.extend(encrypted_content_key);
}
let encrypted_content_signature = signing_key.sign(&encrypted_content).to_bytes();
Ok((
nonce.into(),
key_sets,
encrypted_content,
sender_public_key,
encrypted_content_signature,
))
}
pub fn decrypt_and_verify(
sender_pub_exchange_key: [u8; 32],
receiver_priv_exchange_key: [u8; 32],
encrypted_content_key: [u8; 32],
nonce: [u8; 24],
encrypted_content: Vec<u8>,
// (content, verifying_key) -> verifying key is to compare against deserialized content
) -> Result<(Vec<u8>, [u8; 32]), String> {
let sender_pub_exchange_key = x25519_dalek::PublicKey::from(sender_pub_exchange_key);
let receiver_priv_exchange_key = x25519_dalek::StaticSecret::from(receiver_priv_exchange_key);
let shared_secret = receiver_priv_exchange_key
.diffie_hellman(&sender_pub_exchange_key)
.to_bytes();
let decrypted_content_key = block_decrypt_key(shared_secret, encrypted_content_key);
let content_cipher =
chacha20poly1305::XChaCha20Poly1305::new(GenericArray::from_slice(&decrypted_content_key));
match content_cipher.decrypt(
&chacha20poly1305::XNonce::from(nonce),
encrypted_content.as_ref(),
) {
Ok(content) => {
let signature_as_bytes = &content[0..64];
let signature_as_fixed_bytes: [u8; 64] = signature_as_bytes
.try_into()
.expect("failed to convert signature to fixed bytes");
let verifying_key_as_bytes = &content[64..96];
let verifying_key = verifying_key_as_bytes
.try_into()
.expect("failed to convert verifying key to fixed bytes");
let content = content[96..].to_vec();
match verify_signature(signature_as_fixed_bytes, verifying_key, content.clone()) {
Ok(_) => Ok((content, verifying_key)),
Err(err) => Err(format!("failed to verify signature: {}", err.to_string())),
}
}
Err(err) => return Err(format!("failed to decrypt content: {}", err.to_string())),
}
}
#[cfg(test)]
mod encryption_and_verification_tests {
use super::*;
#[test]
fn test_encryption_and_verification() -> Result<(), String> {
let (priv_exchange_key, pub_exchange_key) = generate_exchange_keys();
let (signing_key, _) = generate_signing_keys();
let content = vec![0u8; 1024];
let (nonce, key_sets, encrypted_content, sender_public_key, _) =
encrypt_and_sign_content(signing_key, content.clone(), pub_exchange_key.to_vec())
.unwrap();
let mut encrypted_content_key: [u8; 32] = [0u8; 32];
encrypted_content_key[0..32].copy_from_slice(&key_sets[32..64]);
let (decrypted_content, _) = decrypt_and_verify(
sender_public_key,
priv_exchange_key,
encrypted_content_key,
nonce,
encrypted_content,
)
.unwrap();
assert_eq!(decrypted_content, content);
Ok(())
}
}
crypto/benches/lib.rs
(sorry for out of order files… just better logically)
use criterion::{criterion_group, criterion_main, Criterion};
use lazy_static::lazy_static;
lazy_static! {
static ref PUB_PRIV_SIGN_VER_CONT_ENC_SIG_NONCE_ECK_SPK: (
[u8; 32],
[u8; 32],
[u8; 32],
[u8; 32],
Vec<u8>,
Vec<u8>,
[u8; 64],
[u8; 24],
[u8; 32],
[u8; 32],
) = {
let (priv_exchange_key, pub_exchange_key) = crypto::generate_exchange_keys();
let (signing_key, verifying_key) = crypto::generate_signing_keys();
let content = vec![0u8; 1024];
let (nonce, key_sets, encrypted_content, sender_public_key, encrypted_content_signature) =
crypto::encrypt_and_sign_content(
signing_key,
content.clone(),
pub_exchange_key.to_vec(),
)
.unwrap();
let mut encrypted_content_key: [u8; 32] = [0u8; 32];
encrypted_content_key[0..32].copy_from_slice(&key_sets[32..64]);
(
priv_exchange_key,
pub_exchange_key,
signing_key,
verifying_key,
content,
encrypted_content,
encrypted_content_signature,
nonce,
encrypted_content_key,
sender_public_key,
)
};
}
fn hash_to_32_bytes_benchmark(c: &mut Criterion) {
c.bench_function("hash_to_32_bytes", |b| {
let val = "benching hash to 32 bytes".to_string();
b.iter(move || {
crypto::hash_to_32_bytes(val.clone());
})
});
}
fn hash_to_string_benchmark(c: &mut Criterion) {
c.bench_function("hash_to_string", |b| {
let val = "benching hash to string".to_string();
b.iter(move || {
crypto::hash_to_string(val.clone());
})
});
}
fn decode_32_byte_key_from_string_benchmark(c: &mut Criterion) {
let (_, _, sign, _, _, _, _, _, _, _) = &*PUB_PRIV_SIGN_VER_CONT_ENC_SIG_NONCE_ECK_SPK;
c.bench_function("decode_32_byte_key_from_string", |b| {
let val = hex::encode(sign);
b.iter(move || {
crypto::decode_32_byte_key_from_string(val.clone());
})
});
}
fn encode_32_byte_key_to_string_benchmark(c: &mut Criterion) {
let (_, _, sign, _, _, _, _, _, _, _) = &*PUB_PRIV_SIGN_VER_CONT_ENC_SIG_NONCE_ECK_SPK;
c.bench_function("encode_32_byte_key_to_string", |b| {
b.iter(move || {
crypto::encode_32_byte_key_to_string(*sign);
})
});
}
fn block_encrypt_key_benchmark(c: &mut Criterion) {
let (pub_key, priv_key, _, _, _, _, _, _, _, _) =
&*PUB_PRIV_SIGN_VER_CONT_ENC_SIG_NONCE_ECK_SPK;
c.bench_function("block_encrypt_key", |b| {
b.iter(|| {
crypto::block_encrypt_key(*pub_key, *priv_key);
})
});
}
fn block_decrypt_key_benchmark(c: &mut Criterion) {
let (pub_key, priv_key, _, _, _, _, _, _, _, _) =
&*PUB_PRIV_SIGN_VER_CONT_ENC_SIG_NONCE_ECK_SPK;
c.bench_function("block_decrypt_key", |b| {
b.iter(|| {
crypto::block_decrypt_key(*pub_key, *priv_key);
})
});
}
fn generate_signing_keys_benchmark(c: &mut Criterion) {
c.bench_function("generate_signing_keys", |b| {
b.iter(move || {
crypto::generate_signing_keys();
})
});
}
fn sign_content_benchmark(c: &mut Criterion) {
let (_, _, sign, _, _, enc, _, _, _, _) = &*PUB_PRIV_SIGN_VER_CONT_ENC_SIG_NONCE_ECK_SPK;
c.bench_function("sign_content", |b| {
b.iter(move || {
crypto::sign_content(enc.clone(), *sign);
})
});
}
fn verify_signature_benchmark(c: &mut Criterion) {
let (_, _, _, ver, _, enc, sig, _, _, _) = &*PUB_PRIV_SIGN_VER_CONT_ENC_SIG_NONCE_ECK_SPK;
c.bench_function("verify_signature", |b| {
b.iter(move || {
crypto::verify_signature(*sig, *ver, enc.clone()).unwrap();
})
});
}
fn generate_exchange_keys_benchmark(c: &mut Criterion) {
c.bench_function("generate_exchange_keys", |b| {
b.iter(move || {
crypto::generate_exchange_keys();
})
});
}
fn encrypt_and_sign_content_benchmark(c: &mut Criterion) {
let (pub_key, _, sign, _, cont, _, _, _, _, _) = &*PUB_PRIV_SIGN_VER_CONT_ENC_SIG_NONCE_ECK_SPK;
c.bench_function("encrypt_and_sign_content", |b| {
b.iter(move || {
crypto::encrypt_and_sign_content(*sign, cont.clone(), pub_key.to_vec())
.unwrap();
})
});
}
fn decrypt_and_verify_benchmark(c: &mut Criterion) {
let (_, priv_key, _, _, _, enc, _, nonce, eck, spk) =
&*PUB_PRIV_SIGN_VER_CONT_ENC_SIG_NONCE_ECK_SPK;
c.bench_function("decrypt_and_verify", |b| {
b.iter(move || {
crypto::decrypt_and_verify(*spk, *priv_key, *eck, *nonce, enc.clone()).unwrap();
})
});
}
criterion_group!(
benches,
hash_to_32_bytes_benchmark,
hash_to_string_benchmark,
block_encrypt_key_benchmark,
block_decrypt_key_benchmark,
generate_signing_keys_benchmark,
sign_content_benchmark,
decode_32_byte_key_from_string_benchmark,
encode_32_byte_key_to_string_benchmark,
verify_signature_benchmark,
generate_exchange_keys_benchmark,
encrypt_and_sign_content_benchmark,
decrypt_and_verify_benchmark,
);
criterion_main!(benches);
Conclusion
Tests are cool and new - to me - so it’s fun to post about them. This also gives better insight into what the crypto lib looks like now.
Caveats
I don’t know that these tests are exhaustive and are mostly just for my own experimentation.