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