commit 9b3451ca36d0ffcdb9731e552cc49d74e2fee8c4
parent b76d05dc5725068147856ff6fe28bd4d31cc0530
Author: Jackson G. Kaindume <seestem@merely.tech>
Date: Sun, 21 Aug 2022 11:33:17 +0200
move crypto to crypto.rs file
Diffstat:
A | src/crypto.rs | | | 191 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 191 insertions(+), 0 deletions(-)
diff --git a/src/crypto.rs b/src/crypto.rs
@@ -0,0 +1,191 @@
+use anyhow::Result;
+use chacha20poly1305::{
+ aead::{Aead, NewAead},
+ Key, XChaCha20Poly1305, XNonce,
+};
+use ring::{
+ digest::{self, digest, Digest},
+ rand::SystemRandom,
+ signature::{Ed25519KeyPair, KeyPair},
+};
+use scrypt::{
+ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
+ Scrypt,
+};
+
+const CREDENTIAL_LEN: usize = digest::SHA256_OUTPUT_LEN;
+
+/// 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 scrypt based hashed password
+ pub fn hash_password(password: &str) -> Result<String> {
+ let salt = SaltString::generate(&mut OsRng);
+ Ok(Scrypt
+ .hash_password(password.as_bytes(), &salt)?
+ .to_string())
+ }
+
+ /// Verify scrypt based password hash
+ fn verify_password_hash(attempted_password: &str, hash: &str) -> Result<bool> {
+ let parsed_hash = PasswordHash::new(hash)?;
+
+ if Scrypt
+ .verify_password(attempted_password.as_bytes(), &parsed_hash)
+ .is_ok()
+ {
+ Ok(true)
+ } else {
+ Ok(false)
+ }
+ }
+
+ /// 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 hash1 = Crypto::hash_password(password1).unwrap();
+
+ let password2 = "a bad password";
+ let hash2 = Crypto::hash_password(password2).unwrap();
+
+ // The generated hashes are all 88 bytes long
+ assert_eq!(hash1.len(), 88);
+ assert_eq!(hash2.len(), 88);
+
+ // 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("a bad password", &hash1).unwrap());
+ assert!(!Crypto::verify_password_hash("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()
+ );
+ }
+}