From 99cae4edc28e9548ca02f6652dc079e7f5acd22f Mon Sep 17 00:00:00 2001 From: jolheiser Date: Tue, 9 Nov 2021 22:05:41 -0600 Subject: [PATCH] Add CLI and some cleanup Signed-off-by: jolheiser --- .gitignore | 4 ++- .woodpecker.yml | 58 +++++++++++++++++++++++++++++++ README.md | 7 ++-- cmd/spectre/main.go | 83 +++++++++++++++++++++++++++++++++++++++++++++ go.sum | 5 --- scope.go | 36 ++++++++++++++++++++ spectre.go | 22 ++++++++---- spectre_test.go | 14 ++++---- template.go | 29 ++++++++++++++++ 9 files changed, 237 insertions(+), 21 deletions(-) create mode 100644 .woodpecker.yml create mode 100644 cmd/spectre/main.go diff --git a/.gitignore b/.gitignore index 62c8935..08dd7e0 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 0000000..72e6341 --- /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 d40829e..0e3437e 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 0000000..6f6a394 --- /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 49ae2ce..81601f9 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,8 @@ 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/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -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 7cd8baa..8480e05 100644 --- a/scope.go +++ b/scope.go @@ -1,5 +1,13 @@ package spectre +import ( + "errors" + "strings" +) + +// Interface guard +var _ Scoper = (*SimpleScoper)(nil) + // Scope is a key scope type Scope string @@ -9,6 +17,34 @@ const ( 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 { Scope(Scope) string diff --git a/spectre.go b/spectre.go index db642df..4f5ba3d 100644 --- a/spectre.go +++ b/spectre.go @@ -11,13 +11,13 @@ import ( type Spectre struct { name string secret string - - key []byte scoper Scoper + + key []byte } // New returns a Spectre client -func New(name, secret string, opts ...SpectreOption) (s *Spectre, err error) { +func New(name, secret string, opts ...Option) (s *Spectre, err error) { s = &Spectre{ name: name, secret: secret, @@ -30,11 +30,11 @@ func New(name, secret string, opts ...SpectreOption) (s *Spectre, err error) { return } -// SpectreOption is a Spectre option -type SpectreOption func(*Spectre) +// Option is a Spectre option +type Option func(*Spectre) // WithScoper assigns a scoper to Spectre -func WithScoper(scoper Scoper) SpectreOption { +func WithScoper(scoper Scoper) Option { return func(s *Spectre) { s.scoper = scoper } @@ -85,7 +85,7 @@ func (s *Spectre) siteKey(name string, counter int, scope Scope) []byte { // Site returns a site password based on Options func (s *Spectre) Site(siteName string, opts ...SiteOption) string { siteOpts := &options{ - template: Long, + template: "", counter: 1, scope: Authentication, } @@ -93,6 +93,10 @@ func (s *Spectre) Site(siteName string, opts ...SiteOption) string { opt(siteOpts) } + if siteOpts.template == "" { + siteOpts.template = siteOpts.scope.DefaultTemplate() + } + siteKey := s.siteKey(siteName, siteOpts.counter, siteOpts.scope) templateSet := templates[siteOpts.template] @@ -113,20 +117,24 @@ type options struct { 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 27deb2d..c132a29 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 @@ func TestSpectre(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 @@ func TestSpectre(t *testing.T) { scope := def(dc.KeyPurpose, tc.KeyPurpose) pass := s.Site(siteName, - WithTemplate(Template(template)), - WithCounter(counter), - WithScope(Scope(scope)), + spectre.WithTemplate(spectre.Template(template)), + spectre.WithCounter(counter), + spectre.WithScope(spectre.Scope(scope)), ) if pass != tc.Result { @@ -52,7 +54,7 @@ func TestSpectre(t *testing.T) { // 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 f7eff74..5fb3ca2 100644 --- a/template.go +++ b/template.go @@ -1,5 +1,10 @@ package spectre +import ( + "errors" + "strings" +) + // Template is a template type type Template string @@ -14,6 +19,30 @@ const ( 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: { "anoxxxxxxxxxxxxxxxxx",