Home

rs-spectre @main - refs - log -
-
https://git.jolheiser.com/rs-spectre.git
Rust implementation for Spectre/Masterpassword
tree log patch
add docs and clean up some code Signed-off-by: jolheiser <git@jolheiser.com>
Signature
-----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgBTEvCQk6VqUAdN2RuH6bj1dNkY oOpbPWj+jw4ua1B1cAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQFNXO7amca/FnzovkfxRfqH4082vZ065185yYl6KPHAIAnKs6Fq1XCwlvU4Hbi40o/ OhINSSfPL678jbfZRs7A0= -----END SSH SIGNATURE-----
jolheiser <git@jolheiser.com>
1 week ago
4 changed files, 95 additions(+), 69 deletions(-)
spectre-cli/src/main.rsspectre/src/lib.rsspectre/src/scope.rsspectre/src/template.rs
M spectre-cli/src/main.rsspectre-cli/src/main.rs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
diff --git a/spectre-cli/src/main.rs b/spectre-cli/src/main.rs
index b88aef01f17e23658b2ad5190adecff70fe535a5..8059b65303152c1c4cae332574a25fbc557d12d9 100644
--- a/spectre-cli/src/main.rs
+++ b/spectre-cli/src/main.rs
@@ -137,9 +137,16 @@     if args.print {
         println!("{pw}");
         return;
     }
-    let mut clipboard = Clipboard::new().unwrap();
-    clipboard.set_text(pw).unwrap();
-    println!("Password copied to clipboard")
+    match copy_to_clipboard(pw) {
+        Ok(_) => println!("Password copied to clipboard"),
+        Err(e) => println!("Failed to copy password: {e}")
+    }
+}
+
+fn copy_to_clipboard(text: String) -> Result<(), Box<dyn std::error::Error>> {
+    let mut clipboard = Clipboard::new()?;
+    clipboard.set_text(text)?;
+    Ok(())
 }
 
 #[cfg(test)]
M spectre/src/lib.rsspectre/src/lib.rs
  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
diff --git a/spectre/src/lib.rs b/spectre/src/lib.rs
index 94ed900433b905ea83e3b8d4adbf7c2c1c5333e4..90b74f304bd8fc6033ded69f6b7bd383b0e5b1cd 100644
--- a/spectre/src/lib.rs
+++ b/spectre/src/lib.rs
@@ -7,6 +7,20 @@
 pub mod scope;
 pub mod template;
 
+/// Spectre implements the Spectre/Masterpassword cipher
+/// 
+/// # Examples
+/// 
+/// ```
+/// use spectre::{scope, SiteOptions, Spectre};
+/// let s = Spectre::new(
+///     "Robert Lee Mitchell",
+///     "banana colored duckling",
+///     scope::SimpleScoper::default(),
+///  );
+///  let pw = s.site("masterpasswordapp.com", SiteOptions::default());
+///  assert_eq!(pw, "Jejr5[RepuSosp")
+/// ```
 #[derive(Debug)]
 pub struct Spectre {
     key: String,
@@ -14,11 +28,12 @@     scoper: Box<dyn scope::Scoper>,
 }
 
 impl Spectre {
-    pub fn new(name: String, secret: String, scoper: impl scope::Scoper + 'static) -> Self {
-        let key = user_key(&name, &secret, &scoper);
+    /// Create a new Spectre instance that can be used to generate site passwords.
+    pub fn new(name: impl Into<String>, secret: impl Into<String>, scoper: impl scope::Scoper + 'static) -> Self {
+        let key = user_key(name, secret, &scoper);
         Self {
             scoper: Box::new(scoper),
-            key: key,
+            key,
         }
     }
 
@@ -36,16 +51,19 @@         let result = hmac.finalize();
         result.into_bytes().to_vec()
     }
 
-    pub fn site(&self, site_name: String, opts: SiteOptions) -> String {
+    /// Generate a site password for a given site_name 
+    /// 
+    /// Generally this is the domain, e.g. `example.com`
+    pub fn site(&self, site_name: impl Into<String>, opts: SiteOptions) -> String {
         let scope = opts.scope.unwrap_or(scope::Scope::AUTHENTICATION);
         let template_type = opts.template.unwrap_or(scope.default_template());
         let templates = template::templates(template_type);
         let counter = opts.counter.unwrap_or(1);
-        let site_key = self.site_key(site_name, counter, scope);
+        let site_key = self.site_key(site_name.into(), counter, scope);
         let site_byte = site_key[0] as u32;
         let templates_len = templates.len() as u32;
         let template_idx = site_byte % templates_len;
-        let template = templates[template_idx as usize].clone();
+        let template = templates[template_idx as usize];
         let mut out = String::new();
         for (i, b) in template.as_bytes().iter().enumerate() {
             let chars = template::chars(b).to_string();
@@ -66,7 +84,8 @@     pub template: Option<template::Template>,
     pub counter: Option<u32>,
 }
 
-fn user_key(name: &str, secret: &str, scoper: &impl scope::Scoper) -> String {
+fn user_key(name: impl Into<String>, secret: impl Into<String>, scoper: &impl scope::Scoper) -> String {
+    let name = name.into();
     let key = scoper.scope(scope::Scope::AUTHENTICATION);
     let out: &mut [u8] = &mut [0u8; 64];
     let mut salt = Vec::new();
@@ -74,7 +93,7 @@     salt.extend_from_slice(key.as_bytes());
     salt.extend_from_slice(&(name.len() as u32).to_be_bytes());
     salt.extend_from_slice(name.as_bytes());
     scrypt(
-        secret.as_bytes(),
+        secret.into().as_bytes(),
         &salt,
         &Params::new(15, 8, 2, 64).unwrap(),
         out,
@@ -87,31 +106,22 @@ #[cfg(test)]
 mod spectre_tests {
     use super::*;
     use serde::Deserialize;
-
-    #[test]
-    fn website_sanity() {
-        let s = Spectre::new(
-            "Robert Lee Mitchell".to_string(),
-            "banana colored duckling".to_string(),
-            scope::SimpleScoper::default(),
-        );
-        let pw = s.site("masterpasswordapp.com".to_string(), SiteOptions::default());
-        assert_eq!(pw, "Jejr5[RepuSosp")
-    }
+    use crate::scope::{Scope, SimpleScoper};
+    use crate::template::Template;
 
     #[test]
     fn custom() {
-        let scoper = scope::SimpleScoper::new("com.jojodev.jolheiser".to_string());
+        let scoper = SimpleScoper::new("com.jojodev.jolheiser");
         let s = Spectre::new(
-            "Robert Lee Mitchell".to_string(),
-            "banana colored duckling".to_string(),
+            "Robert Lee Mitchell",
+            "banana colored duckling",
             scoper,
         );
         let pw = s.site(
-            "jojodev.com".to_string(),
+            "jojodev.com",
             SiteOptions {
-                scope: Some(scope::Scope::IDENTIFICATION),
-                template: Some(template::Template::Maximum),
+                scope: Some(Scope::IDENTIFICATION),
+                template: Some(Template::Maximum),
                 counter: Some(2),
             },
         );
@@ -169,13 +179,13 @@                 SiteOptions {
                     scope: Some(
                         case.key_purpose
                             .unwrap_or("Authentication".to_string())
-                            .parse::<scope::Scope>()
+                            .parse::<Scope>()
                             .unwrap(),
                     ),
                     template: Some(
                         case.result_type
                             .unwrap_or("Long".to_string())
-                            .parse::<template::Template>()
+                            .parse::<Template>()
                             .unwrap(),
                     ),
                     counter: Some(case.key_counter.unwrap_or(1)),
M spectre/src/scope.rsspectre/src/scope.rs
 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
diff --git a/spectre/src/scope.rs b/spectre/src/scope.rs
index 11a3334ffc984303dc448715f08e8fdc95c8de7f..665660cf55627d97331d3fe172dabc1ee9b23fa0 100644
--- a/spectre/src/scope.rs
+++ b/spectre/src/scope.rs
@@ -1,5 +1,6 @@
 use crate::template;
 
+/// The scope of the generated secret
 #[derive(Debug)]
 pub enum Scope {
     AUTHENTICATION,
@@ -8,6 +9,7 @@     RECOVERY,
 }
 
 impl Scope {
+    /// The default [`template::Template`] for the given scope
     pub fn default_template(&self) -> template::Template {
         match self {
             Scope::AUTHENTICATION => template::Template::Long,
@@ -17,6 +19,7 @@         }
     }
 }
 
+/// Any error from parsing a Scope from a string
 #[derive(Debug)]
 pub enum ParseScopeError {
     Invalid,
@@ -35,26 +38,28 @@         }
     }
 }
 
+/// A custom scoper simply needs to return an ideally unique string per-scope
 pub trait Scoper: std::fmt::Debug {
     fn scope(&self, s: Scope) -> String;
 }
 
+/// A generic [`Scoper`] that returns a key with suffix depending on the scope
 #[derive(Debug)]
 pub struct SimpleScoper {
     key: String,
 }
 
 impl SimpleScoper {
-    pub fn new(key: String) -> Self {
-        Self { key: key }
+    /// A new SimpleScoper with a single key
+    pub fn new(key: impl Into<String>) -> Self {
+        Self { key: key.into() }
     }
 }
 
 impl Default for SimpleScoper {
+    /// The default SimpleScoper uses the original masterpassword key
     fn default() -> Self {
-        Self {
-            key: String::from("com.lyndir.masterpassword"),
-        }
+        Self::new("com.lyndir.masterpassword")
     }
 }
 
M spectre/src/template.rsspectre/src/template.rs
  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
diff --git a/spectre/src/template.rs b/spectre/src/template.rs
index b68d429f5a3cc3534be604b162b65cfe4de4330a..9ecac4cf57373d95f1e91c5e39ac121736e5f04a 100644
--- a/spectre/src/template.rs
+++ b/spectre/src/template.rs
@@ -1,3 +1,4 @@
+/// The template to use for secret generation, patterns are described in [`templates`]
 #[derive(Debug)]
 pub enum Template {
     Maximum,
@@ -10,6 +11,7 @@     Phrase,
     Basic,
 }
 
+/// Any error from parsing a Template from a string
 #[derive(Debug)]
 pub enum ParseTemplateError {
     Invalid,
@@ -33,52 +35,54 @@         }
     }
 }
 
-pub(crate) fn templates(t: Template) -> Vec<String> {
+/// The list of templates for Spectre generation, the mapping for each char is found in [`chars`]
+pub(crate) fn templates<'s>(t: Template) -> Vec<&'s str> {
     match t {
         Template::Maximum => vec![
-            "anoxxxxxxxxxxxxxxxxx".to_string(),
-            "axxxxxxxxxxxxxxxxxno".to_string(),
+            "anoxxxxxxxxxxxxxxxxx",
+            "axxxxxxxxxxxxxxxxxno",
         ],
         Template::Long => vec![
-            "CvcvnoCvcvCvcv".to_string(),
-            "CvcvCvcvnoCvcv".to_string(),
-            "CvcvCvcvCvcvno".to_string(),
-            "CvccnoCvcvCvcv".to_string(),
-            "CvccCvcvnoCvcv".to_string(),
-            "CvccCvcvCvcvno".to_string(),
-            "CvcvnoCvccCvcv".to_string(),
-            "CvcvCvccnoCvcv".to_string(),
-            "CvcvCvccCvcvno".to_string(),
-            "CvcvnoCvcvCvcc".to_string(),
-            "CvcvCvcvnoCvcc".to_string(),
-            "CvcvCvcvCvccno".to_string(),
-            "CvccnoCvccCvcv".to_string(),
-            "CvccCvccnoCvcv".to_string(),
-            "CvccCvccCvcvno".to_string(),
-            "CvcvnoCvccCvcc".to_string(),
-            "CvcvCvccnoCvcc".to_string(),
-            "CvcvCvccCvccno".to_string(),
-            "CvccnoCvcvCvcc".to_string(),
-            "CvccCvcvnoCvcc".to_string(),
-            "CvccCvcvCvccno".to_string(),
+            "CvcvnoCvcvCvcv",
+            "CvcvCvcvnoCvcv",
+            "CvcvCvcvCvcvno",
+            "CvccnoCvcvCvcv",
+            "CvccCvcvnoCvcv",
+            "CvccCvcvCvcvno",
+            "CvcvnoCvccCvcv",
+            "CvcvCvccnoCvcv",
+            "CvcvCvccCvcvno",
+            "CvcvnoCvcvCvcc",
+            "CvcvCvcvnoCvcc",
+            "CvcvCvcvCvccno",
+            "CvccnoCvccCvcv",
+            "CvccCvccnoCvcv",
+            "CvccCvccCvcvno",
+            "CvcvnoCvccCvcc",
+            "CvcvCvccnoCvcc",
+            "CvcvCvccCvccno",
+            "CvccnoCvcvCvcc",
+            "CvccCvcvnoCvcc",
+            "CvccCvcvCvccno",
         ],
-        Template::Medium => vec!["CvcnoCvc".to_string(), "CvcCvcno".to_string()],
-        Template::Short => vec!["Cvcn".to_string()],
-        Template::Pin => vec!["nnnn".to_string()],
-        Template::Name => vec!["cvccvcvcv".to_string()],
+        Template::Medium => vec!["CvcnoCvc", "CvcCvcno"],
+        Template::Short => vec!["Cvcn"],
+        Template::Pin => vec!["nnnn"],
+        Template::Name => vec!["cvccvcvcv"],
         Template::Phrase => vec![
-            "cvcc cvc cvccvcv cvc".to_string(),
-            "cvc cvccvcvcv cvcv".to_string(),
-            "cv cvccv cvc cvcvccv".to_string(),
+            "cvcc cvc cvccvcv cvc",
+            "cvc cvccvcvcv cvcv",
+            "cv cvccv cvc cvcvccv",
         ],
         Template::Basic => vec![
-            "aaanaaan".to_string(),
-            "aannaaan".to_string(),
-            "aaannaaa".to_string(),
+            "aaanaaan",
+            "aannaaan",
+            "aaannaaa",
         ],
     }
 }
 
+/// A mapping of each template char to a set of possible characters
 pub(crate) fn chars(u: &u8) -> &str {
     match *u as char {
         'V' => "AEIOU",