Add CLI and some cleanup
continuous-integration/woodpecker the build was successful
Details
continuous-integration/woodpecker the build was successful
Details
Signed-off-by: jolheiser <john.olheiser@gmail.com>main v0.0.1
parent
bbcba946f1
commit
99cae4edc2
|
@ -1 +1,3 @@
|
||||||
.idea/
|
.idea/
|
||||||
|
/spectre
|
||||||
|
/spectre.exe
|
|
@ -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*
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
5
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 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=
|
||||||
|
|
36
scope.go
36
scope.go
|
@ -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
|
||||||
|
|
20
spectre.go
20
spectre.go
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
29
template.go
29
template.go
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue