Add CLI and some cleanup
continuous-integration/woodpecker the build was successful Details

Signed-off-by: jolheiser <john.olheiser@gmail.com>
main v0.0.1
jolheiser 2021-11-09 22:05:41 -06:00
parent bbcba946f1
commit 99cae4edc2
Signed by: jolheiser
GPG Key ID: B853ADA5DA7BBF7A
9 changed files with 237 additions and 21 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
.idea/ .idea/
/spectre
/spectre.exe

58
.woodpecker.yml 100644
View File

@ -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*

View File

@ -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). 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. This is because I've only implemented v3 of the algorithm and the main pieces.

View File

@ -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
}

5
go.sum
View File

@ -1,13 +1,8 @@
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/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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/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-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/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/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/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= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@ -1,5 +1,13 @@
package spectre package spectre
import (
"errors"
"strings"
)
// Interface guard
var _ Scoper = (*SimpleScoper)(nil)
// Scope is a key scope // Scope is a key scope
type Scope string type Scope string
@ -9,6 +17,34 @@ const (
Recovery Scope = "Recovery" 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 // Scoper returns one of the three available scopes
type Scoper interface { type Scoper interface {
Scope(Scope) string Scope(Scope) string

View File

@ -11,13 +11,13 @@ import (
type Spectre struct { type Spectre struct {
name string name string
secret string secret string
scoper Scoper
key []byte key []byte
scoper Scoper
} }
// New returns a Spectre client // 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{ s = &Spectre{
name: name, name: name,
secret: secret, secret: secret,
@ -30,11 +30,11 @@ func New(name, secret string, opts ...SpectreOption) (s *Spectre, err error) {
return return
} }
// SpectreOption is a Spectre option // Option is a Spectre option
type SpectreOption func(*Spectre) type Option func(*Spectre)
// WithScoper assigns a scoper to Spectre // WithScoper assigns a scoper to Spectre
func WithScoper(scoper Scoper) SpectreOption { func WithScoper(scoper Scoper) Option {
return func(s *Spectre) { return func(s *Spectre) {
s.scoper = scoper 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 // Site returns a site password based on Options
func (s *Spectre) Site(siteName string, opts ...SiteOption) string { func (s *Spectre) Site(siteName string, opts ...SiteOption) string {
siteOpts := &options{ siteOpts := &options{
template: Long, template: "",
counter: 1, counter: 1,
scope: Authentication, scope: Authentication,
} }
@ -93,6 +93,10 @@ func (s *Spectre) Site(siteName string, opts ...SiteOption) string {
opt(siteOpts) opt(siteOpts)
} }
if siteOpts.template == "" {
siteOpts.template = siteOpts.scope.DefaultTemplate()
}
siteKey := s.siteKey(siteName, siteOpts.counter, siteOpts.scope) siteKey := s.siteKey(siteName, siteOpts.counter, siteOpts.scope)
templateSet := templates[siteOpts.template] templateSet := templates[siteOpts.template]
@ -113,20 +117,24 @@ type options struct {
scope Scope scope Scope
} }
// SiteOption is an option for Spectre.Site
type SiteOption func(*options) type SiteOption func(*options)
// WithTemplate specifies a Template
func WithTemplate(t Template) SiteOption { func WithTemplate(t Template) SiteOption {
return func(opts *options) { return func(opts *options) {
opts.template = t opts.template = t
} }
} }
// WithCounter specifies a counter
func WithCounter(c int) SiteOption { func WithCounter(c int) SiteOption {
return func(opts *options) { return func(opts *options) {
opts.counter = c opts.counter = c
} }
} }
// WithScope specifies a Scope
func WithScope(s Scope) SiteOption { func WithScope(s Scope) SiteOption {
return func(opts *options) { return func(opts *options) {
opts.scope = s opts.scope = s

View File

@ -1,10 +1,12 @@
package spectre package spectre_test
import ( import (
_ "embed" _ "embed"
"encoding/xml" "encoding/xml"
"strconv" "strconv"
"testing" "testing"
"go.jolheiser.com/spectre"
) )
func TestSpectre(t *testing.T) { func TestSpectre(t *testing.T) {
@ -20,7 +22,7 @@ func TestSpectre(t *testing.T) {
user := def(dc.UserName, tc.UserName) user := def(dc.UserName, tc.UserName)
secret := def(dc.UserSecret, tc.UserSecret) secret := def(dc.UserSecret, tc.UserSecret)
s, err := New(user, secret) s, err := spectre.New(user, secret)
if err != nil { if err != nil {
t.Logf("could not initialize spectre: %v", err) t.Logf("could not initialize spectre: %v", err)
t.Fail() t.Fail()
@ -37,9 +39,9 @@ func TestSpectre(t *testing.T) {
scope := def(dc.KeyPurpose, tc.KeyPurpose) scope := def(dc.KeyPurpose, tc.KeyPurpose)
pass := s.Site(siteName, 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 { if pass != tc.Result {
@ -52,7 +54,7 @@ func TestSpectre(t *testing.T) {
// From the website sanity check // From the website sanity check
func TestSanity(t *testing.T) { 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 { if err != nil {
t.Logf("failed sanity check: %v", err) t.Logf("failed sanity check: %v", err)
t.FailNow() t.FailNow()

View File

@ -1,5 +1,10 @@
package spectre package spectre
import (
"errors"
"strings"
)
// Template is a template type // Template is a template type
type Template string type Template string
@ -14,6 +19,30 @@ const (
Basic Template = "Basic" 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{ var templates = map[Template][]string{
Maximum: { Maximum: {
"anoxxxxxxxxxxxxxxxxx", "anoxxxxxxxxxxxxxxxxx",