-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpassword.rs
More file actions
185 lines (161 loc) · 5.82 KB
/
Copy pathpassword.rs
File metadata and controls
185 lines (161 loc) · 5.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
//! Password secret wrapper type
use secrecy::{ExposeSecret as SecrecyExposeSecret, SecretString};
use serde::de::{Deserialize, Deserializer};
use serde::ser::{Serialize, Serializer};
use std::fmt;
/// Plain password used in DTOs for serialization/deserialization.
///
/// This is intentionally a `String` type alias to mark places where passwords
/// are handled in plain text during configuration file I/O at the application layer.
/// Convert to the secure `Password` type at the DTO-to-domain boundary using `Password::from()`.
///
/// # Lifecycle
///
/// ```text
/// User Input (JSON) → PlainPassword (DTO) → Password (Domain) → PlainPassword (JSON Output)
/// ```
pub type PlainPassword = String;
/// A wrapper type for passwords that prevents accidental exposure.
///
/// Internally uses `SecretString` from the `secrecy` crate for memory zeroing
/// on drop and debug redaction. Adds serialization support by exposing the
/// secret during serialization operations.
#[derive(Clone)]
pub struct Password(SecretString);
impl Password {
/// Creates a new password from any type that can be converted into a String.
///
/// # Examples
/// ```
/// use torrust_tracker_deployer_lib::shared::secrets::Password;
///
/// let from_string = Password::new(String::from("password"));
/// let from_str = Password::new("password");
/// ```
#[must_use]
pub fn new(password: impl Into<String>) -> Self {
Self(SecretString::from(password.into()))
}
/// Exposes the secret password value.
///
/// This method should be used carefully as it provides access to the sensitive data.
///
/// # Debug Tracing
///
/// In debug builds, this method logs the caller location to help audit secret access patterns.
/// No performance impact in release builds.
#[must_use]
#[track_caller]
pub fn expose_secret(&self) -> &str {
#[cfg(debug_assertions)]
tracing::trace!(
location = ?std::panic::Location::caller(),
"Secret password exposed"
);
self.0.expose_secret()
}
}
impl fmt::Debug for Password {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Delegate to SecretString's Debug impl which shows "Secret([REDACTED])"
f.debug_tuple("Password").field(&self.0).finish()
}
}
impl PartialEq for Password {
fn eq(&self, other: &Self) -> bool {
self.expose_secret() == other.expose_secret()
}
}
impl Eq for Password {}
impl Serialize for Password {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Deliberately expose secret during serialization for storage
self.expose_secret().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Password {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(Self::new(s))
}
}
impl From<String> for Password {
fn from(password: String) -> Self {
Self::new(password)
}
}
impl From<&str> for Password {
fn from(password: &str) -> Self {
Self::new(password)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_create_password_from_string() {
let password = Password::new("test-password".to_string());
assert_eq!(password.expose_secret(), "test-password");
}
#[test]
fn it_should_create_password_from_str_reference() {
let password = Password::from("test-password");
assert_eq!(password.expose_secret(), "test-password");
}
#[test]
fn it_should_redact_password_in_debug_output() {
let password = Password::new("secret-pass".to_string());
let debug_output = format!("{password:?}");
assert!(debug_output.contains("Secret"));
assert!(!debug_output.contains("secret-pass"));
}
#[test]
fn it_should_clone_password() {
let password = Password::new("test-password".to_string());
let cloned = password.clone();
assert_eq!(password.expose_secret(), cloned.expose_secret());
}
#[test]
fn it_should_compare_passwords_for_equality() {
let password1 = Password::new("pass".to_string());
let password2 = Password::new("pass".to_string());
let password3 = Password::new("different".to_string());
assert_eq!(password1, password2);
assert_ne!(password1, password3);
}
#[test]
fn it_should_serialize_and_deserialize_password() {
let original = Password::new("test-password".to_string());
let serialized = serde_json::to_string(&original).unwrap();
let deserialized: Password = serde_json::from_str(&serialized).unwrap();
assert_eq!(original.expose_secret(), deserialized.expose_secret());
}
#[test]
fn it_should_support_multiple_input_types_for_password() {
let from_string = Password::from("password".to_string());
let from_str = Password::from("password");
assert_eq!(from_string, from_str);
}
#[test]
fn it_should_accept_flexible_constructor_inputs_for_password() {
// Test that new() accepts anything convertible to String
let _from_string_type = Password::new(String::from("password"));
let _from_str_slice = Password::new("password");
let _from_owned_string = Password::new("password".to_string());
}
#[test]
#[cfg(debug_assertions)]
fn it_should_trace_secret_exposure_in_debug_builds() {
// This test verifies that tracing is compiled and callable in debug builds.
// The actual trace output would require a tracing subscriber to capture.
let password = Password::new("debug-password");
let _exposed = password.expose_secret();
// If this compiles and runs without panic, tracing is working
}
}