go-spectre @main -
refs -
log -
-
https://git.jolheiser.com/go-spectre.git
Go implementation for spectre/masterpassword
Add CLI and some cleanup
Signed-off-by: jolheiser <john.olheiser@gmail.com>
Signature
-----BEGIN PGP SIGNATURE-----
iQIzBAABCgAdFiEEgqEQpE3xoo1QwJO/uFOtpdp7v3oFAmGLRRUACgkQuFOtpdp7
v3oXJQ//aM3RCqO4kU1G6ofkD5kkFrxnYSsq1Q/9OdLxdf1SEO9LsqcQApkqjiA/
82UldB0udF4RadzoqYDbE3HEo5e7FzLPxC0MTV2a5RHVf4OXlPgSNeptNn06uqGT
W8iovsKdLgPv49c7ZZVMAAAvFxIA3K2QHTXDFa1SoDRMY/3VQX0HBD9QkxLieDKl
W8JLE5+uXB+Z/ahtAz+qwgRpGtjm7Rp8IzsbsL26nuS7ITfJ0ojisz5UQ7V05XSi
kIKVpg2IrsNoietIQx0QfL3l74Hd5q6NDw/c+8FXdsBNHFYkyTuH10ubgfqR5kHU
qWTYSpox7cTI1ckQIEeLt8OygSiMc0xtz70aEc35hnP2sA/jI4IMlWAFJ2kSYgz7
npY2AKglLUtsCFFskjzRa9/m4iKOAIfKxGx7iWPFF1TImZ63RqCeDURq6wPmzZv+
WlUaarotYwIsOiULwp/qZwdUjrBgY0RuzEiPLsTTgHzCil2LXauSHtQgnZRqdlc5
nca/EOR2vKdsutm/AvaAbcTh1PQV0AFlYGQbEnxdU3t+6ayJJmTZva8oFdkbGc2a
xzRChHV9ov02vWVSvdV7+QqHAI4lqJyXL/DOlDtEjOKujDj7tEP8/iVwnqYY+iZs
yeZ0kFJ8aS4Mxn0Atw631Od2INq7dECvXI58WNrV+lh0fA+Fbj4=
=bC10
-----END PGP SIGNATURE-----
9 changed files, 238 additions(+), 21 deletions(-)
diff --git a/.gitignore b/.gitignore
index 62c893550adb53d3a8fc29a1584ff831cb829062..08dd7e0d4d56c7221f034ade6ff6a5eb9e9ba4c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
-.idea/
\ No newline at end of file
+.idea/
+/spectre
+/spectre.exe
\ No newline at end of file
diff --git a/.woodpecker.yml b/.woodpecker.yml
new file mode 100644
index 0000000000000000000000000000000000000000..72e6341bb53bc67e052700c2a37851c4408fc8ef
--- /dev/null
+++ b/.woodpecker.yml
@@ -0,0 +1,58 @@
+clone:
+ git:
+ image: woodpeckerci/plugin-git:next
+
+pipeline:
+ compliance:
+ image: golang:1.17
+ commands:
+ - go test -race ./...
+ - go vet ./...
+ - go build
+ when:
+ event: pull_request
+
+ build:
+ image: golang:1.17
+ commands:
+ - GOOS="windows" go build ./cmd/spectre
+ - GOOS="linux" go build ./cmd/spectre
+ when:
+ event: [ push, tag ]
+ branch: main
+
+ release-main:
+ image: jolheiser/drone-gitea-main:latest
+ secrets:
+ - source: gitea_token
+ target: plugin_token
+ base: https://git.jojodev.com
+ files:
+ - "spectre"
+ - "spectre.exe"
+ when:
+ event: push
+ branch: main
+
+ release-tag:
+ image: plugins/gitea-release:1
+ secrets:
+ - source: gitea_token
+ target: plugin_api_key
+ base_url: https://git.jojodev.com
+ files:
+ - "spectre"
+ - "spectre.exe"
+ when:
+ event: tag
+ tag: v*
+
+ prune:
+ image: jolheiser/drone-gitea-prune
+ secrets:
+ - source: gitea_token
+ target: plugin_token
+ base: https://git.jojodev.com
+ when:
+ event: tag
+ tag: v*
diff --git a/README.md b/README.md
index d40829e34956d728e689da4851e09d63f171930d..0e3437e84aed6508394c61ee93feca3c9987e8d8 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,11 @@
-# Spectre
+# Spectre
+
+[![Go Reference](https://pkg.go.dev/badge/go.jolheiser.com/go-spectre.svg)](https://pkg.go.dev/go.jolheiser.com/go-spectre)
A Go implementation of [spectre](https://spectre.app).
-Currently it passes a sub-set of the [CLI tests](https://gitlab.com/spectre.app/cli/-/blob/main/spectre_tests.xml).
+Currently, it passes a sub-set of the [CLI tests](https://gitlab.com/spectre.app/cli/-/blob/main/spectre_tests.xml).
+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.
diff --git a/cmd/spectre/main.go b/cmd/spectre/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..6f6a394c5eebf21e61f0bd7904b9ec9ad5085fc8
--- /dev/null
+++ b/cmd/spectre/main.go
@@ -0,0 +1,83 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+
+ "go.jolheiser.com/spectre"
+)
+
+func main() {
+ fs := flag.NewFlagSet("spectre", flag.ExitOnError)
+ usernameFlag := fs.String("username", "", "username")
+ secretFlag := fs.String("secret", "", "secret")
+ siteFlag := fs.String("site", "", "site")
+ counterFlag := fs.Int("counter", 1, "counter")
+ scoperFlag := fs.String("scoper", "com.lyndir.masterpassword", "scoper base")
+ scopeFlag := spectre.Authentication
+ fs.Func("scope", "scope", func(s string) (err error) {
+ scopeFlag, err = spectre.ParseScope(s)
+ return
+ })
+ var templateFlag spectre.Template
+ fs.Func("template", "template", func(s string) (err error) {
+ templateFlag, err = spectre.ParseTemplate(s)
+ return
+ })
+
+ if err := fs.Parse(os.Args[1:]); err != nil {
+ panic(err)
+ }
+ if err := checkEnv(fs); err != nil {
+ panic(err)
+ }
+
+ if templateFlag == "" {
+ templateFlag = scopeFlag.DefaultTemplate()
+ }
+
+ if *usernameFlag == "" || *secretFlag == "" || *siteFlag == "" {
+ panic("username, secret, and site are required")
+ }
+
+ s, err := spectre.New(*usernameFlag, *secretFlag, spectre.WithScoper(spectre.SimpleScoper{
+ Key: *scoperFlag,
+ }))
+ if err != nil {
+ panic(err)
+ }
+
+ pw := s.Site(*siteFlag,
+ spectre.WithScope(scopeFlag),
+ spectre.WithTemplate(templateFlag),
+ spectre.WithCounter(*counterFlag),
+ )
+
+ fmt.Println(pw)
+}
+
+func checkEnv(fs *flag.FlagSet) error {
+ provided := map[string]struct{}{}
+ fs.Visit(func(f *flag.Flag) {
+ provided[f.Name] = struct{}{}
+ })
+ var visitErr error
+ fs.VisitAll(func(f *flag.Flag) {
+ if visitErr != nil {
+ return
+ }
+ if _, ok := provided[f.Name]; ok {
+ return
+ }
+ env := os.Getenv(fmt.Sprintf("SPECTRE_%s", strings.ToUpper(f.Name)))
+ if env == "" {
+ return
+ }
+ if err := fs.Set(f.Name, env); err != nil {
+ visitErr = fmt.Errorf("could not set flag %q to %q", f.Name, env)
+ }
+ })
+ return nil
+}
diff --git a/go.sum b/go.sum
index 49ae2ce2300a7a01d97e71944aff2f5154dfc415..81601f9b9535b26839699a3de80cfabfe498c80e 100644
--- a/go.sum
+++ b/go.sum
@@ -1,14 +1,9 @@
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
-golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/scope.go b/scope.go
index 7cd8baa598402c8d09a2ec2833c6234c51ab661f..8480e0569397ef22f59c3a47ab8b810a17422f33 100644
--- a/scope.go
+++ b/scope.go
@@ -1,6 +1,15 @@
package spectre
// Scope is a key scope
+
+ "errors"
+ "strings"
+)
+
+// Interface guard
+var _ Scoper = (*SimpleScoper)(nil)
+
+// Scope is a key scope
type Scope string
const (
@@ -8,6 +17,34 @@ Authentication Scope = "Authentication"
Identification Scope = "Identification"
Recovery Scope = "Recovery"
)
+
+// DefaultTemplate is the default Template for a Scope
+func (s Scope) DefaultTemplate() Template {
+ switch s {
+ case Identification:
+ return Name
+ case Recovery:
+ return Phrase
+ case Authentication:
+ fallthrough
+ default:
+ return Long
+ }
+}
+
+// ParseScope returns a Scope from s
+func ParseScope(s string) (Scope, error) {
+ switch strings.ToLower(s) {
+ case "authentication", "a":
+ return Authentication, nil
+ case "identification", "i":
+ return Identification, nil
+ case "recovery", "r":
+ return Recovery, nil
+ default:
+ return "", errors.New("unknown Scope")
+ }
+}
// Scoper returns one of the three available scopes
type Scoper interface {
diff --git a/spectre.go b/spectre.go
index db642df76c33b2e9c482c1e3a84355456999b324..4f5ba3d5e7c8fb5e803c12093d7390c9ce25f5e1 100644
--- a/spectre.go
+++ b/spectre.go
@@ -11,13 +11,13 @@ // Spectre is a spectre client
type Spectre struct {
name string
secret string
-
- key []byte
scoper Scoper
+
+ key []byte
}
// New returns a Spectre client
-package spectre
+)
"strings"
s = &Spectre{
name: name,
@@ -31,12 +31,12 @@ s.key, err = s.userKey()
return
}
-// SpectreOption is a Spectre option
+// Option is a Spectre option
-
)
+// Spectre is a spectre client
// WithScoper assigns a scoper to Spectre
-import (
+// Spectre is a spectre client
return func(s *Spectre) {
s.scoper = scoper
}
@@ -87,7 +87,7 @@
// Site returns a site password based on Options
func (s *Spectre) Site(siteName string, opts ...SiteOption) string {
siteOpts := &options{
- "golang.org/x/crypto/scrypt"
+// Spectre is a spectre client
package spectre
counter: 1,
scope: Authentication,
@@ -96,6 +96,10 @@ 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]
@@ -116,20 +120,24 @@ counter int
scope Scope
}
+// SiteOption is an option for Spectre.Site
type SiteOption func(*options)
+// WithTemplate specifies a Template
func WithTemplate(t Template) SiteOption {
return func(opts *options) {
opts.template = t
}
}
+// WithCounter specifies a counter
func WithCounter(c int) SiteOption {
return func(opts *options) {
opts.counter = c
}
}
+// WithScope specifies a Scope
func WithScope(s Scope) SiteOption {
return func(opts *options) {
opts.scope = s
diff --git a/spectre_test.go b/spectre_test.go
index 27deb2ddd0930082c53921851c35b51a36c9e1f9..c132a2901c577daaeeb620bc36493a962ce36c9d 100644
--- a/spectre_test.go
+++ b/spectre_test.go
@@ -1,10 +1,12 @@
-package spectre
+package spectre_test
import (
_ "embed"
"encoding/xml"
"strconv"
"testing"
+
+ "go.jolheiser.com/spectre"
)
func TestSpectre(t *testing.T) {
@@ -20,7 +22,7 @@ t.Run(tc.ID, func(t *testing.T) {
user := def(dc.UserName, tc.UserName)
secret := def(dc.UserSecret, tc.UserSecret)
- s, err := New(user, secret)
+ s, err := spectre.New(user, secret)
if err != nil {
t.Logf("could not initialize spectre: %v", err)
t.Fail()
@@ -37,9 +39,9 @@ }
scope := def(dc.KeyPurpose, tc.KeyPurpose)
pass := s.Site(siteName,
- WithTemplate(Template(template)),
+ spectre.WithTemplate(spectre.Template(template)),
- WithCounter(counter),
+ spectre.WithCounter(counter),
- WithScope(Scope(scope)),
+ spectre.WithScope(spectre.Scope(scope)),
)
if pass != tc.Result {
@@ -52,7 +54,7 @@ }
// From the website sanity check
func TestSanity(t *testing.T) {
- s, err := New("Robert Lee Mitchell", "banana colored duckling")
+ s, err := spectre.New("Robert Lee Mitchell", "banana colored duckling")
if err != nil {
t.Logf("failed sanity check: %v", err)
t.FailNow()
diff --git a/template.go b/template.go
index f7eff748c401d4a449da7814af4228b1b5e901e2..5fb3ca2787aefe4d555616eef98e939582178659 100644
--- a/template.go
+++ b/template.go
@@ -1,5 +1,10 @@
package spectre
+import (
+ "errors"
+ "strings"
+)
+
// Template is a template type
type Template string
@@ -13,6 +18,30 @@ Name Template = "Name"
Phrase Template = "Phrase"
Basic Template = "Basic"
)
+
+// ParseTemplate parses a Template from s
+func ParseTemplate(s string) (Template, error) {
+ switch strings.ToLower(s) {
+ case "maximum", "max":
+ return Maximum, nil
+ case "long", "l":
+ return Long, nil
+ case "medium", "med":
+ return Medium, nil
+ case "short", "sh":
+ return Short, nil
+ case "pin":
+ return Pin, nil
+ case "name":
+ return Name, nil
+ case "phrase":
+ return Phrase, nil
+ case "basic":
+ return Basic, nil
+ default:
+ return "", errors.New("unknown Template")
+ }
+}
var templates = map[Template][]string{
Maximum: {