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/
|
||||
/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
|
||||
|
||||
[![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.
|
||||
|
||||
|
|
|
@ -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/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=
|
||||
|
|
36
scope.go
36
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
|
||||
|
|
22
spectre.go
22
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
|
||||
|
|
|
@ -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()
|
||||
|
|
29
template.go
29
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",
|
||||
|
|
Loading…
Reference in New Issue