cyrtophora

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 257ac5e10bbf49f8cb5bfe6c43c7f93cb39fc17f
Author: Jackson G. Kaindume <seestem@merely.tech>
Date:   Wed, 10 Aug 2022 22:10:49 +0200

Initial commit

Diffstat:
A.gitignore | 3+++
ACargo.toml | 13+++++++++++++
Asrc/lib.rs | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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() + ); + } +}