├── pkg ├── totp │ ├── generator.go │ ├── models.go │ └── totp.go ├── keyvalue │ ├── model.go │ ├── errors.go │ ├── interface.go │ ├── leveldb.go │ └── mock │ │ └── interface.go ├── tokenycli │ ├── interface.go │ └── service.go ├── session │ ├── interface.go │ └── manager.go ├── password │ ├── interface.go │ ├── errors.go │ ├── manager.go │ └── manager_test.go ├── tokeny │ ├── errors.go │ ├── interface.go │ └── repository.go ├── utils │ ├── strings.go │ └── strings_test.go ├── hotp │ ├── hotp_test.go │ └── hotp.go └── crypto │ ├── utils.go │ └── utils_test.go ├── go.mod ├── DEPLOY.md ├── .github └── workflows │ └── workflow.yml ├── LICENSE ├── main.go ├── .goreleaser.yml ├── README.md ├── .gitignore ├── go.sum └── .golangci.yml /pkg/totp/generator.go: -------------------------------------------------------------------------------- 1 | package totp 2 | 3 | type Generator interface { 4 | Generate() Token 5 | } 6 | -------------------------------------------------------------------------------- /pkg/keyvalue/model.go: -------------------------------------------------------------------------------- 1 | package keyvalue 2 | 3 | type KeyValue struct { 4 | Key string 5 | Value string 6 | } 7 | -------------------------------------------------------------------------------- /pkg/totp/models.go: -------------------------------------------------------------------------------- 1 | package totp 2 | 3 | type Token struct { 4 | Value string 5 | TimeoutSec int64 6 | } 7 | -------------------------------------------------------------------------------- /pkg/keyvalue/errors.go: -------------------------------------------------------------------------------- 1 | package keyvalue 2 | 3 | import "github.com/pkg/errors" 4 | 5 | var ( 6 | ErrNoRecord = errors.New("record not found") 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/tokenycli/interface.go: -------------------------------------------------------------------------------- 1 | package tokenycli 2 | 3 | import "github.com/urfave/cli/v2" 4 | 5 | type Service interface { 6 | Register(app *cli.App) 7 | } 8 | -------------------------------------------------------------------------------- /pkg/session/interface.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | type Manager interface { 4 | IsSessionValid(sessionKey string) (bool, error) 5 | NewSession(sessionKey string) error 6 | } 7 | -------------------------------------------------------------------------------- /pkg/password/interface.go: -------------------------------------------------------------------------------- 1 | package password 2 | 3 | type Manager interface { 4 | IsRegistered() (bool, error) 5 | Register(pwd string, rePwd string) error 6 | Login(pwd string) error 7 | } 8 | -------------------------------------------------------------------------------- /pkg/tokeny/errors.go: -------------------------------------------------------------------------------- 1 | package tokeny 2 | 3 | import "github.com/pkg/errors" 4 | 5 | var ( 6 | ErrNoEntryFound = errors.New("no entry found") 7 | ErrEntryExistedBefore = errors.New("entry existed before") 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/utils/strings.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "fmt" 4 | 5 | func Padding0(otp string, expectedLength int) string { 6 | fmtTemplate := fmt.Sprintf("%%0%ds", expectedLength) 7 | return fmt.Sprintf(fmtTemplate, otp) 8 | } 9 | -------------------------------------------------------------------------------- /pkg/hotp/hotp_test.go: -------------------------------------------------------------------------------- 1 | package hotp 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestGenerate(t *testing.T) { 9 | result := Generate([]byte("my-very-key"), []byte("my-very-counter"), 6) 10 | assert.Equal(t, "789672", result) 11 | } 12 | 13 | -------------------------------------------------------------------------------- /pkg/password/errors.go: -------------------------------------------------------------------------------- 1 | package password 2 | 3 | import "github.com/pkg/errors" 4 | 5 | var ( 6 | ErrPasswordsMismatch = errors.New("passwords do not match") 7 | ErrNotRegistered = errors.New("have not registered yet") 8 | ErrWrongPassword = errors.New("wrong password") 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/keyvalue/interface.go: -------------------------------------------------------------------------------- 1 | package keyvalue 2 | 3 | //go:generate mockgen -source=interface.go -destination=mock/interface.go 4 | type Store interface { 5 | Set(key string, value string) error 6 | Get(key string) (string, error) 7 | Delete(key string) error 8 | GetAllWithPrefixed(keyPrefix string) ([]KeyValue, error) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/tokeny/interface.go: -------------------------------------------------------------------------------- 1 | package tokeny 2 | 3 | import "github.com/zalopay-oss/tokeny/pkg/totp" 4 | 5 | type Repository interface { 6 | Add(alias string, secret string) error 7 | Generate(alias string) (totp.Token, error) 8 | Delete(alias string) error 9 | List() ([]string, error) 10 | LastValidEntry() (string, error) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/crypto/utils.go: -------------------------------------------------------------------------------- 1 | // nolint: gosec 2 | package crypto 3 | 4 | import ( 5 | "crypto/hmac" 6 | "crypto/sha1" 7 | "log" 8 | ) 9 | 10 | func ComputeHMACSHA1(key []byte, data []byte) []byte { 11 | h := hmac.New(sha1.New, key) 12 | _, err := h.Write(data) 13 | if err != nil { 14 | log.Fatalln(err) 15 | } 16 | return h.Sum(nil) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/crypto/utils_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "encoding/hex" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestComputeHMACSHA1(t *testing.T) { 10 | result := ComputeHMACSHA1([]byte("my-something"), []byte("thequickbrownfoxjumpsoverthelazydog")) 11 | assert.Equal(t, "e5028c8a840936e8565629b795fb1c1e0bc53b0d", hex.EncodeToString(result)) 12 | } 13 | 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zalopay-oss/tokeny 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/atotto/clipboard v0.1.2 7 | github.com/golang/mock v1.4.4 8 | github.com/manifoldco/promptui v0.7.0 9 | github.com/pkg/errors v0.9.1 10 | github.com/stretchr/testify v1.6.1 11 | github.com/syndtr/goleveldb v1.0.0 12 | github.com/urfave/cli/v2 v2.2.0 13 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 14 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /DEPLOY.md: -------------------------------------------------------------------------------- 1 | # Deploying 2 | 3 | **Tokeny** is using [GoReleaser](https://goreleaser.com/) to build and deploy binaries to GitHub Releases. 4 | 5 | In the meantime, GoReleaser is having [problem](https://github.com/goreleaser/goreleaser/issues/708) with cross compiling when `CGO_ENABLED` is required. 6 | 7 | The solution is running GoReleaser inside a Docker container as proposed by @robdefeo at [goreleaser-xcgo](https://github.com/mailchain/goreleaser-xcgo). 8 | 9 | ```bash 10 | docker run --rm --privileged -e GITHUB_TOKEN=$GITHUB_TOKEN -v $(pwd):/go/src/github.com/zalopay-oss/tokeny -v /var/run/docker.sock:/var/run/docker.sock -w /go/src/github.com/zalopay-oss/tokeny mailchain/goreleaser-xcgo --rm-dist 11 | ``` 12 | 13 | * `$GITHUB_TOKEN` represents your GitHub's personal access token 14 | 15 | -------------------------------------------------------------------------------- /pkg/hotp/hotp.go: -------------------------------------------------------------------------------- 1 | package hotp 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "math" 7 | 8 | "github.com/zalopay-oss/tokeny/pkg/crypto" 9 | "github.com/zalopay-oss/tokeny/pkg/utils" 10 | ) 11 | 12 | func Generate(key []byte, counter []byte, otpLength int) string { 13 | hash := crypto.ComputeHMACSHA1(key, counter) 14 | truncatedHash := truncate(hash) 15 | hNum := binary.BigEndian.Uint32(truncatedHash) 16 | otp := hNum % (uint32)(math.Pow10(otpLength)) 17 | result := utils.Padding0(fmt.Sprint(otp), otpLength) 18 | return result 19 | } 20 | 21 | func truncate(hash []byte) []byte { 22 | var ( 23 | last4BitFilter byte = 0xf 24 | firstBitFilter byte = 0x7f 25 | ) 26 | offset := hash[19] & last4BitFilter 27 | truncatedHash := hash[offset : offset+4] 28 | truncatedHash[0] &= firstBitFilter 29 | return truncatedHash 30 | } 31 | -------------------------------------------------------------------------------- /pkg/utils/strings_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestPadding0(t *testing.T) { 10 | tests := []struct { 11 | input string 12 | desiredLength int 13 | expected string 14 | }{ 15 | { 16 | input: "123456", 17 | desiredLength: 6, 18 | expected: "123456", 19 | }, 20 | { 21 | input: "", 22 | desiredLength: 6, 23 | expected: "000000", 24 | }, 25 | { 26 | input: "1", 27 | desiredLength: 6, 28 | expected: "000001", 29 | }, 30 | { 31 | input: "321", 32 | desiredLength: 6, 33 | expected: "000321", 34 | }, 35 | } 36 | for i, test := range tests { 37 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 38 | result := Padding0(test.input, test.desiredLength) 39 | assert.Equal(t, test.expected, result) 40 | }) 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /pkg/totp/totp.go: -------------------------------------------------------------------------------- 1 | package totp 2 | 3 | import ( 4 | "encoding/base32" 5 | "encoding/binary" 6 | "strings" 7 | "time" 8 | 9 | "github.com/zalopay-oss/tokeny/pkg/hotp" 10 | ) 11 | 12 | const ( 13 | tokenLength = 6 14 | bufferLength = 8 15 | otpTTL int64 = 30 16 | ) 17 | 18 | type generator struct { 19 | secret []byte 20 | } 21 | 22 | func NewGenerator(secret string) (*generator, error) { 23 | s, err := base32.StdEncoding.DecodeString(strings.ToUpper(secret)) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return &generator{s}, nil 28 | } 29 | 30 | func (g *generator) Generate() Token { 31 | now := time.Now().Unix() 32 | quotient := now / otpTTL 33 | remainder := now % otpTTL 34 | 35 | data := make([]byte, bufferLength) 36 | binary.BigEndian.PutUint64(data, uint64(quotient)) 37 | 38 | return Token{ 39 | Value: hotp.Generate(g.secret, data, tokenLength), 40 | TimeoutSec: otpTTL - remainder, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: unit-test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | unit-test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-go@v2 12 | with: 13 | go-version: 1.17.2 14 | - run: go test ./... 15 | 16 | lint: 17 | name: lint 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: golangci/golangci-lint-action@v2 22 | with: 23 | version: v1.29 24 | 25 | release: 26 | name: release 27 | runs-on: ubuntu-latest 28 | needs: [unit-test, lint] 29 | steps: 30 | - uses: actions/checkout@v2 31 | with: 32 | fetch-depth: 0 33 | - uses: actions/setup-go@v2 34 | with: 35 | go-version: 1.17.2 36 | - uses: goreleaser/goreleaser-action@v2 37 | if: startsWith(github.ref, 'refs/tags/') 38 | with: 39 | version: latest 40 | args: release --rm-dist 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lê Thái Phúc Quang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/keyvalue/leveldb.go: -------------------------------------------------------------------------------- 1 | package keyvalue 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/syndtr/goleveldb/leveldb" 7 | "github.com/syndtr/goleveldb/leveldb/util" 8 | ) 9 | 10 | type levelDB struct { 11 | db *leveldb.DB 12 | } 13 | 14 | func NewLevelDBStore(db *leveldb.DB) *levelDB { 15 | return &levelDB{db: db} 16 | } 17 | 18 | func (l *levelDB) Set(key string, value string) error { 19 | return l.db.Put([]byte(key), []byte(value), nil) 20 | } 21 | 22 | func (l *levelDB) Get(key string) (string, error) { 23 | r, err := l.db.Get([]byte(key), nil) 24 | if errors.Is(err, leveldb.ErrNotFound) { 25 | return "", ErrNoRecord 26 | } 27 | if err != nil { 28 | return "", err 29 | } 30 | return string(r), nil 31 | } 32 | 33 | func (l *levelDB) Delete(key string) error { 34 | return l.db.Delete([]byte(key), nil) 35 | } 36 | 37 | func (l *levelDB) GetAllWithPrefixed(keyPrefix string) ([]KeyValue, error) { 38 | result := make([]KeyValue, 0) 39 | iter := l.db.NewIterator(util.BytesPrefix([]byte(keyPrefix)), nil) 40 | for iter.Next() { 41 | result = append(result, KeyValue{ 42 | Key: string(iter.Key()), 43 | Value: string(iter.Value()), 44 | }) 45 | } 46 | iter.Release() 47 | if err := iter.Error(); err != nil { 48 | return nil, err 49 | } 50 | return result, nil 51 | } 52 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/user" 8 | 9 | "github.com/syndtr/goleveldb/leveldb" 10 | "github.com/urfave/cli/v2" 11 | "github.com/zalopay-oss/tokeny/pkg/keyvalue" 12 | "github.com/zalopay-oss/tokeny/pkg/password" 13 | "github.com/zalopay-oss/tokeny/pkg/session" 14 | "github.com/zalopay-oss/tokeny/pkg/tokeny" 15 | "github.com/zalopay-oss/tokeny/pkg/tokenycli" 16 | ) 17 | 18 | func main() { 19 | usr, err := user.Current() 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | dbDir := fmt.Sprintf("%s/.tokeny", usr.HomeDir) 24 | if err := os.MkdirAll(dbDir, os.ModePerm); err != nil { 25 | log.Fatal(err) 26 | } 27 | db, err := leveldb.OpenFile(fmt.Sprintf("%s/ld.db", dbDir), nil) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | defer func() { _ = db.Close() }() 32 | kvStore := keyvalue.NewLevelDBStore(db) 33 | 34 | pwdManager := password.NewManager(kvStore) 35 | 36 | sessionManager := session.NewManager(kvStore) 37 | 38 | tokenRepo := tokeny.NewRepository(kvStore) 39 | 40 | cliSvc := tokenycli.NewService(pwdManager, sessionManager, tokenRepo) 41 | 42 | app := cli.NewApp() 43 | app.EnableBashCompletion = true 44 | app.Usage = "Another TOTP generator" 45 | 46 | err = cliSvc.Register(app) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | if err = app.Run(os.Args); err != nil { 52 | log.SetFlags(0) 53 | log.Println(err.Error()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: tokeny 2 | 3 | builds: 4 | - # macOS 5 | id: darwin 6 | main: ./main.go 7 | env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - darwin 11 | goarch: 12 | - amd64 13 | - arm64 14 | targets: 15 | - darwin_amd64 16 | - darwin_arm64 17 | ldflags: 18 | - -s -w 19 | - # Linux 20 | id: linux 21 | main: ./main.go 22 | env: 23 | - CGO_ENABLED=0 24 | goos: 25 | - linux 26 | goarch: 27 | - amd64 28 | brews: 29 | - name: tokeny 30 | tap: 31 | owner: zalopay-oss 32 | name: homebrew-tap 33 | url_template: "https://github.com/zalopay-oss/tokeny/releases/download/{{ .Tag }}/{{ .ArtifactName }}" 34 | commit_author: 35 | name: Tokeny's GitHubAction 36 | email: phuc.quang102@gmail.com 37 | commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" 38 | folder: Formula 39 | homepage: "https://github.com/zalopay-oss/tokeny" 40 | description: "Minimal TOTP generator from your Shell" 41 | archives: 42 | - replacements: 43 | amd64: x86_64 44 | darwin: macOS 45 | linux: Linux 46 | files: 47 | - README.md 48 | - LICENSE 49 | checksum: 50 | name_template: 'checksums.txt' 51 | snapshot: 52 | name_template: "{{ .Tag }}-next" 53 | changelog: 54 | sort: asc 55 | filters: 56 | exclude: 57 | - '^docs:' 58 | - '^test:' 59 | - '^dev:' 60 | - 'README' 61 | - Merge pull request 62 | - Merge branch -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tokeny 2 | 3 | Tokeny is a minimal CLI **[TOTP](https://tools.ietf.org/html/rfc6238) (Time-Based One-Time Password)** generator. 4 | 5 | ## 1. Installation 6 | 7 | ### Using Homebrew 8 | 9 | ``` 10 | brew install zalopay-oss/tap/tokeny 11 | ``` 12 | 13 | or 14 | 15 | ``` 16 | brew tap zalopay-oss/tap 17 | brew install tokeny 18 | ``` 19 | 20 | ### Using pre-built binaries 21 | 22 | Download proper binary from [releases section](https://github.com/zalopay-oss/tokeny/releases) and put into your paths. 23 | 24 | ### Build from source 25 | 26 | You can also clone this repo and build it yourself. 27 | 28 | ## 2. Usage 29 | 30 | Please consult `tokeny --help` for all features' usages. 31 | 32 | ```bash 33 | NAME: 34 | tokeny - Another TOTP generator 35 | 36 | USAGE: 37 | tokeny [global options] command [command options] [arguments...] 38 | 39 | COMMANDS: 40 | setup setup master password 41 | add add new entry 42 | get get OTP 43 | delete delete selected entry 44 | list list all entries 45 | help, h Shows a list of commands or help for one command 46 | 47 | GLOBAL OPTIONS: 48 | --help, -h show help (default: false) 49 | ``` 50 | 51 | ### Master Password 52 | 53 | **Tokeny** requires a Master Password for authenticating you against the whole application. 54 | 55 | Master Password can be set **only once**, on the very first time you run `tokeny setup`. After that, all other commands will ask for Master Password once for every 5 minutes. 56 | 57 | In case you lost your Master Password, the only way to reset it is removing all data (including token entries), located at `$HOME/.tokeny`. 58 | 59 | ## 3. Caution 60 | 61 | Please think twice before using **Tokeny**, since having token generator in your machine may make you lose the benefits of **Two-Factor** Authentication. 62 | -------------------------------------------------------------------------------- /pkg/password/manager.go: -------------------------------------------------------------------------------- 1 | package password 2 | 3 | import ( 4 | "encoding/hex" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/zalopay-oss/tokeny/pkg/keyvalue" 8 | "golang.org/x/crypto/bcrypt" 9 | ) 10 | 11 | const ( 12 | keyPassword = "password" 13 | ) 14 | 15 | type manager struct { 16 | kvStore keyvalue.Store 17 | } 18 | 19 | func NewManager(kvStore keyvalue.Store) *manager { 20 | return &manager{kvStore: kvStore} 21 | } 22 | 23 | func (m *manager) IsRegistered() (bool, error) { 24 | _, err := m.kvStore.Get(keyPassword) 25 | if err == nil { 26 | return true, nil 27 | } 28 | if errors.Is(err, keyvalue.ErrNoRecord) { 29 | return false, nil 30 | } 31 | return false, err 32 | } 33 | 34 | func (m *manager) Register(pwd string, rePwd string) error { 35 | saltedPwd, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) 36 | if err != nil { 37 | return err 38 | } 39 | err = bcrypt.CompareHashAndPassword(saltedPwd, []byte(rePwd)) 40 | if err != nil { 41 | if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { 42 | return ErrPasswordsMismatch 43 | } 44 | return err 45 | } 46 | 47 | err = m.kvStore.Set(keyPassword, hex.EncodeToString(saltedPwd)) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (m *manager) Login(pwd string) error { 56 | savedPwdHexStr, err := m.kvStore.Get(keyPassword) 57 | if err != nil { 58 | if errors.Is(err, keyvalue.ErrNoRecord) { 59 | return ErrNotRegistered 60 | } 61 | return err 62 | } 63 | 64 | savedPwd, err := hex.DecodeString(savedPwdHexStr) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | err = bcrypt.CompareHashAndPassword(savedPwd, []byte(pwd)) 70 | if err != nil { 71 | if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { 72 | return ErrWrongPassword 73 | } 74 | return err 75 | } 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /pkg/session/manager.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/zalopay-oss/tokeny/pkg/keyvalue" 10 | ) 11 | 12 | const ( 13 | keySessionPrefix = "session:" 14 | sessionDurationSec = 300 15 | ) 16 | 17 | type manager struct { 18 | kvStore keyvalue.Store 19 | } 20 | 21 | func NewManager(kvStore keyvalue.Store) *manager { 22 | result := &manager{kvStore: kvStore} 23 | err := result.cleanUp() 24 | if err != nil { 25 | println(err.Error()) 26 | } 27 | return result 28 | } 29 | 30 | func (m *manager) IsSessionValid(sessionKey string) (bool, error) { 31 | key := m.composeSessionKey(sessionKey) 32 | expiredTSStr, err := m.kvStore.Get(key) 33 | if err != nil { 34 | if errors.Is(err, keyvalue.ErrNoRecord) { 35 | return false, nil 36 | } 37 | return false, err 38 | } 39 | expired, err := m.isTimeStringExpired(expiredTSStr) 40 | if err != nil { 41 | return false, err 42 | } 43 | return !expired, nil 44 | } 45 | 46 | func (m *manager) NewSession(sessionKey string) error { 47 | key := m.composeSessionKey(sessionKey) 48 | expiredTS := time.Now().Unix() + sessionDurationSec 49 | return m.kvStore.Set(key, fmt.Sprintf("%d", expiredTS)) 50 | } 51 | 52 | func (m *manager) composeSessionKey(sessionKey string) string { 53 | return keySessionPrefix + sessionKey 54 | } 55 | 56 | func (m *manager) cleanUp() error { 57 | kvs, err := m.kvStore.GetAllWithPrefixed(keySessionPrefix) 58 | if err != nil { 59 | return err 60 | } 61 | for _, kv := range kvs { 62 | expired, err := m.isTimeStringExpired(kv.Value) 63 | if err != nil { 64 | return err 65 | } 66 | if !expired { 67 | continue 68 | } 69 | err = m.kvStore.Delete(kv.Key) 70 | if err != nil { 71 | return err 72 | } 73 | } 74 | return nil 75 | } 76 | 77 | func (m *manager) isTimeStringExpired(tsStr string) (bool, error) { 78 | expiredTS, err := strconv.ParseInt(tsStr, 10, 64) 79 | if err != nil { 80 | return false, err 81 | } 82 | return expiredTS < time.Now().Unix(), nil 83 | } 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### macOS template 3 | # General 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | ### Go template 31 | # Binaries for programs and plugins 32 | *.exe 33 | *.exe~ 34 | *.dll 35 | *.so 36 | *.dylib 37 | 38 | # Test binary, built with `go test -c` 39 | *.test 40 | 41 | # Output of the go coverage tool, specifically when used with LiteIDE 42 | *.out 43 | 44 | # Dependency directories (remove the comment below to include it) 45 | vendor/ 46 | 47 | ### JetBrains template 48 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 49 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 50 | 51 | # User-specific stuff 52 | .idea/**/workspace.xml 53 | .idea/**/tasks.xml 54 | .idea/**/usage.statistics.xml 55 | .idea/**/dictionaries 56 | .idea/**/shelf 57 | .idea/** 58 | 59 | 60 | # Generated files 61 | .idea/**/contentModel.xml 62 | 63 | # Sensitive or high-churn files 64 | .idea/**/dataSources/ 65 | .idea/**/dataSources.ids 66 | .idea/**/dataSources.local.xml 67 | .idea/**/sqlDataSources.xml 68 | .idea/**/dynamic.xml 69 | .idea/**/uiDesigner.xml 70 | .idea/**/dbnavigator.xml 71 | 72 | # Gradle 73 | .idea/**/gradle.xml 74 | .idea/**/libraries 75 | 76 | # Gradle and Maven with auto-import 77 | # When using Gradle or Maven with auto-import, you should exclude module files, 78 | # since they will be recreated, and may cause churn. Uncomment if using 79 | # auto-import. 80 | .idea/artifacts 81 | .idea/compiler.xml 82 | .idea/jarRepositories.xml 83 | .idea/modules.xml 84 | .idea/*.iml 85 | .idea/modules 86 | *.iml 87 | *.ipr 88 | 89 | # CMake 90 | cmake-build-*/ 91 | 92 | # Mongo Explorer plugin 93 | .idea/**/mongoSettings.xml 94 | 95 | # File-based project format 96 | *.iws 97 | 98 | # IntelliJ 99 | out/ 100 | 101 | # Goreleaser 102 | dist/ 103 | 104 | # mpeltonen/sbt-idea plugin 105 | .idea_modules/ 106 | 107 | # JIRA plugin 108 | atlassian-ide-plugin.xml 109 | 110 | # Cursive Clojure plugin 111 | .idea/replstate.xml 112 | 113 | # Crashlytics plugin (for Android Studio and IntelliJ) 114 | com_crashlytics_export_strings.xml 115 | crashlytics.properties 116 | crashlytics-build.properties 117 | fabric.properties 118 | 119 | # Editor-based Rest Client 120 | .idea/httpRequests 121 | 122 | # Android studio 3.1+ serialized cache file 123 | .idea/caches/build_file_checksums.ser 124 | 125 | /tokeny 126 | 127 | # VSCode 128 | __debug_bin 129 | .vscode 130 | -------------------------------------------------------------------------------- /pkg/keyvalue/mock/interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: interface.go 3 | 4 | // Package mock_keyvalue is a generated GoMock package. 5 | package mock_keyvalue 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | keyvalue "github.com/zalopay-oss/tokeny/pkg/keyvalue" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockStore is a mock of Store interface 14 | type MockStore struct { 15 | ctrl *gomock.Controller 16 | recorder *MockStoreMockRecorder 17 | } 18 | 19 | // MockStoreMockRecorder is the mock recorder for MockStore 20 | type MockStoreMockRecorder struct { 21 | mock *MockStore 22 | } 23 | 24 | // NewMockStore creates a new mock instance 25 | func NewMockStore(ctrl *gomock.Controller) *MockStore { 26 | mock := &MockStore{ctrl: ctrl} 27 | mock.recorder = &MockStoreMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockStore) EXPECT() *MockStoreMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // Set mocks base method 37 | func (m *MockStore) Set(key, value string) error { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "Set", key, value) 40 | ret0, _ := ret[0].(error) 41 | return ret0 42 | } 43 | 44 | // Set indicates an expected call of Set 45 | func (mr *MockStoreMockRecorder) Set(key, value interface{}) *gomock.Call { 46 | mr.mock.ctrl.T.Helper() 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockStore)(nil).Set), key, value) 48 | } 49 | 50 | // Get mocks base method 51 | func (m *MockStore) Get(key string) (string, error) { 52 | m.ctrl.T.Helper() 53 | ret := m.ctrl.Call(m, "Get", key) 54 | ret0, _ := ret[0].(string) 55 | ret1, _ := ret[1].(error) 56 | return ret0, ret1 57 | } 58 | 59 | // Get indicates an expected call of Get 60 | func (mr *MockStoreMockRecorder) Get(key interface{}) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStore)(nil).Get), key) 63 | } 64 | 65 | // Delete mocks base method 66 | func (m *MockStore) Delete(key string) error { 67 | m.ctrl.T.Helper() 68 | ret := m.ctrl.Call(m, "Delete", key) 69 | ret0, _ := ret[0].(error) 70 | return ret0 71 | } 72 | 73 | // Delete indicates an expected call of Delete 74 | func (mr *MockStoreMockRecorder) Delete(key interface{}) *gomock.Call { 75 | mr.mock.ctrl.T.Helper() 76 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockStore)(nil).Delete), key) 77 | } 78 | 79 | // GetAllWithPrefixed mocks base method 80 | func (m *MockStore) GetAllWithPrefixed(keyPrefix string) ([]keyvalue.KeyValue, error) { 81 | m.ctrl.T.Helper() 82 | ret := m.ctrl.Call(m, "GetAllWithPrefixed", keyPrefix) 83 | ret0, _ := ret[0].([]keyvalue.KeyValue) 84 | ret1, _ := ret[1].(error) 85 | return ret0, ret1 86 | } 87 | 88 | // GetAllWithPrefixed indicates an expected call of GetAllWithPrefixed 89 | func (mr *MockStoreMockRecorder) GetAllWithPrefixed(keyPrefix interface{}) *gomock.Call { 90 | mr.mock.ctrl.T.Helper() 91 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllWithPrefixed", reflect.TypeOf((*MockStore)(nil).GetAllWithPrefixed), keyPrefix) 92 | } 93 | -------------------------------------------------------------------------------- /pkg/tokeny/repository.go: -------------------------------------------------------------------------------- 1 | package tokeny 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "unicode" 7 | 8 | "github.com/zalopay-oss/tokeny/pkg/keyvalue" 9 | "github.com/zalopay-oss/tokeny/pkg/totp" 10 | ) 11 | 12 | const ( 13 | entryKeyPrefix = "entry:" 14 | lastValidKey = "last_valid" 15 | ) 16 | 17 | type repository struct { 18 | kvStore keyvalue.Store 19 | } 20 | 21 | func NewRepository(kvStore keyvalue.Store) *repository { 22 | return &repository{kvStore: kvStore} 23 | } 24 | 25 | func (r *repository) Add(alias string, secret string) error { 26 | key := r.composeEntryKey(alias) 27 | _, err := r.kvStore.Get(key) 28 | if err == nil { 29 | return ErrEntryExistedBefore 30 | } 31 | if !errors.Is(err, keyvalue.ErrNoRecord) { 32 | return err 33 | } 34 | secret = strings.Map(func(r rune) rune { 35 | if unicode.IsSpace(r) { 36 | return -1 37 | } 38 | return r 39 | }, secret) 40 | return r.kvStore.Set(key, secret) 41 | } 42 | 43 | func (r *repository) Generate(alias string) (totp.Token, error) { 44 | key := r.composeEntryKey(alias) 45 | secret, err := r.kvStore.Get(key) 46 | if err != nil { 47 | if errors.Is(err, keyvalue.ErrNoRecord) { 48 | return totp.Token{}, ErrNoEntryFound 49 | } 50 | return totp.Token{}, err 51 | } 52 | g, err := totp.NewGenerator(secret) 53 | if err != nil { 54 | return totp.Token{}, err 55 | } 56 | result := g.Generate() 57 | err = r.rememberLastValidEntry(alias) 58 | if err != nil { 59 | return totp.Token{}, err 60 | } 61 | return result, nil 62 | } 63 | 64 | func (r *repository) Delete(alias string) error { 65 | key := r.composeEntryKey(alias) 66 | _, err := r.kvStore.Get(key) 67 | if err != nil { 68 | if errors.Is(err, keyvalue.ErrNoRecord) { 69 | return ErrNoEntryFound 70 | } 71 | return err 72 | } 73 | err = r.kvStore.Delete(key) 74 | if err != nil { 75 | return err 76 | } 77 | err = r.removeLastValidIfEqual(alias) 78 | if err != nil { 79 | return err 80 | } 81 | return nil 82 | } 83 | 84 | func (r *repository) List() ([]string, error) { 85 | kvs, err := r.kvStore.GetAllWithPrefixed(entryKeyPrefix) 86 | if err != nil { 87 | return nil, err 88 | } 89 | result := make([]string, len(kvs)) 90 | for i, kv := range kvs { 91 | result[i] = strings.TrimPrefix(kv.Key, entryKeyPrefix) 92 | } 93 | return result, nil 94 | } 95 | 96 | func (r *repository) removeLastValidIfEqual(alias string) error { 97 | lastValid, err := r.kvStore.Get(lastValidKey) 98 | if err != nil { 99 | if errors.Is(err, keyvalue.ErrNoRecord) { 100 | return nil 101 | } 102 | return err 103 | } 104 | if alias != lastValid { 105 | return nil 106 | } 107 | return r.kvStore.Delete(lastValidKey) 108 | } 109 | 110 | func (r *repository) rememberLastValidEntry(alias string) error { 111 | return r.kvStore.Set(lastValidKey, alias) 112 | } 113 | 114 | func (r *repository) LastValidEntry() (string, error) { 115 | result, err := r.kvStore.Get(lastValidKey) 116 | if err != nil { 117 | if errors.Is(err, keyvalue.ErrNoRecord) { 118 | return "", ErrNoEntryFound 119 | } 120 | return "", err 121 | } 122 | return result, nil 123 | } 124 | 125 | func (r *repository) composeEntryKey(alias string) string { 126 | return entryKeyPrefix + alias 127 | } 128 | -------------------------------------------------------------------------------- /pkg/password/manager_test.go: -------------------------------------------------------------------------------- 1 | package password 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/hex" 6 | "github.com/golang/mock/gomock" 7 | "github.com/zalopay-oss/tokeny/pkg/keyvalue" 8 | mock_keyvalue "github.com/zalopay-oss/tokeny/pkg/keyvalue/mock" 9 | "github.com/stretchr/testify/assert" 10 | "golang.org/x/crypto/bcrypt" 11 | "testing" 12 | ) 13 | 14 | func TestKeyPasswordValue(t *testing.T) { 15 | assert.Equal(t, "password", keyPassword) 16 | } 17 | 18 | func TestManager_IsRegistered(t *testing.T) { 19 | tests := []struct { 20 | name string 21 | kvExpectedResult string 22 | kvExpectedError error 23 | expectedResult bool 24 | expectedError error 25 | }{ 26 | { 27 | name: "Success", 28 | kvExpectedResult: "dump result", 29 | kvExpectedError: nil, 30 | expectedResult: true, 31 | expectedError: nil, 32 | }, 33 | { 34 | name: "Not found", 35 | kvExpectedResult: "dump result", 36 | kvExpectedError: keyvalue.ErrNoRecord, 37 | expectedResult: false, 38 | expectedError: nil, 39 | }, 40 | { 41 | name: "Unknown error", 42 | kvExpectedResult: "dump result", 43 | kvExpectedError: sql.ErrNoRows, 44 | expectedResult: false, 45 | expectedError: sql.ErrNoRows, 46 | }, 47 | } 48 | for _, test := range tests { 49 | t.Run(test.name, func(t *testing.T) { 50 | ctrl := gomock.NewController(t) 51 | kvMock := mock_keyvalue.NewMockStore(ctrl) 52 | defer ctrl.Finish() 53 | 54 | kvMock. 55 | EXPECT(). 56 | Get(keyPassword). 57 | Return(test.kvExpectedResult, test.kvExpectedError). 58 | Times(1) 59 | 60 | manager := NewManager(kvMock) 61 | result, err := manager.IsRegistered() 62 | if test.expectedError != nil { 63 | assert.Error(t, err) 64 | assert.Equal(t, test.expectedError, err) 65 | } else { 66 | assert.NoError(t, err) 67 | assert.Equal(t, test. expectedResult, result) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestManager_Register(t *testing.T) { 74 | tests := []struct { 75 | name string 76 | password string 77 | rePassword string 78 | kvStoreExpectedCall int 79 | kvStoreExpectedError error 80 | expectedError error 81 | }{ 82 | { 83 | name: "Passwords mismatch", 84 | password: "123", 85 | rePassword: "234", 86 | kvStoreExpectedCall: 0, 87 | kvStoreExpectedError: nil, 88 | expectedError: ErrPasswordsMismatch, 89 | }, 90 | { 91 | name: "KVStore error", 92 | password: "123", 93 | rePassword: "123", 94 | kvStoreExpectedCall: 1, 95 | kvStoreExpectedError: sql.ErrNoRows, 96 | expectedError: sql.ErrNoRows, 97 | }, 98 | { 99 | name: "Success", 100 | password: "123", 101 | rePassword: "123", 102 | kvStoreExpectedCall: 1, 103 | kvStoreExpectedError: nil, 104 | expectedError: nil, 105 | }, 106 | } 107 | for _, test := range tests { 108 | t.Run(test.name, func(t *testing.T) { 109 | ctrl := gomock.NewController(t) 110 | defer ctrl.Finish() 111 | kvMock := mock_keyvalue.NewMockStore(ctrl) 112 | 113 | kvMock.EXPECT(). 114 | Set(keyPassword, gomock.Any()). 115 | Return(test.kvStoreExpectedError). 116 | Times(test.kvStoreExpectedCall) 117 | 118 | err := NewManager(kvMock).Register(test.password, test.rePassword) 119 | if test.expectedError != nil { 120 | assert.Error(t, err) 121 | assert.Equal(t, test.expectedError, err) 122 | } else { 123 | assert.NoError(t, err) 124 | } 125 | }) 126 | } 127 | } 128 | 129 | func TestManager_Login(t *testing.T) { 130 | tests := []struct { 131 | name string 132 | inputPassword string 133 | correctPassword string 134 | kvError error 135 | expectedError error 136 | }{ 137 | { 138 | name: "Not registered", 139 | inputPassword: "123", 140 | correctPassword: "", 141 | kvError: keyvalue.ErrNoRecord, 142 | expectedError: ErrNotRegistered, 143 | }, 144 | { 145 | name: "KVStore error", 146 | inputPassword: "123", 147 | correctPassword: "", 148 | kvError: sql.ErrConnDone, 149 | expectedError: sql.ErrConnDone, 150 | }, 151 | { 152 | name: "Password mismatch", 153 | inputPassword: "123", 154 | correctPassword: "456", 155 | kvError: nil, 156 | expectedError: ErrWrongPassword, 157 | }, 158 | { 159 | name: "Success", 160 | inputPassword: "123", 161 | correctPassword: "123", 162 | kvError: nil, 163 | expectedError: nil, 164 | }, 165 | } 166 | for _, test := range tests { 167 | t.Run(test.name, func(t *testing.T) { 168 | hashedPass, err := bcrypt.GenerateFromPassword([]byte(test.correctPassword), bcrypt.DefaultCost) 169 | assert.NoError(t, err) 170 | hashedPassString := hex.EncodeToString(hashedPass) 171 | 172 | ctrl := gomock.NewController(t) 173 | defer ctrl.Finish() 174 | kvMock := mock_keyvalue.NewMockStore(ctrl) 175 | 176 | kvMock.EXPECT(). 177 | Get(keyPassword). 178 | Return(hashedPassString, test.kvError). 179 | Times(1) 180 | 181 | err = NewManager(kvMock).Login(test.inputPassword) 182 | if test.expectedError != nil { 183 | assert.Error(t, err) 184 | assert.Equal(t, test.expectedError, err) 185 | } else { 186 | assert.NoError(t, err) 187 | } 188 | }) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /pkg/tokenycli/service.go: -------------------------------------------------------------------------------- 1 | package tokenycli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/atotto/clipboard" 8 | "github.com/manifoldco/promptui" 9 | "github.com/pkg/errors" 10 | "github.com/urfave/cli/v2" 11 | "github.com/zalopay-oss/tokeny/pkg/password" 12 | "github.com/zalopay-oss/tokeny/pkg/session" 13 | "github.com/zalopay-oss/tokeny/pkg/tokeny" 14 | ) 15 | 16 | var ( 17 | ppidStr = fmt.Sprintf("%d", os.Getppid()) 18 | ) 19 | 20 | type service struct { 21 | pwdManager password.Manager 22 | sessionManager session.Manager 23 | tokenRepo tokeny.Repository 24 | } 25 | 26 | func NewService(pwdManager password.Manager, sessionManager session.Manager, tokenRepo tokeny.Repository) *service { 27 | return &service{ 28 | pwdManager: pwdManager, 29 | sessionManager: sessionManager, 30 | tokenRepo: tokenRepo, 31 | } 32 | } 33 | 34 | func (s *service) Register(app *cli.App) error { 35 | userProfileAvailable, err := s.pwdManager.IsRegistered() 36 | if err != nil { 37 | return err 38 | } 39 | 40 | if !userProfileAvailable { 41 | app.Commands = s.getSetupCommand() 42 | } else { 43 | app.Commands = s.getNormalCommands() 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func (s *service) getSetupCommand() []*cli.Command { 50 | return []*cli.Command{ 51 | { 52 | Name: "setup", 53 | Usage: "setup master password", 54 | Action: s.setup, 55 | }, 56 | } 57 | } 58 | 59 | func (s *service) getNormalCommands() []*cli.Command { 60 | return []*cli.Command{ 61 | { 62 | Name: "add", 63 | Usage: "add new entry", 64 | Flags: []cli.Flag{ 65 | &cli.StringFlag{ 66 | Name: "alias", 67 | Aliases: []string{"a"}, 68 | Required: true, 69 | Usage: "entry name/alias, must be identical to each other", 70 | }, 71 | &cli.StringFlag{ 72 | Name: "secret", 73 | Aliases: []string{"s"}, 74 | Required: true, 75 | Usage: "secret of the entry", 76 | }, 77 | }, 78 | Action: s.sessionWrapper(s.add), 79 | }, 80 | { 81 | Name: "get", 82 | Usage: "get OTP", 83 | Flags: []cli.Flag{ 84 | &cli.BoolFlag{ 85 | Name: "copy", 86 | Aliases: []string{"c"}, 87 | Required: false, 88 | Usage: "copy generated token to clipboard", 89 | }, 90 | }, 91 | Action: s.sessionWrapper(s.get), 92 | }, 93 | { 94 | Name: "delete", 95 | Usage: "delete selected entry", 96 | Action: s.sessionWrapper(s.delete), 97 | }, 98 | { 99 | Name: "list", 100 | Usage: "list all entries", 101 | Action: s.sessionWrapper(s.list), 102 | }, 103 | } 104 | } 105 | 106 | func (s *service) setup(c *cli.Context) error { 107 | registered, err := s.pwdManager.IsRegistered() 108 | if err != nil { 109 | return err 110 | } 111 | if registered { 112 | println("You have registered already.") 113 | return nil 114 | } 115 | return s.doRegister() 116 | } 117 | 118 | func (s *service) doRegister() error { 119 | prompt := promptui.Prompt{ 120 | Label: "Password", 121 | Mask: ' ', 122 | } 123 | 124 | pwd, err := prompt.Run() 125 | 126 | if err != nil { 127 | return err 128 | } 129 | 130 | prompt = promptui.Prompt{ 131 | Label: "Re-type password", 132 | Mask: ' ', 133 | } 134 | 135 | rePwd, err := prompt.Run() 136 | 137 | if err != nil { 138 | return err 139 | } 140 | 141 | err = s.pwdManager.Register(pwd, rePwd) 142 | if err != nil { 143 | if errors.Is(err, password.ErrPasswordsMismatch) { 144 | println("Passwords do not match, please try again.") 145 | return nil 146 | } 147 | return err 148 | } 149 | 150 | println("Registered.") 151 | return nil 152 | } 153 | 154 | func (s *service) sessionWrapper(actionFunc cli.ActionFunc) cli.ActionFunc { 155 | return func(c *cli.Context) error { 156 | if valid, err := s.ensureSession(); err != nil || !valid { 157 | return err 158 | } 159 | return actionFunc(c) 160 | } 161 | } 162 | 163 | func (s *service) add(c *cli.Context) error { 164 | alias := c.String("alias") 165 | secret := c.String("secret") 166 | err := s.tokenRepo.Add(alias, secret) 167 | if err != nil { 168 | if errors.Is(err, tokeny.ErrEntryExistedBefore) { 169 | println("Alias has been used before, please choose another.") 170 | return nil 171 | } 172 | return err 173 | } 174 | println("Entry has been added successfully.") 175 | return nil 176 | } 177 | 178 | func (s *service) get(c *cli.Context) error { 179 | var alias string 180 | if c.NArg() > 0 { 181 | alias = c.Args().Get(0) 182 | } else { 183 | var err error 184 | alias, err = s.tokenRepo.LastValidEntry() 185 | if errors.Is(err, tokeny.ErrNoEntryFound) { 186 | println("Please specify entry to generate token: tokeny get ") 187 | return nil 188 | } 189 | if err != nil { 190 | return err 191 | } 192 | } 193 | t, err := s.tokenRepo.Generate(alias) 194 | if err != nil { 195 | if errors.Is(err, tokeny.ErrNoEntryFound) { 196 | println("Invalid entry, please choose another.") 197 | return nil 198 | } 199 | return err 200 | } 201 | secString := "second" 202 | if t.TimeoutSec > 1 { 203 | secString += "s" 204 | } 205 | fmt.Printf("Here is your token for '%s', valid within the next %d %s\n", alias, t.TimeoutSec, secString) 206 | println(t.Value) 207 | if c.Bool("copy") { 208 | err := clipboard.WriteAll(t.Value) 209 | if err != nil { 210 | println("Cannot copy to clipboard.") 211 | } else { 212 | println("Copied to clipboard.") 213 | } 214 | } 215 | return nil 216 | } 217 | 218 | func (s *service) delete(c *cli.Context) error { 219 | if c.NArg() == 0 { 220 | println("Please specify entry to be deleted.") 221 | return nil 222 | } 223 | alias := c.Args().Get(0) 224 | 225 | err := s.tokenRepo.Delete(alias) 226 | if err != nil { 227 | if errors.Is(err, tokeny.ErrNoEntryFound) { 228 | println("Invalid entry, please choose another.") 229 | return nil 230 | } 231 | return err 232 | } 233 | println("Deleted.") 234 | return nil 235 | } 236 | 237 | func (s *service) list(c *cli.Context) error { 238 | aliases, err := s.tokenRepo.List() 239 | if err != nil { 240 | return err 241 | } 242 | 243 | if len(aliases) == 0 { 244 | println("No entry.") 245 | return nil 246 | } 247 | 248 | if len(aliases) == 1 { 249 | println("Here is your entry:") 250 | } else { 251 | println("Here are your entries:") 252 | } 253 | 254 | for _, alias := range aliases { 255 | println(alias) 256 | } 257 | 258 | return nil 259 | } 260 | 261 | func (s *service) ensureSession() (bool, error) { 262 | valid, err := s.sessionManager.IsSessionValid(ppidStr) 263 | if err != nil { 264 | return false, err 265 | } 266 | 267 | if valid { 268 | return true, nil 269 | } 270 | 271 | err = s.doLogin() 272 | if err != nil { 273 | if errors.Is(err, password.ErrWrongPassword) { 274 | println("Wrong password, please try again.") 275 | return false, nil 276 | } 277 | return false, err 278 | } 279 | 280 | err = s.sessionManager.NewSession(ppidStr) 281 | if err != nil { 282 | return false, err 283 | } 284 | 285 | return true, nil 286 | } 287 | 288 | func (s *service) doLogin() error { 289 | prompt := promptui.Prompt{ 290 | Label: "Password", 291 | Mask: ' ', 292 | } 293 | 294 | result, err := prompt.Run() 295 | 296 | if err != nil { 297 | return err 298 | } 299 | 300 | err = s.pwdManager.Login(result) 301 | if err != nil { 302 | return err 303 | } 304 | return nil 305 | } 306 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= 3 | github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 4 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 5 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 6 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 7 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 8 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 9 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 10 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 11 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 12 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 15 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 16 | github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= 17 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 18 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 19 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 20 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= 21 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 22 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 23 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 24 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= 25 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= 26 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 27 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 28 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 29 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 30 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 31 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= 32 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 33 | github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4= 34 | github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= 35 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 36 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 37 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 38 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 39 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 40 | github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= 41 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 42 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= 43 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 44 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 45 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 46 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 47 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 48 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 49 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 50 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 51 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 52 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 53 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 54 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= 56 | github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= 57 | github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= 58 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 59 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 60 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 61 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 62 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 63 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 64 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 65 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= 66 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 67 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 68 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 69 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 71 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 72 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 73 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= 75 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 77 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 78 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 80 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 81 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 83 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 84 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 85 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 86 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 87 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 88 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 89 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 90 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 91 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This file contains all available configuration options 2 | # with their default values. 3 | 4 | # options for analysis running 5 | run: 6 | # default concurrency is a available CPU number 7 | concurrency: 2 8 | 9 | # timeout for analysis, e.g. 30s, 5m, default is 1m 10 | timeout: 50m 11 | 12 | # exit code when at least one issue was found, default is 1 13 | issues-exit-code: 1 14 | 15 | # include test files or not, default is true 16 | tests: false 17 | 18 | # list of build tags, all linters use it. Default is empty list. 19 | # build-tags: 20 | # - mytag 21 | 22 | # which dirs to skip: issues from them won't be reported; 23 | # can use regexp here: generated.*, regexp is applied on full path; 24 | # default value is empty list, but default dirs are skipped independently 25 | # from this option's value (see skip-dirs-use-default). 26 | # skip-dirs: 27 | 28 | 29 | # default is true. Enables skipping of directories: 30 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 31 | skip-dirs-use-default: true 32 | 33 | # which files to skip: they will be analyzed, but issues from them 34 | # won't be reported. Default value is empty list, but there is 35 | # no need to include all autogenerated files, we confidently recognize 36 | # autogenerated files. If it's not please let us know. 37 | # skip-files: 38 | # - ".*\\.my\\.go$" 39 | # - lib/bad.go 40 | 41 | # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": 42 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 43 | # automatic updating of go.mod described above. Instead, it fails when any changes 44 | # to go.mod are needed. This setting is most useful to check that go.mod does 45 | # not need updates, such as in a continuous integration and testing system. 46 | # If invoked with -mod=vendor, the go command assumes that the vendor 47 | # directory holds the correct copies of dependencies and ignores 48 | # the dependency descriptions in go.mod. 49 | # modules-download-mode: vendor 50 | 51 | 52 | # output configuration options 53 | output: 54 | # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" 55 | format: colored-line-number 56 | 57 | # print lines of code with issue, default is true 58 | print-issued-lines: true 59 | 60 | # print linter name in the end of issue text, default is true 61 | print-linter-name: true 62 | 63 | 64 | ## all available settings of specific linters 65 | linters-settings: 66 | # errcheck: 67 | # # report about not checking of errors in type assetions: `a := b.(MyStruct)`; 68 | # # default is false: such cases aren't reported by default. 69 | # check-type-assertions: false 70 | # 71 | # # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 72 | # # default is false: such cases aren't reported by default. 73 | # check-blank: false 74 | # 75 | # # [deprecated] comma-separated list of pairs of the form pkg:regex 76 | # # the regex is used to ignore names within pkg. (default "fmt:.*"). 77 | # # see https://github.com/kisielk/errcheck#the-deprecated-method for details 78 | # ignore: fmt:.*,io/ioutil:^Read.* 79 | # 80 | # # path to a file containing a list of functions to exclude from checking 81 | # # see https://github.com/kisielk/errcheck#excluding-functions for details 82 | # exclude: /path/to/file.txt 83 | # 84 | funlen: 85 | lines: 60 86 | statements: 40 87 | # 88 | # govet: 89 | # # report about shadowed variables 90 | # check-shadowing: true 91 | # 92 | # # settings per analyzer 93 | # settings: 94 | # printf: # analyzer name, run `go tool vet help` to see all analyzers 95 | # funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer 96 | # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 97 | # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 98 | # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 99 | # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 100 | # 101 | # # enable or disable analyzers by name 102 | # enable: 103 | # - atomicalign 104 | # enable-all: false 105 | # disable: 106 | # - shadow 107 | # disable-all: false 108 | # golint: 109 | # # minimal confidence for issues, default is 0.8 110 | # min-confidence: 0.8 111 | # gofmt: 112 | # # simplify code: gofmt with `-s` option, true by default 113 | # simplify: true 114 | # goimports: 115 | # # put imports beginning with prefix after 3rd-party packages; 116 | # # it's a comma-separated list of prefixes 117 | # local-prefixes: github.com/org/project 118 | # gocyclo: 119 | # # minimal code complexity to report, 30 by default (but we recommend 10-20) 120 | # min-complexity: 10 121 | # gocognit: 122 | # # minimal code complexity to report, 30 by default (but we recommend 10-20) 123 | # min-complexity: 10 124 | # maligned: 125 | # # print struct with more effective memory layout or not, false by default 126 | # suggest-new: true 127 | # dupl: 128 | # # tokens count to trigger issue, 150 by default 129 | # threshold: 100 130 | # goconst: 131 | # # minimal length of string constant, 3 by default 132 | # min-len: 3 133 | # # minimal occurrences count to trigger, 3 by default 134 | # min-occurrences: 3 135 | # depguard: 136 | # list-type: blacklist 137 | # include-go-root: false 138 | # packages: 139 | # - github.com/sirupsen/logrus 140 | # packages-with-error-messages: 141 | # # specify an error message to output when a blacklisted package is used 142 | # github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" 143 | # misspell: 144 | # # Correct spellings using locale preferences for US or UK. 145 | # # Default is to use a neutral variety of English. 146 | # # Setting locale to US will correct the British spelling of 'colour' to 'color'. 147 | # locale: US 148 | # ignore-words: 149 | # - someword 150 | # lll: 151 | # # max line length, lines longer will be reported. Default is 120. 152 | # # '\t' is counted as 1 character by default, and can be changed with the tab-width option 153 | # line-length: 120 154 | # # tab width in spaces. Default to 1. 155 | # tab-width: 1 156 | # unused: 157 | # # treat code as a program (not a library) and report unused exported identifiers; default is false. 158 | # # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: 159 | # # if it's called for subdir of a project it can't find funcs usages. All text editor integrations 160 | # # with golangci-lint call it on a directory with the changed file. 161 | # check-exported: false 162 | # unparam: 163 | # # Inspect exported functions, default is false. Set to true if no external program/library imports your code. 164 | # # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 165 | # # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 166 | # # with golangci-lint call it on a directory with the changed file. 167 | # check-exported: false 168 | nakedret: 169 | # # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 170 | max-func-lines: 60 171 | # prealloc: 172 | # # XXX: we don't recommend using this linter before doing performance profiling. 173 | # # For most programs usage of prealloc will be a premature optimization. 174 | # 175 | # # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 176 | # # True by default. 177 | # simple: true 178 | # range-loops: true # Report preallocation suggestions on range loops, true by default 179 | # for-loops: false # Report preallocation suggestions on for loops, false by default 180 | # gocritic: 181 | # # Which checks should be enabled; can't be combined with 'disabled-checks'; 182 | # # See https://go-critic.github.io/overview#checks-overview 183 | # # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` 184 | # # By default list of stable checks is used. 185 | # enabled-checks: 186 | ## - rangeValCopy 187 | # 188 | # # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty 189 | # disabled-checks: 190 | # - regexpMust 191 | # 192 | # # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. 193 | # # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". 194 | # enabled-tags: 195 | # - performance 196 | # 197 | # settings: # settings passed to gocritic 198 | # captLocal: # must be valid enabled check name 199 | # paramsOnly: true 200 | # rangeValCopy: 201 | # sizeThreshold: 32 202 | # godox: 203 | # # report any comments starting with keywords, this is useful for TODO or FIXME comments that 204 | # # might be left in the code accidentally and should be resolved before merging 205 | # keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting 206 | # - NOTE 207 | # - OPTIMIZE # marks code that should be optimized before merging 208 | # - HACK # marks hack-arounds that should be removed before merging 209 | # dogsled: 210 | # # checks assignments with too many blank identifiers; default is 2 211 | # max-blank-identifiers: 2 212 | # 213 | # whitespace: 214 | # multi-if: false # Enforces newlines (or comments) after every multi-line if statement 215 | # multi-func: false # Enforces newlines (or comments) after every multi-line function signature 216 | # wsl: 217 | # # If true append is only allowed to be cuddled if appending value is 218 | # # matching variables, fields or types on line above. Default is true. 219 | # strict-append: true 220 | # # Allow calls and assignments to be cuddled as long as the lines have any 221 | # # matching variables, fields or types. Default is true. 222 | # allow-assign-and-call: true 223 | # # Allow multiline assignments to be cuddled. Default is true. 224 | # allow-multiline-assign: true 225 | # # Allow declarations (var) to be cuddled. 226 | # allow-cuddle-declarations: false 227 | # # Allow trailing comments in ending of blocks 228 | # allow-trailing-comment: false 229 | # # Force newlines in end of case at this limit (0 = never). 230 | # force-case-trailing-whitespace: 0 231 | 232 | linters: 233 | disable-all: true 234 | enable: 235 | - bodyclose 236 | - deadcode 237 | - depguard 238 | - dogsled 239 | - dupl 240 | - errcheck 241 | - funlen 242 | - gochecknoinits 243 | - goconst 244 | - gocritic 245 | - gocyclo 246 | - gofmt 247 | - goimports 248 | - gomnd 249 | - goprintffuncname 250 | - gosec 251 | - gosimple 252 | - govet 253 | - ineffassign 254 | - interfacer 255 | - lll 256 | - misspell 257 | - nakedret 258 | - rowserrcheck 259 | - scopelint 260 | - staticcheck 261 | - structcheck 262 | - stylecheck 263 | - typecheck 264 | - unconvert 265 | - unparam 266 | - unused 267 | - varcheck 268 | - whitespace 269 | presets: 270 | - bugs 271 | - unused 272 | fast: false 273 | 274 | 275 | issues: 276 | # List of regexps of issue texts to exclude, empty list by default. 277 | # But independently from this option we use default exclude patterns, 278 | # it can be disabled by `exclude-use-default: false`. To list all 279 | # excluded by default patterns execute `golangci-lint run --help` 280 | exclude: 281 | - abcdef 282 | 283 | # Excluding configuration per-path, per-linter, per-text and per-source 284 | exclude-rules: 285 | - path: pkg/step 286 | linters: 287 | - stylecheck 288 | 289 | - path: _test\.go 290 | linters: 291 | - gocyclo 292 | - errcheck 293 | - dupl 294 | - gosec 295 | 296 | # Exclude known linters from partially hard-vendored code, 297 | # which is impossible to exclude via "nolint" comments. 298 | - path: internal/hmac/ 299 | text: "weak cryptographic primitive" 300 | linters: 301 | - gosec 302 | 303 | # Exclude some staticcheck messages 304 | - linters: 305 | - staticcheck 306 | text: "SA9003:" 307 | 308 | - linters: 309 | - stylecheck 310 | text: "ST1000:|ST1021:|ST1020:" 311 | 312 | - linters: 313 | - golint 314 | text: "should have comment" 315 | 316 | # Exclude lll issues for long lines with go:generate 317 | - linters: 318 | - lll 319 | source: "^//go:generate " 320 | 321 | - path: zrc\/pkg\/common\/json\.go 322 | linters: 323 | - gosec 324 | 325 | # Independently from option `exclude` we use default exclude patterns, 326 | # it can be disabled by this option. To list all 327 | # excluded by default patterns execute `golangci-lint run --help`. 328 | # Default value for this option is true. 329 | exclude-use-default: false 330 | 331 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 332 | max-issues-per-linter: 0 333 | 334 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 335 | max-same-issues: 0 336 | 337 | # Show only new issues: if there are unstaged changes or untracked files, 338 | # only those changes are analyzed, else only changes in HEAD~ are analyzed. 339 | # It's a super-useful option for integration of golangci-lint into existing 340 | # large codebase. It's not practical to fix all existing issues at the moment 341 | # of integration: much better don't allow issues in new code. 342 | # Default is false. 343 | new: false 344 | 345 | # Show only new issues created after git revision `REV` 346 | #new-from-rev: REV 347 | 348 | # Show only new issues created in git patch with set file path. 349 | #new-from-patch: path/to/patch/file 350 | --------------------------------------------------------------------------------