1use anyhow::anyhow;
2use ring::aead::{AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey};
3use ring::rand::{SecureRandom, SystemRandom};
4use sha2::{Digest, Sha256};
5
6pub struct CredentialCrypto {
7 pub key: String,
8}
9
10pub trait CredentialCryptoProvider {
11 fn new(key: String) -> CredentialCrypto;
12 fn generate_salt(&self) -> Result<String, anyhow::Error>;
13 fn encrypt(&self, salt: &str, plaintext: &str) -> Result<String, anyhow::Error>;
14 fn decrypt(&self, salt: &str, password: &str) -> Result<String, anyhow::Error>;
15}
16
17impl CredentialCryptoProvider for CredentialCrypto {
18 fn new(key: String) -> CredentialCrypto {
19 CredentialCrypto { key } }
21
22 fn generate_salt(&self) -> Result<String, anyhow::Error> {
23 let mut nonce_bytes = [0u8; 12];
24
25 SystemRandom::new()
26 .fill(&mut nonce_bytes)
27 .map_err(|err| anyhow!("Cannot generate random nonce {err}"))?;
28
29 Ok(hex::encode(nonce_bytes))
30 }
31
32 fn encrypt(&self, salt: &str, plaintext: &str) -> Result<String, anyhow::Error> {
33 if plaintext.is_empty() {
34 return Err(anyhow!("password is empty"));
35 }
36
37 let nonce_bytes: [u8; 12] = hex::decode(salt)?
38 .try_into()
39 .map_err(|err| anyhow!("nonce must be exactly 12 bytes {:#?}", err))?;
40
41 let nonce = Nonce::assume_unique_for_key(nonce_bytes);
42
43 let mut hasher = Sha256::new();
44 hasher.update(self.key.as_bytes());
45 let key_bytes = hasher.finalize();
46
47 let unbound_key = match UnboundKey::new(&AES_256_GCM, &key_bytes) {
48 Ok(key) => key,
49 Err(err) => {
50 return Err(anyhow!("Key derivation failed {err}"));
51 }
52 };
53 let key = LessSafeKey::new(unbound_key);
54
55 let mut in_out = plaintext.as_bytes().to_vec();
57 if let Err(err) = key.seal_in_place_append_tag(nonce, Aad::empty(), &mut in_out) {
58 return Err(anyhow!("encryption failed {err}"));
59 }
60
61 Ok(hex::encode(in_out))
62 }
63
64 fn decrypt(&self, salt: &str, password: &str) -> Result<String, anyhow::Error> {
65 if password.is_empty() {
66 return Err(anyhow!("password is empty"));
67 }
68
69 let nonce_bytes: [u8; 12] = hex::decode(salt)?
70 .try_into()
71 .map_err(|err| anyhow!("nonce must be exactly 12 bytes {:#?}", err))?;
72
73 let nonce = Nonce::assume_unique_for_key(nonce_bytes);
74
75 let mut hasher = Sha256::new();
76 hasher.update(self.key.as_bytes());
77 let key_bytes = hasher.finalize();
78
79 let unbound_key = match UnboundKey::new(&AES_256_GCM, &key_bytes) {
80 Ok(v) => v,
81 Err(err) => {
82 return Err(anyhow!("Key derivation failed {err}"));
83 }
84 };
85 let key = LessSafeKey::new(unbound_key);
86
87 let mut in_out = hex::decode(&password)?;
88
89 let plaintext_slice = match key.open_in_place(nonce, Aad::empty(), &mut in_out) {
91 Ok(v) => v,
92 Err(err) => {
93 return Err(anyhow!("decryption failed {err}"));
94 }
95 };
96
97 let plaintext = String::from_utf8_lossy(plaintext_slice).into_owned();
98
99 Ok(plaintext)
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use crate::{CredentialCrypto, CredentialCryptoProvider};
106
107 #[test]
108 fn generate_payload() {
109 let client = CredentialCrypto::new("1000000002".to_string());
110
111 let salt = "b3d6f762773d4ef7d1cbdfda";
112 let password = "21c95597d3829efd728eb1084e2e22b2350a199b8c14";
113 let p = client.decrypt(salt, password).unwrap();
114
115 let w = client.encrypt(salt, "888888").unwrap();
116
117 println!("{}, {}, {}", p, w, password == w);
118 }
119}