signstar_crypto/
passphrase.rs1use std::{fmt::Display, fs::read_to_string, path::Path, str::FromStr};
4
5use rand::{Rng, distributions::Alphanumeric, thread_rng};
6use secrecy::{ExposeSecret, SecretString};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, thiserror::Error)]
11pub enum Error {
12 #[error("Unable to convert string to passphrase")]
14 Passphrase,
15
16 #[error(
18 "The passphrase should be at least {required_length} characters long, but is only {length} characters long."
19 )]
20 Length {
21 length: usize,
23
24 required_length: usize,
26 },
27}
28
29#[derive(Clone, Debug)]
33pub struct PassphrasePolicy {
34 pub minimum_length: usize,
36}
37
38impl Default for PassphrasePolicy {
39 fn default() -> Self {
40 Self {
41 minimum_length: Passphrase::DEFAULT_LENGTH,
42 }
43 }
44}
45
46#[derive(Clone, Debug, Default, Deserialize)]
51pub struct Passphrase(SecretString);
52
53impl Passphrase {
54 pub const DEFAULT_LENGTH: usize = 30;
56
57 pub fn new(passphrase: String) -> Self {
66 Self(SecretString::new(passphrase.into()))
67 }
68
69 pub fn new_with_policy(
95 passphrase: String,
96 policy: &PassphrasePolicy,
97 ) -> Result<Self, crate::Error> {
98 let passphrase = Self::new(passphrase);
99 passphrase.check_against_policy(policy)?;
100
101 Ok(passphrase)
102 }
103
104 pub fn generate(length: Option<usize>) -> Self {
119 let length = {
120 let mut length = length.unwrap_or(Self::DEFAULT_LENGTH);
121 if length < Self::DEFAULT_LENGTH {
122 length = Self::DEFAULT_LENGTH
123 }
124 length
125 };
126
127 Self::new(
128 thread_rng()
129 .sample_iter(&Alphanumeric)
130 .take(length)
131 .map(char::from)
132 .collect(),
133 )
134 }
135
136 pub fn check_against_policy(&self, policy: &PassphrasePolicy) -> Result<(), crate::Error> {
171 if self.len() < policy.minimum_length {
172 return Err(Error::Length {
173 length: self.len(),
174 required_length: policy.minimum_length,
175 }
176 .into());
177 }
178
179 Ok(())
180 }
181
182 pub fn expose_owned(&self) -> String {
184 self.0.expose_secret().to_owned()
185 }
186
187 pub fn expose_borrowed(&self) -> &str {
189 self.0.expose_secret()
190 }
191
192 pub fn len(&self) -> usize {
194 self.expose_borrowed().len()
195 }
196
197 pub fn is_empty(&self) -> bool {
199 self.expose_borrowed().is_empty()
200 }
201}
202
203impl Display for Passphrase {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 write!(f, "[REDACTED]")
206 }
207}
208
209impl FromStr for Passphrase {
210 type Err = crate::Error;
211
212 fn from_str(s: &str) -> Result<Self, Self::Err> {
213 Ok(Self(SecretString::from(s.to_string())))
214 }
215}
216
217impl Serialize for Passphrase {
218 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
225 where
226 S: serde::Serializer,
227 {
228 self.0.expose_secret().serialize(serializer)
229 }
230}
231
232impl TryFrom<&Path> for Passphrase {
233 type Error = crate::Error;
234
235 fn try_from(path: &Path) -> Result<Self, Self::Error> {
242 Passphrase::from_str(
243 &read_to_string(path).map_err(|source| crate::Error::IoPath {
244 path: path.to_path_buf(),
245 context: "reading a passphrase from the file",
246 source,
247 })?,
248 )
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use std::io::Write;
255
256 use rstest::rstest;
257 use tempfile::{NamedTempFile, TempDir};
258 use testresult::TestResult;
259
260 use super::*;
261
262 #[test]
263 fn passphrase_display() -> TestResult {
264 let passphrase = Passphrase::new("a-secret-passphrase".to_string());
265 assert_eq!(format!("{passphrase}"), "[REDACTED]");
266 Ok(())
267 }
268
269 #[rstest]
270 #[case::too_short_use_default(Some(20), 30)]
271 #[case::none_use_default(None, 30)]
272 #[case::longer_than_default(Some(31), 31)]
273 fn passphrase_generate(#[case] input_length: Option<usize>, #[case] output_length: usize) {
274 let passphrase = Passphrase::generate(input_length);
275 assert_eq!(passphrase.expose_borrowed().len(), output_length);
276 }
277
278 #[test]
280 fn passphrase_try_from_path_succeeds() -> TestResult {
281 let temp_file = {
282 let mut temp_file = NamedTempFile::new()?;
283 temp_file.write_all("passphrase".as_bytes())?;
284 temp_file
285 };
286 let _passphrase = Passphrase::try_from(temp_file.path())?;
287
288 Ok(())
289 }
290
291 #[test]
293 fn passphrase_try_from_path_fails_on_path_is_dir() -> TestResult {
294 let temp_file = TempDir::new()?;
295 assert!(Passphrase::try_from(temp_file.path()).is_err());
296
297 Ok(())
298 }
299
300 #[rstest]
301 #[case::with_len(Passphrase::new("foo".to_string()), 3)]
302 #[case::empty(Passphrase::new("".to_string()), 0)]
303 fn passphrase_len(#[case] passphrase: Passphrase, #[case] len: usize) {
304 assert_eq!(passphrase.len(), len);
305 }
306
307 #[rstest]
308 #[case::with_len(Passphrase::new("foo".to_string()), false)]
309 #[case::empty(Passphrase::new("".to_string()), true)]
310 fn passphrase_is_empty(#[case] passphrase: Passphrase, #[case] is_empty: bool) {
311 assert_eq!(passphrase.is_empty(), is_empty);
312 }
313
314 #[rstest]
315 #[case::empty_policy_allows_empty("", PassphrasePolicy { minimum_length: 0 })]
316 #[case::longer_than_minimum_requirement("foobar", PassphrasePolicy { minimum_length: 3 })]
317 fn passphrase_new_with_policy_succeeds(
318 #[case] passphrase: &str,
319 #[case] policy: PassphrasePolicy,
320 ) -> TestResult {
321 match Passphrase::new_with_policy(passphrase.to_string(), &policy) {
322 Ok(_) => {}
323 Err(error) => panic!(
324 "Expected to successfully create a passphrase from input \"{passphrase}\" and policy {policy:?}, but got error: {error}"
325 ),
326 }
327
328 Ok(())
329 }
330
331 #[rstest]
332 #[case::empty_policy_allows_one_char("", PassphrasePolicy { minimum_length: 1 })]
333 #[case::shorter_than_minimum_requirement("foobar", PassphrasePolicy::default())]
334 fn passphrase_new_with_policy_fails_on_short_passphrase(
335 #[case] passphrase: &str,
336 #[case] policy: PassphrasePolicy,
337 ) -> TestResult {
338 match Passphrase::new_with_policy(passphrase.to_string(), &policy) {
339 Ok(_) => panic!("Expected to fail with an error, but succeeded instead."),
340 Err(crate::Error::Passphrase(Error::Length { .. })) => {}
341 Err(error) => panic!(
342 "Expected to fail with Error::Length, but failed with different error: {error}"
343 ),
344 }
345
346 Ok(())
347 }
348
349 #[test]
350 fn passphrase_check_against_policy() -> TestResult {
351 let passphrase = Passphrase::new("passphrase".to_string());
352 let policy = PassphrasePolicy { minimum_length: 10 };
353
354 match passphrase.check_against_policy(&policy) {
355 Ok(_) => {}
356 Err(error) => panic!(
357 "Expected to successfully check the passphrase against the policy {policy:?}, but got error: {error}"
358 ),
359 }
360
361 let policy = PassphrasePolicy::default();
362 match passphrase.check_against_policy(&policy) {
363 Ok(_) => panic!("Expected to fail with an error, but succeeded instead."),
364 Err(crate::Error::Passphrase(Error::Length { .. })) => {}
365 Err(error) => panic!(
366 "Expected to fail with Error::Length, but failed with different error: {error}"
367 ),
368 }
369
370 Ok(())
371 }
372}