etopay_sdk/types/
newtypes.rs1use super::{
23 crypto::Blake2b256,
24 error::{Result, TypeError},
25};
26use aes_gcm::{
27 Aes256Gcm, Nonce,
28 aead::{Aead, KeyInit},
29};
30use blake2::Digest;
31use log::warn;
32use rand::RngCore;
33use serde::{Deserialize, Serialize};
34use zxcvbn::{Score, zxcvbn};
35
36macro_rules! impl_redacted_debug {
37 ($type:ty) => {
38 impl core::fmt::Debug for $type {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 write!(f, "{}(<REDACTED>)", stringify!($type))
41 }
42 }
43 };
44}
45
46#[derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop, Clone)]
48pub struct PlainPassword(String);
49impl_redacted_debug!(PlainPassword);
50
51impl PlainPassword {
52 pub fn try_from_string(password: impl Into<String>) -> Result<Self> {
54 let password: String = password.into();
55 if password.is_empty() {
56 return Err(TypeError::EmptyPassword);
57 }
58
59 let password_strength = zxcvbn(password.as_str(), &[]).score();
62
63 if password_strength < Score::Three {
64 warn!("User attempted to set a weak password");
65 return Err(TypeError::WeakPassword);
66 }
67
68 Ok(Self(password))
69 }
70
71 pub fn encrypt(&self, pin: &EncryptionPin, salt: &EncryptionSalt) -> Result<EncryptedPassword> {
73 let key = Blake2b256::new()
74 .chain_update(pin.0.as_ref())
75 .chain_update(salt.0.as_ref())
76 .finalize();
77
78 let Ok(cipher) = Aes256Gcm::new_from_slice(&key) else {
79 return Err(TypeError::PasswordEncryption);
80 };
81
82 let nonce = Nonce::from_slice(salt.0.as_ref()); let Ok(cipher) = cipher.encrypt(nonce, self.0.as_ref()) else {
84 return Err(TypeError::PasswordEncryption);
85 };
86
87 Ok(EncryptedPassword(cipher.into()))
88 }
89
90 pub fn into_secret(&self) -> secrecy::SecretBox<[u8]> {
92 secrecy::SecretBox::new(self.0.as_bytes().into())
93 }
94
95 pub fn into_secret_string(&self) -> secrecy::SecretString {
97 self.0.clone().into()
98 }
99
100 pub fn as_str(&self) -> &str {
102 &self.0
103 }
104}
105
106impl TryFrom<String> for PlainPassword {
107 type Error = TypeError;
108 fn try_from(value: String) -> Result<Self> {
109 Self::try_from_string(value)
110 }
111}
112impl TryFrom<&str> for PlainPassword {
113 type Error = TypeError;
114 fn try_from(value: &str) -> Result<Self> {
115 Self::try_from_string(value)
116 }
117}
118
119#[derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop, Deserialize, Serialize, Clone)]
121#[cfg_attr(test, derive(PartialEq))]
122pub struct EncryptedPassword(Box<[u8]>);
123impl_redacted_debug!(EncryptedPassword);
124
125impl EncryptedPassword {
126 pub fn decrypt(&self, pin: &EncryptionPin, salt: &EncryptionSalt) -> Result<PlainPassword> {
129 let key = Blake2b256::new()
130 .chain_update(pin.0.as_ref())
131 .chain_update(salt.0.as_ref())
132 .finalize();
133
134 let Ok(cipher) = Aes256Gcm::new_from_slice(&key) else {
135 return Err(TypeError::PasswordEncryption);
136 };
137
138 let nonce = Nonce::from_slice(salt.0.as_ref()); let Ok(plaintext) = cipher.decrypt(nonce, self.0.as_ref()) else {
140 return Err(TypeError::InvalidPinOrPassword);
141 };
142
143 Ok(PlainPassword(String::from_utf8_lossy(&plaintext).to_string()))
144 }
145
146 pub unsafe fn new_unchecked(bytes: impl Into<Vec<u8>>) -> Self {
153 Self(bytes.into().into())
154 }
155}
156
157#[derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
159pub struct EncryptionPin(Box<[u8]>);
160impl_redacted_debug!(EncryptionPin);
161
162impl EncryptionPin {
163 pub fn try_from_string(pin: impl Into<String>) -> Result<Self> {
165 let pin: String = pin.into();
166 if pin.is_empty() {
167 return Err(TypeError::EmptyPin);
168 }
169
170 if pin.len() < 6 {
171 warn!("Pin is less than 6 digits");
172 return Err(TypeError::WeakPin);
173 }
174
175 if !pin.chars().all(|c| c.is_ascii_digit()) {
176 warn!("Pin contains non-numeric characters");
177 return Err(TypeError::NonNumericPin);
178 }
179
180 Ok(Self(pin.as_bytes().into()))
181 }
182}
183impl TryFrom<String> for EncryptionPin {
184 type Error = TypeError;
185 fn try_from(value: String) -> Result<Self> {
186 Self::try_from_string(value)
187 }
188}
189impl TryFrom<&str> for EncryptionPin {
190 type Error = TypeError;
191 fn try_from(value: &str) -> Result<Self> {
192 Self::try_from_string(value)
193 }
194}
195
196#[derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop, Deserialize, Serialize, Clone)]
199#[cfg_attr(test, derive(PartialEq))]
200pub struct EncryptionSalt(Box<[u8; 12]>);
201impl_redacted_debug!(EncryptionSalt);
202
203impl EncryptionSalt {
204 pub fn generate() -> Self {
206 let mut salt = [0u8; 12];
207 rand::rng().fill_bytes(&mut salt);
208 Self(Box::new(salt))
209 }
210}
211
212impl From<[u8; 12]> for EncryptionSalt {
213 fn from(value: [u8; 12]) -> Self {
214 Self(value.into())
215 }
216}
217
218#[derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop, Deserialize, Serialize, Clone)]
221pub struct AccessToken(String);
222impl_redacted_debug!(AccessToken);
223
224impl AccessToken {
225 pub fn try_from_string(token: impl Into<String>) -> Result<Self> {
227 let token: String = token.into();
228 if token.is_empty() {
229 return Err(TypeError::EmptyAccessToken);
230 }
231
232 Ok(Self(token))
233 }
234
235 pub fn as_str(&self) -> &str {
237 &self.0
238 }
239}
240
241impl TryFrom<String> for AccessToken {
242 type Error = TypeError;
243 fn try_from(value: String) -> Result<Self> {
244 Self::try_from_string(value)
245 }
246}
247impl TryFrom<&str> for AccessToken {
248 type Error = TypeError;
249 fn try_from(value: &str) -> Result<Self> {
250 Self::try_from_string(value)
251 }
252}
253
254#[cfg(test)]
255mod test {
256 use super::*;
257 use crate::types::newtypes::PlainPassword;
258
259 #[test]
260 fn test_debug_is_redacted() {
261 let debug = format!(
262 "{:?}",
263 PlainPassword::try_from_string("correcthorsebatterystaple").unwrap()
264 );
265 assert!(!debug.contains("correcthorsebatterystaple"));
266 }
267
268 #[test]
269 fn test_encrypt_password_success() {
270 let password = PlainPassword::try_from_string("strong_password").unwrap();
271 let pin = EncryptionPin::try_from_string("123456").unwrap();
272 let salt = EncryptionSalt::generate();
273
274 let encrypted_password = password.encrypt(&pin, &salt).unwrap();
275 assert!(!encrypted_password.0.is_empty());
276 }
277
278 #[test]
279 fn test_decrypt_password_success() {
280 let password = PlainPassword::try_from_string("strong_password").unwrap();
281 let pin = EncryptionPin::try_from_string("123456").unwrap();
282 let salt = EncryptionSalt::generate();
283
284 let encrypted_password = password.encrypt(&pin, &salt).unwrap();
285 let decrypted_password = encrypted_password.decrypt(&pin, &salt).unwrap();
286
287 assert_eq!(decrypted_password.0, password.0);
288 }
289
290 #[test]
291 fn test_encrypt_password_with_special_characters() {
292 let password = PlainPassword::try_from_string("strong_password!@#$%^&*()").unwrap();
293 let pin = EncryptionPin::try_from_string("123456").unwrap();
294 let salt = EncryptionSalt::generate();
295
296 let encrypted_password = password.encrypt(&pin, &salt).unwrap();
297 let decrypted_password = encrypted_password.decrypt(&pin, &salt).unwrap();
298
299 assert_eq!(decrypted_password.0, password.0);
300 }
301
302 #[test]
303 fn test_decrypt_password_failure_wrong_pin() {
304 let password = PlainPassword::try_from_string("strong_password").unwrap();
305 let pin = EncryptionPin::try_from_string("123456").unwrap();
306 let wrong_pin = EncryptionPin::try_from_string("654321").unwrap();
307 let salt = EncryptionSalt::generate();
308
309 let encrypted_password = password.encrypt(&pin, &salt).unwrap();
310 let decrypted_password = encrypted_password.decrypt(&wrong_pin, &salt);
311
312 decrypted_password.unwrap_err();
313 }
314
315 #[test]
316 fn test_decrypt_password_failure_invalid_data() {
317 let pin = EncryptionPin::try_from_string("123456").unwrap();
318 let salt = EncryptionSalt::generate();
319
320 let invalid_encrypted_password = unsafe { EncryptedPassword::new_unchecked(b"invalid_encrypted_password") };
322
323 let decrypted_password = invalid_encrypted_password.decrypt(&pin, &salt);
324
325 decrypted_password.unwrap_err();
326 }
327
328 #[test]
329 fn test_generate_salt() {
330 let salt = EncryptionSalt::generate();
331 assert_eq!(salt.0.len(), 12);
332 }
333
334 #[test]
335 fn test_empty_password_fails() {
336 let result = PlainPassword::try_from_string("");
337 assert!(matches!(result, Err(TypeError::EmptyPassword)));
338 }
339
340 #[test]
341 fn test_weak_password_fails() {
342 let result = PlainPassword::try_from_string("weak pass");
343 assert!(matches!(result, Err(TypeError::WeakPassword)));
344 }
345
346 #[test]
347 fn test_strong_password_succeeds() {
348 let result = PlainPassword::try_from_string("Str0ngP@ssw0rd123!");
349 result.unwrap();
350 }
351
352 #[test]
353 fn test_empty_pin_fails() {
354 let result = EncryptionPin::try_from_string("");
355 assert!(matches!(result, Err(TypeError::EmptyPin)));
356 }
357
358 #[test]
359 fn test_short_pin_fails() {
360 let result = EncryptionPin::try_from_string("123");
361 assert!(matches!(result, Err(TypeError::WeakPin)));
362 }
363
364 #[test]
365 fn test_non_numeric_pin_fails() {
366 let result = EncryptionPin::try_from_string("abc123");
367 assert!(matches!(result, Err(TypeError::NonNumericPin)));
368 }
369
370 #[test]
371 fn test_valid_pin_succeeds() {
372 let result = EncryptionPin::try_from_string("123456");
373 result.unwrap();
374 }
375}