go-spectre @main -
refs -
log -
-
https://git.jolheiser.com/go-spectre.git
Go implementation for spectre/masterpassword
Add implementation
Signed-off-by: jolheiser <john.olheiser@gmail.com>
Signature
-----BEGIN PGP SIGNATURE-----
iQIzBAABCgAdFiEEgqEQpE3xoo1QwJO/uFOtpdp7v3oFAmGabhoACgkQuFOtpdp7
v3ptRA/8C2rxLPRC7ulki+WItOdpXU7iKCLpcnJrTAJtNdx+d1ScMXzvHN5Bm62I
ORqveuBFY9KjZagqE69wvepKP99+bv5bLxdqjwHjw8TcmPQcHsbrVIRPADF+Mmnh
G8lZm8o8zYnvng9ZEMXKx9QXKwW1bYTvwy0E74i7Ebg6+/Soc9PrUkRwHQ6JH66Y
UY8QsEsBlgREnhiY4jkIZhLBSKEWfCkU0JwaS9Om4/YSiWXxrUWpRn0WhyRltGKJ
F53n+V+EvfGvmQ8T69fWRbNMvuiA3Q1NU44AZKOWhfAwd3nClh62WxeykcA4PiDr
xoMyBo3LwLLwU9EMQd/w6ez3Rd8+nA0lh7uQVUp4ZduI2TDkXYab2pKoKiKRTTLj
Vc4W7Kf9L+/Dj5g+vFFpCTg/Q1MnWL4oUyL0A6ZAuDF8plvknPmbfJgwLs69qeE4
sH7RyyLjMI/p2Hko/C/KOEcc6o4GYbUopoQW+UzH7tICmiku90XTeWdHumzp+SvD
W9HUc/q9NXb9VXHeM2fr3cFJ9WEdjeqn906dzSBwduceIeP9SgvTncxpLajVaxAy
nVb0ZyptKMjcVFbv3tlQzF7EBAYVbs72szwKwJeB/F4UBab6Pzdr4RRmhPDieTR1
jbAUAcr6vL2WNKVMbAb3aV9yql6GdfevMVt8DycFhaMFfqo/UME=
=ozv0
-----END PGP SIGNATURE-----
6 changed files, 219 additions(+), 77 deletions(-)
diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md
new file mode 100644
index 0000000000000000000000000000000000000000..bd625e5e1e8c1bef7314b7e129a170abd6482e3d
--- /dev/null
+++ b/IMPLEMENTATION.md
@@ -0,0 +1,132 @@
+## User Key Derivation
+([code](user_key.go))
+
+A user's key should be generated at "construction" time of a potential client. The user key derivation step is the
+longer part of the generation and should be separated from the cheaper site key and password generation steps.
+
+### Variables
+
+* `user_name`
+* `user_secret`
+
+### Steps
+
+1. `user_name` as byte slice
+2. `user_secret` as byte slice
+3. `key_scope` ([Authentication](#scopes)) as byte slice
+4. `key_salt` = `key_scope` + `len(user_name)` (big endian) + `user_name`
+5. `user_key` = `scrypt(password = user_secret, salt = key_salt, N = 32768, r = 8, p = 2, key_length = 64)`
+
+Store the `user_key` for use later.
+
+
+## Site Key Generation
+([code](site_key.go))
+
+The site key generation step is part of getting a password and is used against a [template](#templates).
+
+### Variables
+
+* `site_name`
+* `counter`
+* [`scope`](#scopes)
+
+### Steps
+
+1. `site_name` as byte slice
+2. `scope` as byte slice
+3. `key_salt` = `scope` + `len(site_name)` (big endian) + `site_name` + `counter` (big endian)
+4. `site_key` = `HMAC-SHA256(key = user_key, data = key_salt)`
+
+
+## Site Password Generation
+([code](site_password.go))
+
+### Variables
+
+* `site_name`
+* `counter` (default is `1`)
+* `scope` (default is [Authentication](#scopes))
+* `template_type` (default is [based on scope](#scopes))
+
+### Steps
+
+1. `site_key` as byte slice
+2. `template_set` = `templates[ template_type ]` ([templates](#templates))
+3. `template` = `template_set[ site_key[0] % len(template_set) ]`
+4. for each character `b` in the template (`loop_index` starting at `0`)
+ 1. `chars` = `characters[ b ]` ([characters](#characters))
+ 2. `char` = `chars[ site_key[ loop_index + 1 ] % len(chars) ]`
+ 3. add `char` to output string
+
+
+## Walkthrough
+
+* `user_name` = `Robert Lee Mitchell`
+* `user_secret` = `banana colored duckling`
+* `scope` = [Authentication](#scopes)
+* `counter` = `1`
+* `template` = [Long](#scopes)
+* `site_name` = `masterpasswordapp.com`
+
+### User Key
+
+```text
+[24 76 42 206 37 187 113 129 122 202 164 134 75 113 147 21 177 89 17 50 52 178 162 191 86 144 232 125 103 172 42 251 195 72 15 109 194 103 28 206 230 240 192 133 230 226 64 32 195 166 175 242 54 123 217 242 58 194 205 104 168 74 95 194]
+```
+
+### Site Key
+
+```text
+[18 27 156 216 202 205 54 139 226 53 64 140 63 35 242 105 24 249 162 30 135 30 0 50 101 141 213 27 212 150 120 210]
+```
+
+### Site Password
+
+`Jejr5[RepuSosp`
+
+## Appendix
+
+### Scopes
+([code](scope.go))
+
+|Scope|Value|Default Template|
+|:---:|:---:|:---:|
+|Authentication|`com.lyndir.masterpassword`|Long|
+|Identification|`com.lyndir.masterpaswword.login`|Name|
+|Recovery|`com.lyndir.masterpassword.answer`|Phrase|
+
+**NOTE:** One difference in my implementations compared to the original is allowing consumers to choose
+their own "scoper", some interface that can respond to the three scopes with whatever response they choose.
+**This means that any custom implementation will not return the same results, so keep that in mind when using
+a service.**
+
+### Templates
+([code](template.go))
+
+|Template|Sets|
+|:---:|:---:|
+|Maximum| `["anoxxxxxxxxxxxxxxxxx", "axxxxxxxxxxxxxxxxxno"]`|
+|Long|`["CvcvnoCvcvCvcv", "CvcvCvcvnoCvcv", "CvcvCvcvCvcvno", "CvccnoCvcvCvcv", "CvccCvcvnoCvcv", "CvccCvcvCvcvno", "CvcvnoCvccCvcv", "CvcvCvccnoCvcv", "CvcvCvccCvcvno", "CvcvnoCvcvCvcc", "CvcvCvcvnoCvcc", "CvcvCvcvCvccno", "CvccnoCvccCvcv", "CvccCvccnoCvcv", "CvccCvccCvcvno", "CvcvnoCvccCvcc", "CvcvCvccnoCvcc", "CvcvCvccCvccno", "CvccnoCvcvCvcc", "CvccCvcvnoCvcc", "CvccCvcvCvccno"]`|
+|Medium|`["CvcnoCvc", "CvcCvcno"]`|
+|Short|`["Cvcn"]`|
+|Pin|`["nnnn"]`|
+|Name|`["cvccvcvcv"]`|
+|Phrase|`["cvcc cvc cvccvcv cvc", "cvc cvccvcvcv cvcv", "cv cvccv cvc cvcvccv"]`|
+|Basic|`["aaanaaan", "aannaaan", "aaannaaa"]`|
+
+### Characters
+([code](template.go))
+
+|Symbol|Set|
+|:---:|:---:|
+|V|`AEIOU`|
+|C|`BCDFGHJKLMNPQRSTVWXYZ`|
+|v|`aeiou`|
+|c|`bcdfghjklmnpqrstvwxyz`|
+|A|`AEIOUBCDFGHJKLMNPQRSTVWXYZ`|
+|a|`AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz`|
+|n|`0123456789`|
+|o|`@&%?,=[]_:-+*$#!'^~;()/.`|
+|x|`AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz0123456789!@#$%^&*()`|
+|(literal space)|` `|
diff --git a/README.md b/README.md
index 0e3437e84aed6508394c61ee93feca3c9987e8d8..fee5f08e4410268091fa512dcc7e8b6b790dccd5 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,10 @@ It also passes the JS [sanity check](https://gitlab.com/spectre.app/www/-/blob/306704b129a2c43544af202b8b6fb5c7e665ce66/assets/js/mpw-js/mpw.js#L205).
This is because I've only implemented v3 of the algorithm and the main pieces.
+[Implementation breakdown](IMPLEMENTATION.md)
+
+Generally code wouldn't split across files this liberally. It's done here for ease of reference.
+
## License
[GPLv3](LICENSE) - same as the algorithm author
\ No newline at end of file
diff --git a/site_key.go b/site_key.go
new file mode 100644
index 0000000000000000000000000000000000000000..0ff733e1980665d960817b36981ce52fc06bf003
--- /dev/null
+++ b/site_key.go
@@ -0,0 +1,31 @@
+package spectre
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+)
+
+func siteKey(userKey []byte, scoper Scoper, siteName string, counter int, scope Scope) []byte {
+ nameBytes := []byte(siteName)
+ scopeBytes := []byte(scoper.Scope(scope))
+
+ nameBytesLen := len(nameBytes)
+ keySalt := append(scopeBytes,
+ byte(nameBytesLen>>24),
+ byte(nameBytesLen>>16),
+ byte(nameBytesLen>>8),
+ byte(nameBytesLen),
+ )
+ keySalt = append(keySalt, nameBytes...)
+ keySalt = append(keySalt,
+ byte(counter>>24),
+ byte(counter>>16),
+ byte(counter>>8),
+ byte(counter),
+ )
+
+ sign := hmac.New(sha256.New, userKey)
+ sign.Write(keySalt)
+
+ return sign.Sum(nil)
+}
diff --git a/site_password.go b/site_password.go
new file mode 100644
index 0000000000000000000000000000000000000000..fbf3676aa54e874f06cfecf45c5db496cdc07083
--- /dev/null
+++ b/site_password.go
@@ -0,0 +1,31 @@
+package spectre
+
+import "strings"
+
+func site(userKey []byte, scoper Scoper, siteName string, opts ...SiteOption) string {
+ siteOpts := &options{
+ template: "",
+ counter: 1,
+ scope: Authentication,
+ }
+ for _, opt := range opts {
+ opt(siteOpts)
+ }
+
+ if siteOpts.template == "" {
+ siteOpts.template = siteOpts.scope.DefaultTemplate()
+ }
+
+ siteKey := siteKey(userKey, scoper, siteName, siteOpts.counter, siteOpts.scope)
+
+ templateSet := templates[siteOpts.template]
+ template := templateSet[int(siteKey[0])%len(templateSet)]
+
+ var out strings.Builder
+ for idx, b := range template {
+ chars := characters[string(b)]
+ char := chars[int(siteKey[idx+1])%len(chars)]
+ out.WriteByte(char)
+ }
+ return out.String()
+}
diff --git a/spectre.go b/spectre.go
index 4f5ba3d5e7c8fb5e803c12093d7390c9ce25f5e1..b640fbf763656b6d73d3ee5ba25a06f45c93f7cc 100644
--- a/spectre.go
+++ b/spectre.go
@@ -1,12 +1,5 @@
package spectre
-import (
- "crypto/hmac"
- "crypto/sha256"
- "golang.org/x/crypto/scrypt"
- "strings"
-)
-
// Spectre is a spectre client
type Spectre struct {
name string
@@ -26,8 +19,8 @@ }
for _, opt := range opts {
opt(s)
}
+// Spectre is a spectre client
- "crypto/sha256"
return
}
@@ -41,81 +34,12 @@ s.scoper = scoper
}
}
-func (s *Spectre) userKey() ([]byte, error) {
- nameBytes := []byte(s.name)
-import (
"crypto/sha256"
- keyScope := []byte(s.scoper.Scope(Authentication))
-
- nameBytesLen := len(nameBytes)
-import (
)
- byte(nameBytesLen>>24),
- byte(nameBytesLen>>16),
- byte(nameBytesLen>>8),
- byte(nameBytesLen),
- )
- keySalt = append(keySalt, nameBytes...)
-
- "crypto/hmac"
"crypto/sha256"
-}
-
-func (s *Spectre) siteKey(name string, counter int, scope Scope) []byte {
- nameBytes := []byte(name)
- scopeBytes := []byte(s.scoper.Scope(scope))
-
- nameBytesLen := len(nameBytes)
- "crypto/hmac"
// Spectre is a spectre client
-import (
// Spectre is a spectre client
- byte(nameBytesLen>>16),
- byte(nameBytesLen>>8),
- byte(nameBytesLen),
- )
- keySalt = append(keySalt, nameBytes...)
- keySalt = append(keySalt,
- byte(counter>>24),
- byte(counter>>16),
- "crypto/sha256"
import (
- byte(counter),
- )
-
- sign := hmac.New(sha256.New, s.key)
- sign.Write(keySalt)
-
- return sign.Sum(nil)
-}
-
-// Site returns a site password based on Options
-func (s *Spectre) Site(siteName string, opts ...SiteOption) string {
- siteOpts := &options{
- template: "",
- counter: 1,
- scope: Authentication,
- }
- for _, opt := range opts {
- opt(siteOpts)
- }
-
- if siteOpts.template == "" {
- siteOpts.template = siteOpts.scope.DefaultTemplate()
- }
-
- siteKey := s.siteKey(siteName, siteOpts.counter, siteOpts.scope)
-
- templateSet := templates[siteOpts.template]
- template := templateSet[int(siteKey[0])%len(templateSet)]
-
- var out strings.Builder
- for idx, b := range template {
- chars := characters[string(b)]
- char := chars[int(siteKey[idx+1])%len(chars)]
- out.WriteByte(char)
- }
- return out.String()
}
type options struct {
diff --git a/user_key.go b/user_key.go
new file mode 100644
index 0000000000000000000000000000000000000000..2660238a3f1ca6b9264d06fd3224598cdd64bbe5
--- /dev/null
+++ b/user_key.go
@@ -0,0 +1,20 @@
+package spectre
+
+import "golang.org/x/crypto/scrypt"
+
+func userKey(name, secret string, scoper Scoper) ([]byte, error) {
+ nameBytes := []byte(name)
+ secretBytes := []byte(secret)
+ keyScope := []byte(scoper.Scope(Authentication))
+
+ nameBytesLen := len(nameBytes)
+ keySalt := append(keyScope,
+ byte(nameBytesLen>>24),
+ byte(nameBytesLen>>16),
+ byte(nameBytesLen>>8),
+ byte(nameBytesLen),
+ )
+ keySalt = append(keySalt, nameBytes...)
+
+ return scrypt.Key(secretBytes, keySalt, 32768, 8, 2, 64)
+}