commit 257ac5e10bbf49f8cb5bfe6c43c7f93cb39fc17f
Author: Jackson G. Kaindume <seestem@merely.tech>
Date: Wed, 10 Aug 2022 22:10:49 +0200
Initial commit
Diffstat:
3 files changed, 243 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,3 @@
+*~
+/target
+/Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "bizkit-crypto"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.57"
+ring = "0.16.20"
+chacha20poly1305 = "0.9.0"
+base64 = "0.13.0"
+\ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
@@ -0,0 +1,227 @@
+use anyhow::Result;
+use chacha20poly1305::{
+ aead::{Aead, NewAead},
+ Key, XChaCha20Poly1305, XNonce,
+};
+use ring::{
+ digest::{self, digest, Digest},
+ pbkdf2,
+ rand::SystemRandom,
+ signature::{Ed25519KeyPair, KeyPair},
+};
+use std::num::NonZeroU32;
+
+static PBKDF2_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;
+const CREDENTIAL_LEN: usize = digest::SHA256_OUTPUT_LEN;
+const PBKDF2_ITERATIONS: u32 = 100_000;
+/// Database seed used to generate password hashes,
+/// so that an attacker cannot crack the same user's password across
+/// databases in the unfortunate but common case that the user has
+/// used the same password for multiple systems.
+const DATABASE_SEED: &str =
+ "The cipher has an interesting history: although its true origins are unknown";
+/// Nonce used when encrypting the Crypto's keypair (Crypto::encrypt_keypair_document())
+const AED_NONCE: &str = "people the information ";
+pub type Credential = [u8; CREDENTIAL_LEN];
+
+pub struct Credentials {
+ /// Base64 encoded key pair serialized as a PKCS#8 document
+ pub keypair_doc: String,
+ /// Base64 encoded public key
+ pub public_key: String,
+}
+
+pub struct Crypto {}
+
+impl Crypto {
+ /// Generate Ed25519 key pair, for signing
+ pub fn gen_ed25519_document() -> Result<Credentials> {
+ let rng = SystemRandom::new();
+ // TODO: don't use expect()
+ let keypair_document =
+ Ed25519KeyPair::generate_pkcs8(&rng).expect("Could not generate Ed25519KeyPair");
+ let keypair = Ed25519KeyPair::from_pkcs8(keypair_document.as_ref())
+ .expect("Could not decode keypair from keypair document");
+ let public_key_string = base64::encode(keypair.public_key().as_ref());
+ let keypair_document_string = base64::encode(keypair_document.as_ref());
+ let cred = Credentials {
+ keypair_doc: keypair_document_string,
+ public_key: public_key_string,
+ };
+
+ Ok(cred)
+ }
+
+ /// Generate pbkdf2 based hashed password
+ pub fn hash_password(username: &str, password: &str) -> Result<String> {
+ let pbkdf2_iterations: NonZeroU32 = NonZeroU32::new(PBKDF2_ITERATIONS).unwrap();
+ let salt = Crypto::gen_password_salt(username);
+ let mut to_store: Credential = [0u8; CREDENTIAL_LEN];
+ pbkdf2::derive(
+ PBKDF2_ALG,
+ pbkdf2_iterations,
+ &salt,
+ password.as_bytes(),
+ &mut to_store,
+ );
+
+ let hash = base64::encode(to_store);
+ Ok(hash)
+ }
+
+ /// Verify pbkdf2 based password hash
+ fn verify_password_hash(username: &str, attempted_password: &str, hash: &str) -> Result<bool> {
+ let pbkdf2_iterations: NonZeroU32 = NonZeroU32::new(PBKDF2_ITERATIONS).unwrap();
+ let salt = Crypto::gen_password_salt(username);
+ let hash_bytes = base64::decode(hash)?;
+ let res = pbkdf2::verify(
+ PBKDF2_ALG,
+ pbkdf2_iterations,
+ &salt,
+ attempted_password.as_bytes(),
+ &hash_bytes,
+ );
+
+ match res {
+ Ok(()) => Ok(true),
+ Err(_e) => Ok(false),
+ }
+ }
+
+ /// Generate password salt
+ /// The salt should have a user-specific component so that an attacker
+ /// cannot crack one password for multiple users in the database. It
+ /// should have a database-unique component so that an attacker cannot
+ /// crack the same user's password across databases in the unfortunate
+ /// but common case that the user has used the same password for
+ /// multiple systems.
+ fn gen_password_salt(username: &str) -> Vec<u8> {
+ let db_salt_component = DATABASE_SEED.as_bytes();
+ let mut salt = Vec::with_capacity(db_salt_component.len() + username.as_bytes().len());
+ salt.extend(db_salt_component.as_ref());
+ salt.extend(username.as_bytes());
+ salt
+ }
+
+ /// Encrypt the keypair (ed25519) document, using XChaCha20Poly1305
+ /// so that it can be safely persisted into the database. The
+ /// Crypto's login password is used a the secret key.
+ pub fn encrypt_keypair_document(keypair_document: &str, password: &str) -> Result<String> {
+ let secret_key = Crypto::gen_secret_key(password);
+ let key = Key::from_slice(&secret_key.as_ref()[..32]); // 32-bytes
+ let aead = XChaCha20Poly1305::new(key);
+ let nonce = XNonce::from_slice(AED_NONCE.as_bytes()); // 24-bytes; unique
+ let keypair_document_bytes = base64::decode(keypair_document)?;
+ let res = base64::encode(
+ aead.encrypt(nonce, keypair_document_bytes.as_ref())
+ .expect("encryption failure!"),
+ );
+ Ok(res)
+ }
+
+ /// Decrypt the encrypted keypair (ed25519), using XChaCha20Poly1305
+ fn decrypt_keypair_document(cyphertext: &str, password: &str) -> Result<String> {
+ let secret_key = Crypto::gen_secret_key(password);
+ let key = Key::from_slice(&secret_key.as_ref()[..32]); // 32-bytes
+ let nonce = XNonce::from_slice(AED_NONCE.as_bytes()); // 24-bytes; unique
+ let aead = XChaCha20Poly1305::new(key);
+ let cyphertext_bytes = base64::decode(cyphertext)?;
+ let res = base64::encode(
+ aead.decrypt(nonce, cyphertext_bytes.as_ref())
+ .expect("decryption failure!"),
+ );
+ Ok(res)
+ }
+
+ /// Generate secret key, used to encrypt the ed25519 keypair
+ fn gen_secret_key(password: &str) -> Digest {
+ digest(&digest::SHA256, password.as_bytes())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use ring::signature::{Ed25519KeyPair, KeyPair, UnparsedPublicKey, ED25519};
+
+ #[test]
+ fn hash_password() {
+ let password1 = "a bad password";
+ let username1 = "goldephoenix";
+ let hash1 = Crypto::hash_password(username1, password1).unwrap();
+
+ let password2 = "a bad password";
+ let username2 = "alexis";
+ let hash2 = Crypto::hash_password(username2, password2).unwrap();
+
+ // The generated hashes are all 32 bytes long
+ assert_eq!(hash1.len(), 44);
+ assert_eq!(hash2.len(), 44);
+
+ // Even if the user passwords are the same they do not generate
+ // the same hashes because the username is also used as a
+ // seed when the hash is generated.
+ assert_ne!(hash1, hash2);
+
+ // Verify password
+ assert!(Crypto::verify_password_hash(username1, "a bad password", &hash1).unwrap());
+ assert!(!Crypto::verify_password_hash(username1, "wrong password", &hash1).unwrap());
+ }
+
+ #[test]
+ fn gen_keypairs() {
+ let cred = Crypto::gen_ed25519_document().unwrap();
+ let document = base64::decode(cred.keypair_doc).unwrap();
+ let key_pair = Ed25519KeyPair::from_pkcs8(&document).unwrap();
+
+ // Sign the message "hello, world".
+ const MESSAGE: &[u8] = b"hello, world";
+ let sig = key_pair.sign(MESSAGE);
+
+ // Normally an application would extract the bytes of the signature and
+ // send them in a protocol message to the peer(s). Here we just get the
+ // public key key directly from the key pair.
+ let peer_public_key_bytes = key_pair.public_key().as_ref();
+
+ // Verify the signature of the message using the public key. Normally the
+ // verifier of the message would parse the inputs to this code out of the
+ // protocol message(s) sent by the signer.
+ let peer_public_key = UnparsedPublicKey::new(&ED25519, peer_public_key_bytes);
+ peer_public_key.verify(MESSAGE, sig.as_ref()).unwrap();
+ }
+
+ #[test]
+ fn symmetric_encryption_decryption() {
+ let password = "my very weak password";
+ let cred = Crypto::gen_ed25519_document().unwrap();
+ let document = base64::decode(cred.keypair_doc.clone()).unwrap();
+ let expected_key_pair = Ed25519KeyPair::from_pkcs8(&document).unwrap();
+
+ let wrong_cred = Crypto::gen_ed25519_document().unwrap();
+ let wrong_document = base64::decode(wrong_cred.keypair_doc).unwrap();
+ let wrong_key_pair = Ed25519KeyPair::from_pkcs8(&wrong_document).unwrap();
+
+ // encrypt the document
+ let cyphertext = Crypto::encrypt_keypair_document(&cred.keypair_doc, password).unwrap();
+ let cleartext = Crypto::decrypt_keypair_document(&cyphertext, password).unwrap();
+
+ let cleartext_bytes = base64::decode(cleartext.clone()).unwrap();
+ let key_pair = Ed25519KeyPair::from_pkcs8(cleartext_bytes.as_ref()).unwrap();
+
+ assert_ne!(cyphertext, cleartext);
+ assert_eq!(
+ expected_key_pair.public_key().as_ref(),
+ key_pair.public_key().as_ref()
+ );
+
+ assert_ne!(
+ expected_key_pair.public_key().as_ref(),
+ wrong_key_pair.public_key().as_ref()
+ );
+
+ assert_ne!(
+ key_pair.public_key().as_ref(),
+ wrong_key_pair.public_key().as_ref()
+ );
+ }
+}