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 } }
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);
}
}