Just checking back in on the cryptography stuff. Still feeling genuinely bad about that whole DeleteMessage
thing. But ya live and ya learn
and nobody got hurt, and at the end of the day, the imaginary boogeyman I’m trying to please is just me anyway.
Okay, so with that in mind a few things have been added and a few names have changed. There is also a new benchmark update (https://problemchild.engineering/benchmaks/crypto/2023-08-18/report) and so forth.
I switched back from hex
to base64
. I honestly don’t remember why I switched before, and I’d worked with hex before in the automotive space
so I was like “oh cool I know this one so it’s probably good!” but then turns out after doing a little bit of research, I’m not doing anything with
URLs or with human readableness, so the additional 70%(? math is hard) in size that hex shows up with doesn’t add value in either of those dimensions (hex encodes 2 for every byte where base64
encodes 3 for every 4), and fortunately, the switch actually sped up the benchmark too. So best of both worlds.
Okay, here’s the code:
Cargo.toml
[package]
name = "crypto"
version = "0.1.0"
edition = "2021"
[dependencies]
base64 = "0.21.2"
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
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 string_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 encode_key_to_string(val: [u8; 32]) -> String {
#[allow(deprecated)]
base64::encode(val)
}
pub fn encode_key_to_string_encrypted(key: [u8; 32], val: String) -> String {
#[allow(deprecated)]
base64::encode(block_encrypt_key(key, string_to_32_bytes(val)))
}
pub fn decode_key_from_string(val: String) -> [u8; 32] {
#[allow(deprecated)]
let decoded_val = base64::decode(val).expect("failed to decode val");
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
}
#[cfg(test)]
mod hash_and_encoding_tests {
use super::*;
#[test]
fn test_hash_and_encoding() -> Result<(), String> {
let val = "testing hash and encoding".to_string();
let val2 = "testing hash and encoding".to_string();
let bytes_hash = string_to_32_bytes(val.clone());
let bytes_hash2 = string_to_32_bytes(val2.clone());
assert_eq!(bytes_hash, bytes_hash2);
let str_hash = encode_key_to_string_encrypted([0u8; 32], val.clone());
let str_hash2 = encode_key_to_string_encrypted([0u8; 32], val2.clone());
assert_eq!(str_hash, str_hash2);
let str_hash_as_bytes = decode_key_from_string(str_hash);
assert_eq!(str_hash_as_bytes, bytes_hash);
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[0..16].copy_from_slice(block_one_as_generic_array.as_slice());
combined_array[16..32].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[0..16].copy_from_slice(block_one_as_generic_array.as_slice());
combined_array[16..32].copy_from_slice(block_two_as_generic_array.as_slice());
combined_array
}
pub fn block_encrypt_signature(key: [u8; 32], content: [u8; 64]) -> [u8; 64] {
let block_one: [u8; 32] = content[0..32]
.try_into()
.expect("failed to convert block one to fixed bytes");
let block_two: [u8; 32] = content[32..64]
.try_into()
.expect("failed to convert block two to fixed bytes");
let block_one_encrypted = block_encrypt_key(key, block_one);
let block_two_encrypted = block_encrypt_key(key, block_two);
let mut combined_array: [u8; 64] = [0; 64];
combined_array[0..32].copy_from_slice(block_one_encrypted.as_slice());
combined_array[32..64].copy_from_slice(block_two_encrypted.as_slice());
combined_array
}
pub fn block_decrypt_signature(key: [u8; 32], encrypted_content: [u8; 64]) -> [u8; 64] {
let block_one: [u8; 32] = encrypted_content[0..32]
.try_into()
.expect("failed to convert block one to fixed bytes");
let block_two: [u8; 32] = encrypted_content[32..64]
.try_into()
.expect("failed to convert block two to fixed bytes");
let block_one_decrypted = block_decrypt_key(key, block_one);
let block_two_decrypted = block_decrypt_key(key, block_two);
let mut combined_array: [u8; 64] = [0; 64];
combined_array[0..32].copy_from_slice(block_one_decrypted.as_slice());
combined_array[32..64].copy_from_slice(block_two_decrypted.as_slice());
combined_array
}
#[cfg(test)]
mod encryption_tests {
use super::*;
#[test]
fn test_key_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(())
}
#[test]
fn test_signature_encryption() -> Result<(), String> {
let (_, pub_exchange_key) = generate_exchange_keys();
let content = [0u8; 64];
let encrypted_signature = block_encrypt_signature(pub_exchange_key, content);
let decrypted_signature = block_decrypt_signature(pub_exchange_key, encrypted_signature);
assert_eq!(decrypted_signature, content);
Ok(())
}
}
pub fn generate_signing_keys() -> ([u8; 32], [u8; 32]) {
let signing_key: SigningKey = SigningKey::generate(&mut rand_core::OsRng);
(
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)
) -> Result<([u8; 24], Vec<u8>, Vec<u8>, [u8; 32]), 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);
}
Ok((nonce.into(), key_sets, encrypted_content, sender_public_key))
}
pub fn decrypt_content(
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_content(
sender_public_key,
priv_exchange_key,
encrypted_content_key,
nonce,
encrypted_content,
)
.unwrap();
assert_eq!(decrypted_content, content);
Ok(())
}
}
bench/lib.rs
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 signature = crypto::sign_content(content.clone(), signing_key);
let (nonce, key_sets, encrypted_content, sender_public_key) =
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,
signature,
nonce,
encrypted_content_key,
sender_public_key,
)
};
}
fn string_to_32_bytes_benchmark(c: &mut Criterion) {
c.bench_function("string_to_32_bytes", |b| {
let val = "benching string to 32 bytes".to_string();
b.iter(move || {
crypto::string_to_32_bytes(val.clone());
})
});
}
fn encode_key_to_string_encrypted_benchmark(c: &mut Criterion) {
c.bench_function("encode_key_to_string_encrypted", |b| {
let val = "benching key to encrypted string".to_string();
let key = [0u8; 32];
b.iter(move || {
crypto::encode_key_to_string_encrypted(key, val.clone());
})
});
}
fn decode_key_from_string_benchmark(c: &mut Criterion) {
c.bench_function("decode_key_from_string", |b| {
let val = crypto::encode_key_to_string_encrypted(
[0u8; 32],
"benching decode 32 byte key".to_string(),
);
b.iter(move || {
crypto::decode_key_from_string(val.clone());
})
});
}
fn encode_key_to_string_benchmark(c: &mut Criterion) {
let (_, _, sign, _, _, _, _, _, _, _) = &*PUB_PRIV_SIGN_VER_CONT_ENC_SIG_NONCE_ECK_SPK;
c.bench_function("encode_key_to_string", |b| {
b.iter(move || {
crypto::encode_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 block_encrypt_signature_benchmark(c: &mut Criterion) {
let (pub_key, _, _, _, _, _, sig, _, _, _) = &*PUB_PRIV_SIGN_VER_CONT_ENC_SIG_NONCE_ECK_SPK;
c.bench_function("block_encrypt_signature", |b| {
b.iter(|| {
crypto::block_encrypt_signature(*pub_key, *sig);
})
});
}
fn block_decrypt_signature_benchmark(c: &mut Criterion) {
let (pub_key, _, _, _, _, _, sig, _, _, _) = &*PUB_PRIV_SIGN_VER_CONT_ENC_SIG_NONCE_ECK_SPK;
c.bench_function("block_decrypt_signature", |b| {
b.iter(|| {
crypto::block_decrypt_signature(*pub_key, *sig);
})
});
}
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, cont, _, 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, cont.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_content_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_content", |b| {
b.iter(move || {
crypto::decrypt_content(*spk, *priv_key, *eck, *nonce, enc.clone()).unwrap();
})
});
}
criterion_group!(
benches,
string_to_32_bytes_benchmark,
encode_key_to_string_encrypted_benchmark,
decode_key_from_string_benchmark,
encode_key_to_string_benchmark,
block_encrypt_key_benchmark,
block_decrypt_key_benchmark,
block_encrypt_signature_benchmark,
block_decrypt_signature_benchmark,
generate_signing_keys_benchmark,
sign_content_benchmark,
decode_key_from_string_benchmark,
encode_key_to_string_benchmark,
verify_signature_benchmark,
generate_exchange_keys_benchmark,
encrypt_and_sign_content_benchmark,
decrypt_content_benchmark,
);
criterion_main!(benches);
Conclusion
Not gonna go in-depth but that’s what it looks like rn.
Also, sorry for ignoring the deprecated warnings on base64::encode
/base64::decode
. I’ll update it later.