credential-crypto 0.1.0

Rust credential crypto library
Documentation
use anyhow::anyhow;
use ring::aead::{AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey};
use ring::rand::{SecureRandom, SystemRandom};
use sha2::{Digest, Sha256};

pub struct CredentialCrypto {
    pub key: String,
}

pub trait CredentialCryptoProvider {
    fn new(key: String) -> CredentialCrypto;
    fn generate_salt(&self) -> Result<String, anyhow::Error>;
    fn encrypt(&self, salt: &str, plaintext: &str) -> Result<String, anyhow::Error>;
    fn decrypt(&self, salt: &str, password: &str) -> Result<String, anyhow::Error>;
}

impl CredentialCryptoProvider for CredentialCrypto {
    fn new(key: String) -> CredentialCrypto {
        CredentialCrypto { key } // machine_no  card_no
    }

    fn generate_salt(&self) -> Result<String, anyhow::Error> {
        let mut nonce_bytes = [0u8; 12];

        SystemRandom::new()
            .fill(&mut nonce_bytes)
            .map_err(|err| anyhow!("Cannot generate random nonce {err}"))?;

        Ok(hex::encode(nonce_bytes))
    }

    fn encrypt(&self, salt: &str, plaintext: &str) -> Result<String, anyhow::Error> {
        if plaintext.is_empty() {
            return Err(anyhow!("password is empty"));
        }

        let nonce_bytes: [u8; 12] = hex::decode(salt)?
            .try_into()
            .map_err(|err| anyhow!("nonce must be exactly 12 bytes {:#?}", err))?;

        let nonce = Nonce::assume_unique_for_key(nonce_bytes);

        let mut hasher = Sha256::new();
        hasher.update(self.key.as_bytes());
        let key_bytes = hasher.finalize();

        let unbound_key = match UnboundKey::new(&AES_256_GCM, &key_bytes) {
            Ok(key) => key,
            Err(err) => {
                return Err(anyhow!("Key derivation failed {err}"));
            }
        };
        let key = LessSafeKey::new(unbound_key);

        // 加密
        let mut in_out = plaintext.as_bytes().to_vec();
        if let Err(err) = key.seal_in_place_append_tag(nonce, Aad::empty(), &mut in_out) {
            return Err(anyhow!("encryption failed {err}"));
        }

        Ok(hex::encode(in_out))
    }

    fn decrypt(&self, salt: &str, password: &str) -> Result<String, anyhow::Error> {
        if password.is_empty() {
            return Err(anyhow!("password is empty"));
        }

        let nonce_bytes: [u8; 12] = hex::decode(salt)?
            .try_into()
            .map_err(|err| anyhow!("nonce must be exactly 12 bytes {:#?}", err))?;

        let nonce = Nonce::assume_unique_for_key(nonce_bytes);

        let mut hasher = Sha256::new();
        hasher.update(self.key.as_bytes());
        let key_bytes = hasher.finalize();

        let unbound_key = match UnboundKey::new(&AES_256_GCM, &key_bytes) {
            Ok(v) => v,
            Err(err) => {
                return Err(anyhow!("Key derivation failed {err}"));
            }
        };
        let key = LessSafeKey::new(unbound_key);

        let mut in_out = hex::decode(&password)?;

        // 執行解密
        let plaintext_slice = match key.open_in_place(nonce, Aad::empty(), &mut in_out) {
            Ok(v) => v,
            Err(err) => {
                return Err(anyhow!("decryption failed {err}"));
            }
        };

        let plaintext = String::from_utf8_lossy(plaintext_slice).into_owned();

        Ok(plaintext)
    }
}

#[cfg(test)]
mod tests {
    use crate::{CredentialCrypto, CredentialCryptoProvider};

    #[test]
    fn generate_payload() {
        let client = CredentialCrypto::new("1000000002".to_string());

        let salt = "b3d6f762773d4ef7d1cbdfda";
        let password = "21c95597d3829efd728eb1084e2e22b2350a199b8c14";
        let p = client.decrypt(salt, password).unwrap();

        let w = client.encrypt(salt, "888888").unwrap();

        println!("{}, {}, {}", p, w, password == w);
    }
}