Pull dev into main (#24)

This commit is contained in:
Ricky Pike
2022-07-09 12:11:25 -05:00
committed by GitHub
parent 408aaecf59
commit f825720660
53 changed files with 2882 additions and 1768 deletions

8
.codecov.yml Normal file
View File

@@ -0,0 +1,8 @@
coverage:
status:
project:
default:
target: 80%
patch:
default:
target: 80%

12
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5

View File

@@ -2,31 +2,41 @@ on: [push, pull_request]
name: Build
jobs:
test:
name: Build and Test
strategy:
matrix:
go-version: [1.16.x]
go-version: [1.18.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Code format
run: diff -u <(echo -n) <(gofmt -d -s .)
- name: Vet
run: go vet ./...
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
- name: Unit tests
run: go test -race -coverprofile=coverage.out ./...
- name: Function coverage
run: go tool cover "-func=coverage.out"
- name: Upload coverage report
uses: codecov/codecov-action@v2
uses: codecov/codecov-action@v3
with:
file: ./coverage.out
- name: Build and Execute
- name: Build and Test
run: |
go build -o totp-test -ldflags "-X main.version=$(./scripts/get-version.sh)" ./totp/...
./totp-test --help
./scripts/totp-test.sh

View File

@@ -7,21 +7,22 @@ on:
jobs:
goreleaser:
name: GoReleaser
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: 1.18.x
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
uses: goreleaser/goreleaser-action@v3
with:
args: release --rm-dist
env:

6
.gitignore vendored
View File

@@ -1,6 +1,8 @@
.vscode/
bin/
dist/
cmd/testcollection.json
coverage.*
settings.json
dist/
testcollection.json
totp

7
.golangci.yml Normal file
View File

@@ -0,0 +1,7 @@
linters:
enable:
- gosec
linters-settings:
gosec:
excludes:
- G404

View File

@@ -4,10 +4,10 @@ before:
hooks:
- go mod tidy
builds:
- main: ./totp
- main: ./cmd
binary: totp
ldflags:
- -s -w -X github.com/arcanericky/totp/cmd.versionText={{.Version}}
- -s -w -X github.com/arcanericky/totp/commands.versionText={{.Version}}
env:
- CGO_ENABLED=0
goos:

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Ricky Pike
Copyright (c) 2022 Ricky Pike
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,7 +1,7 @@
VERSION=1.0.0-incubation
VERSION_INJECT=github.com/arcanericky/totp/cmd.versionText
SRCS=*.go totp/*.go cmd/*.go
SRCS=*.go cmd/*.go commands/*.go
MAIN=./cmd/...
EXECUTABLE=bin/totp
LINUX=$(EXECUTABLE)-linux
@@ -31,23 +31,23 @@ linux-arm: $(LINUX_ARM32)
linux-arm64: $(LINUX_ARM64)
test:
go test -race -coverprofile=coverage.txt -covermode=atomic . ./cmd
go test -race -coverprofile=coverage.txt -covermode=atomic . ./commands
go tool cover -html=coverage.txt -o coverage.html
$(WINDOWS_AMD64): $(SRCS)
GOOS=windows GOARCH=amd64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" github.com/arcanericky/totp/totp
GOOS=windows GOARCH=amd64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" $(MAIN)
$(LINUX_AMD64): $(SRCS)
GOOS=linux GOARCH=amd64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" github.com/arcanericky/totp/totp
GOOS=linux GOARCH=amd64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" $(MAIN)
$(DARWIN_AMD64): $(SRCS)
GOOS=darwin GOARCH=amd64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" github.com/arcanericky/totp/totp
GOOS=darwin GOARCH=amd64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" $(MAIN)
$(LINUX_ARM32): $(SRCS)
GOOS=linux GOARCH=arm go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" github.com/arcanericky/totp/totp
GOOS=linux GOARCH=arm go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" $(MAIN)
$(LINUX_ARM64): $(SRCS)
GOOS=linux GOARCH=arm64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" github.com/arcanericky/totp/totp
GOOS=linux GOARCH=arm64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" $(MAIN)
clean:
rm -rf bin

View File

@@ -1,6 +1,6 @@
# TOTP
A time-based one-time password (TOTP) code generator written in Go. Basically a command-line interface that's [Google Authenticator](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_US) or [Authy](https://authy.com/) for your Windows, macOS, or Linux machine.
A time-based one-time password (TOTP) code generator written in Go. A command-line interface that's like [Google Authenticator](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_US) or [Authy](https://authy.com/) for your Windows, macOS, or Linux machine.
[![Build](https://github.com/arcanericky/totp/actions/workflows/builder.yml/badge.svg?branch=master)](https://github.com/arcanericky/totp/actions/workflows/builder.yml)
[![codecov](https://codecov.io/gh/arcanericky/totp/branch/master/graph/badge.svg)](https://codecov.io/gh/arcanericky/totp)
@@ -13,7 +13,7 @@ It generates TOTP codes used for two-factor authentication at sites such as Goog
**Warning**
Every copy of your two-factor credentials increases your risk profile. Using this utility is no exception. This utility will store your TOTP secrets unencrypted on your filesystem. The only protection offered is to store these secrets in a file readable by only your user and protected by the operating system only.
## How to Use
## Quick Start
**Add TOTP secrets** to the TOTP configuration file with the `config add` option, specifying the name and secret value. Note the secret names are **case sensitive**.
@@ -21,7 +21,7 @@ Every copy of your two-factor credentials increases your risk profile. Using thi
totp config add mysecretname seed
```
**Generate TOTP codes** using the `totp` command to specify the secret name. Note that because `totp` reserves the use of the words `config` and `version`, don't use them to name a secret.
**Generate TOTP codes** using the `totp` command to specify the secret name. Note that because `totp` reserves the use of the words `config` and `version` for commands, don't use them to name a secret. If you've generated and installed `totp` completions for for your shell, pressing tab on a partially completed secret name will trigger autocomplete.
```sh
totp mysecretname
@@ -33,6 +33,8 @@ totp mysecretname
totp config list
```
Aliases are `ls` and `l`.
**Update secret entries** using the `config update` command. Note that `config update` and `config add` are actually the same command and can be used interchangeably.
```sh
@@ -45,12 +47,16 @@ totp config update mysecretname newseed
totp config rename mysecretname mynewname
```
Aliases are `ren` and `mv`.
**Delete secret entries** with the `config delete` command
```sh
totp config delete mynewname
```
Aliases are `remove`, `erase`, `rm`, and `del`.
**Remove all the secrets** and start over using the `config reset` command
```sh
@@ -76,17 +82,29 @@ totp --help
totp config --help
```
**Bash completion** can be enabled by using `config completion`.
**Shell completion** can be enabled by using the `completion` command.
Bash
```sh
. <(totp config completion)
. <(totp completion bash)
```
Powershell
```powershell
. totp completion powershell | Out-String | Invoke-Expression
```
## TOTP Data Location
The location for saved data is extracted from the `LOCALAPPDATA` environment variable in Windows and the `HOME` environment for Linux/MacOS and in the file `totp-config.json`. This can be customized using the `--file` option or by setting the `TOTP_CONFIG` environment variable.
## Using the Time Machine
`totp` has the `--time`, `--forward`, and `--backward` options that are used to manipulate the time for which the TOTP code is generated. This is useful if `totp` is being used on a machine with the incorrect time.
`totp` implements the `--time`, `--forward`, and `--backward` options to manipulate the time for which the TOTP code is generated. This is useful if `totp` is being used on a machine with the incorrect time.
The `--time` option takes an [RFC3339 formatted time string](https://tools.ietf.org/html/rfc3339) as its argument and uses it to generate the TOTP code. Note that the `--forward` and `--backward` options will modify this option value.
The `--time` option takes an [RFC3339 formatted time string](https://tools.ietf.org/html/rfc3339) as its argument and uses it to generate the TOTP code. Note that the `--forward` and `--backward` options will internally modify this option value.
Examples with `--time`:
@@ -114,6 +132,8 @@ The `--follow` option is also compatible with the time machine.
```sh
totp --time 2001-10-31T20:00:00-05:00 --follow --secret seed
877737
208737
```
## Using the Stdio Option
@@ -123,7 +143,7 @@ If storing secrets in the clear isn't ideal for you, `totp` supports streaming t
The `totp <secret name>` and `totp config list` commands support loading the collection via standard input. The
`totp config update`, `totp config delete`, and `totp config rename` commands support loading via standard input and sending the modified collection to standard output. Experiment with the `--stdio` option to observe how this works.
**Learning with Cleartext Data**
### Learning with Plaintext Data
Note the `--file` option can achieve the same results as this example. This is meant to teach how stdio works with `totp`.
@@ -145,7 +165,7 @@ Generate a TOTP code
totp secretname --stdio < totp.json
```
**Encrypting Shared Secret Collection**
### Encrypting Shared Secret Collection
Using what was learned above, a contrived example for encrypting data with [GnuPG](https://gnupg.org/) follows.
@@ -186,7 +206,7 @@ gpg --quiet --batch --passphrase mypassphrase --decrypt totp-collection.gpg | to
## Building
`totp` is mostly developed using Go 1.12.x on Debian based systems. Only `go` is required but to use the automated actions the `Makefile` provides, `make` must be installed.
`totp` is mostly developed using Go 1.18.x on Debian based systems. Only `go` is required but to use the automated actions the `Makefile` provides, `make` must be installed.
To build everything:
@@ -228,4 +248,4 @@ My [ga-cmd project](https://github.com/arcanericky/ga-cmd) is more popular than
## Credits
This utility uses the [otp package by pquerna](https://github.com/pquerna/otp). Without this library, I probably wouldn't have bothered creating this.
This utility uses the [otp package by pquerna](https://github.com/pquerna/otp). Without this library, I probably wouldn't have bothered creating this front-end.

View File

@@ -1,15 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
)
var configCmd = &cobra.Command{
Use: "config",
Short: "Configure totp",
Long: `Configure totp`,
}
func init() {
rootCmd.AddCommand(configCmd)
}

View File

@@ -1,29 +0,0 @@
package cmd
import (
"os"
"github.com/spf13/cobra"
)
// completionCmd represents the completion command
var completionCmd = &cobra.Command{
Use: "completion",
Short: "Generates bash completion scripts",
Long: `To load completion run
. <(totp config completion)
To configure your bash shell to load completions for each session add to your bashrc
# ~/.bashrc or ~/.profile
. <(totp config completion)
`,
Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenBashCompletion(os.Stdout)
},
}
func init() {
configCmd.AddCommand(completionCmd)
}

View File

@@ -1,9 +0,0 @@
package cmd
import (
"testing"
)
func TestConfigCompletion(t *testing.T) {
completionCmd.Run(nil, []string{})
}

View File

@@ -1,44 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var configDeleteCmd = &cobra.Command{
Use: "delete",
Aliases: []string{"remove", "erase", "rm", "del"},
Short: "Delete a secret",
Long: `Delete a secret`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
fmt.Fprintln(os.Stderr, "Must provide a secret name to delete.")
return
}
deleteSecret(args[0])
},
}
func deleteSecret(name string) {
s, err := collectionFile.loader()
if err != nil {
fmt.Fprintln(os.Stderr, "Error loading settings", err)
} else {
_, err := s.DeleteSecret(name)
if err != nil {
fmt.Fprintln(os.Stderr, "Error deleting secret", err)
} else {
s.Save()
printResultf("Deleted secret %s\n", name)
}
}
}
func init() {
configCmd.AddCommand(configDeleteCmd)
configDeleteCmd.Flags().BoolP(optionStdio, "", false, "load with stdin, save with stdout")
}

View File

@@ -1,97 +0,0 @@
package cmd
import (
"fmt"
"os"
"strings"
"github.com/arcanericky/totp"
"github.com/spf13/cobra"
)
var configListCmd = &cobra.Command{
Use: "list",
Short: "List secrets",
Long: `List secrets`,
Run: func(cmd *cobra.Command, args []string) {
listSecrets(cmd)
},
}
func titleLine(len int) string {
var builder strings.Builder
builder.Grow(len)
for i := 0; i < len; i++ {
builder.WriteString("-")
}
return builder.String()
}
func listSecretNames(secrets []totp.Secret) {
for _, s := range secrets {
fmt.Println(s.Name)
}
}
func listAllInfo(secrets []totp.Secret) {
nameTitle := "Name"
secretTitle := "Secret"
addedDateTitle := "Date Added"
modifiedDateTitle := "Date Modified"
maxNameLen := len(nameTitle)
maxSecretLen := len(secretTitle)
for _, s := range secrets {
nameLen := len(s.Name)
if nameLen > maxNameLen {
maxNameLen = nameLen
}
secretLen := len(s.Value)
if secretLen > maxSecretLen {
maxSecretLen = secretLen
}
}
timeFormat := "Jan _2 2006 15:04:05"
timeFormatLen := len(timeFormat)
timeFormatLine := titleLine(len(timeFormat))
fmt.Printf("%-*s %-*s %-*s %-*s\n",
maxNameLen, nameTitle,
maxSecretLen, secretTitle,
timeFormatLen, addedDateTitle,
timeFormatLen, modifiedDateTitle)
fmt.Printf("%s %s %s %s\n", titleLine(maxNameLen), titleLine(maxSecretLen), timeFormatLine, timeFormatLine)
for _, s := range secrets {
fmt.Printf("%-*s %-*s %s %s\n", maxNameLen, s.Name, maxSecretLen, s.Value, s.DateAdded.Format(timeFormat), s.DateModified.Format(timeFormat))
}
}
func listSecrets(cmd *cobra.Command) {
c, err := collectionFile.loader()
if err != nil {
fmt.Fprintln(os.Stderr, "Error loading collection", err)
} else {
names, err := cmd.Flags().GetBool("names")
if err != nil {
fmt.Fprintln(os.Stderr, "Error getting names option", err)
return
}
secrets := c.GetSecrets()
if names == true {
listSecretNames(secrets)
} else {
listAllInfo(secrets)
}
}
}
func init() {
configCmd.AddCommand(configListCmd)
configListCmd.Flags().BoolP("names", "n", false, "list only secret names")
configListCmd.Flags().BoolP(optionStdio, "", false, "load data from stdin")
}

View File

@@ -1,47 +0,0 @@
package cmd
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
)
var configRenameCmd = &cobra.Command{
Use: "rename",
Aliases: []string{"ren", "mv"},
Short: "Rename a secret",
Long: `Rename a secret`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
fmt.Fprintln(os.Stderr, "Must provide source and target.")
return
}
renameSecret(args[0], args[1])
},
}
func renameSecret(source, target string) {
if isReservedCommand(target) {
fmt.Fprintln(os.Stderr, "The name \""+target+"\" is reserved for the "+target+" command")
return
}
s, _ := collectionFile.loader()
_, err := s.RenameSecret(source, target)
if err != nil {
fmt.Fprintln(os.Stderr, "Error renaming secret:", err)
} else {
s.Save()
printResultf("Renamed secret %s to %s\n", source, target)
}
}
func init() {
configCmd.AddCommand(configRenameCmd)
configRenameCmd.Flags().BoolP(optionStdio, "", false, "load with stdin, save with stdout")
configRenameCmd.SetUsageTemplate(strings.Replace(rootCmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}}\n {{.CommandPath}} [old secret name] [new secret name]", 1))
}

View File

@@ -1,26 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var configResetCmd = &cobra.Command{
Use: "reset",
Short: "Reset the TOTP colllection",
Long: "Reset the TOTP colllection",
Run: func(cmd *cobra.Command, args []string) {
configReset()
},
}
func configReset() {
os.Remove(collectionFile.filename)
fmt.Println("Collection file removed")
}
func init() {
configCmd.AddCommand(configResetCmd)
}

View File

@@ -1,19 +0,0 @@
package cmd
import (
"os"
"testing"
)
func TestConfigReset(t *testing.T) {
collectionFile.filename = "testcollection.json"
createTestData(t)
configResetCmd.Run(nil, []string{})
_, err := os.Stat(collectionFile.filename)
if !os.IsNotExist(err) {
t.Error("Failed to remove the collection file")
}
}

View File

@@ -1,53 +0,0 @@
package cmd
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
)
var configUpdateCmd = &cobra.Command{
Use: "update",
Aliases: []string{"add"},
Short: "Add or update a secret",
Long: `Add or update a secret`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
fmt.Fprintln(os.Stderr, "Must provide name and secret")
return
}
updateSecret(args[0], args[1])
},
}
func updateSecret(name, value string) {
if isReservedCommand(name) {
fmt.Fprintln(os.Stderr, "The name \""+name+"\" is reserved for the "+name+" command")
return
}
s, _ := collectionFile.loader()
secret, err := s.UpdateSecret(name, value)
if err != nil {
fmt.Fprintln(os.Stderr, "Error updating secret:", err)
} else {
s.Save()
action := "Updated"
if secret.DateAdded == secret.DateModified {
action = "Added"
}
printResultf("%s secret %s\n", action, name)
}
}
func init() {
configCmd.AddCommand(configUpdateCmd)
configUpdateCmd.Flags().BoolP(optionStdio, "", false, "load with stdin, save with stdout")
configUpdateCmd.SetUsageTemplate(strings.Replace(rootCmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}}\n {{.CommandPath}} [secret name] [secret value]", 1))
}

View File

@@ -1,285 +0,0 @@
package cmd
import (
"fmt"
"os"
"strings"
"time"
api "github.com/arcanericky/totp"
"github.com/pquerna/otp/totp"
"github.com/spf13/cobra"
)
const (
optionBackward = "backward"
optionFile = "file"
optionFollow = "follow"
optionForward = "forward"
optionSecret = "secret"
optionStdio = "stdio"
optionTime = "time"
)
type generateCodesAPI func(time.Duration, time.Duration, time.Duration, func(time.Duration), string, string)
var generateCodesService generateCodesAPI
var rootCmd = &cobra.Command{
Use: "totp",
Short: "TOTP Generator",
Long: `TOTP Generator`,
Args: cobra.ArbitraryArgs,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if cmd.Flags().Changed(optionFile) {
cfgFile, err := cmd.Flags().GetString(optionFile)
if err != nil {
fmt.Println("Error processing collection file option", err)
return
}
collectionFile.filename = cfgFile
}
if cmd.Flags().Lookup(optionStdio) != nil {
useStdio, err := cmd.Flags().GetBool(optionStdio)
if err != nil {
fmt.Println("Error processing stdio option", err)
return
}
if useStdio == true {
collectionFile.loader = loadCollectionFromStdin
collectionFile.useStdio = true
}
}
},
Run: func(cmd *cobra.Command, args []string) {
// Process the secret option
secret, err := cmd.Flags().GetString(optionSecret)
if err != nil {
fmt.Println("Error getting secret", err)
return
}
// Process the backward option
backward, err := cmd.Flags().GetDuration(optionBackward)
if err != nil {
fmt.Println("Error processing backward option", err)
return
}
// Process the forward option
forward, err := cmd.Flags().GetDuration(optionForward)
if err != nil {
fmt.Println("Error processing forward option", err)
return
}
// Process the time option
timeString, err := cmd.Flags().GetString(optionTime)
if err != nil {
fmt.Println("Error processing time option", err)
return
}
follow, err := cmd.Flags().GetBool(optionFollow)
if err != nil {
fmt.Println("Error processing follow option", err)
return
}
var codeTime time.Time
// Override if time was given
if len(timeString) > 0 {
codeTime, err = time.Parse(time.RFC3339, timeString)
if err != nil {
fmt.Println("Error parsing the time option", err)
return
}
} else {
codeTime = time.Now()
// codeOffset is 0
}
secretLen := len(secret)
argsLen := len(args)
// No secret, no secret name
if secretLen == 0 && argsLen == 0 {
fmt.Fprintf(os.Stderr, "Secret name or secret is required.\n\n")
cmd.Help()
return
}
// Secret given but additional arguments were also given
if secretLen > 0 && argsLen > 0 {
fmt.Fprintf(os.Stderr, "Secret was given so additional arguments are not needed.\n\n")
cmd.Help()
return
}
// No secret given and too many args
if secretLen == 0 && argsLen > 1 {
fmt.Fprintf(os.Stderr, "Too many arguments. Only one secret name is required.\n\n")
cmd.Help()
return
}
// Load the secret name
secretName := ""
if argsLen == 1 {
secretName = args[0]
}
// If here then a stored shared secret is wanted
generateCode(secretName, secret, codeTime.Add(forward-backward))
if follow {
generateCodesService(codeTime.Sub(time.Now())-backward+forward, 0, 30*time.Second, time.Sleep, secretName, secret)
}
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getSecretNamesForCompletion(toComplete), cobra.ShellCompDirectiveNoFileComp
},
}
func getSecretNamesForCompletion(toComplete string) []string {
var secretNames []string
var err error
var c *api.Collection
c, err = collectionFile.loader()
if err != nil {
fmt.Fprintln(os.Stderr, "Error loading collection", err)
} else {
secrets := c.GetSecrets()
for _, s := range secrets {
if strings.HasPrefix(s.Name, toComplete) {
secretNames = append(secretNames, s.Name)
}
}
}
return secretNames
}
func generateCode(name string, secret string, t time.Time) error {
var code string
var err error
var c *api.Collection
if len(secret) != 0 {
code, err = totp.GenerateCode(secret, t)
} else {
c, err = collectionFile.loader()
if err != nil {
fmt.Fprintln(os.Stderr, "Error loading collection", err)
} else {
code, err = c.GenerateCodeWithTime(name, t)
if err != nil {
fmt.Fprintln(os.Stderr, "Error generating code", err)
}
}
}
if err == nil {
fmt.Println(code)
}
return err
}
func durationToNextInterval(now time.Time) time.Duration {
var sleepSeconds int
s := now.Second()
switch {
case s == 0, s < 30:
sleepSeconds = 30 - s
case s >= 30:
sleepSeconds = 60 - s
}
return time.Duration(sleepSeconds)*time.Second -
time.Duration(now.Nanosecond())*time.Nanosecond
}
func callOnInterval(runtime time.Duration, interval time.Duration, exec func() bool) {
stopper := make(chan bool)
if runtime > 0 {
go func() {
time.Sleep(runtime)
stopper <- true
}()
}
if exec != nil && exec() {
return
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-stopper:
return
case <-ticker.C:
if exec != nil && exec() {
return
}
}
}
}
func generateCodes(timeOffset time.Duration, durationToRun time.Duration, intervalTime time.Duration, sleep func(time.Duration), secretName, secret string) {
sleep(durationToNextInterval(time.Now().Add(timeOffset)) + 10*time.Millisecond)
callOnInterval(durationToRun, intervalTime,
func() bool {
generateCode(secretName, secret, time.Now().Add(timeOffset))
return false
})
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() int {
retVal := 0
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
retVal = 1
}
return retVal
}
func init() {
var duration time.Duration
generateCodesService = generateCodes
rootCmd.PersistentFlags().StringP(optionFile, "f", "", "secret collection file")
rootCmd.Flags().StringP(optionSecret, "s", "", "TOTP secret value")
rootCmd.Flags().BoolP(optionStdio, "", false, "load with stdin")
rootCmd.Flags().StringP(optionTime, "", "", "RFC3339 time for TOTP (2019-06-23T20:00:00-05:00)")
rootCmd.Flags().DurationP(optionBackward, "", duration, "move time backward (ex. \"30s\")")
rootCmd.Flags().DurationP(optionForward, "", duration, "move time forward (ex. \"1m\")")
rootCmd.Flags().BoolP(optionFollow, "", false, "continuous output")
rootCmd.SetUsageTemplate(strings.Replace(rootCmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}}\n {{.CommandPath}} [secret name]", 1))
}

View File

@@ -1,209 +0,0 @@
package cmd
import (
"os"
"testing"
"time"
"github.com/spf13/pflag"
)
type flagValue struct{}
func (f flagValue) Set(s string) error {
return nil
}
func (f flagValue) Type() string {
return ""
}
func (f flagValue) String() string {
return ""
}
func TestRoot(t *testing.T) {
collectionFile.filename = "testcollection.json"
secretList := createTestData(t)
// No parameters
rootCmd.Run(rootCmd, []string{})
// Valid entry and secret
rootCmd.Run(rootCmd, []string{secretList[0].name})
// Non-existing entry
rootCmd.Run(rootCmd, []string{"invalidsecret"})
// Test follow condition
savedGenerateCodesService := generateCodesService
generateCodesService = func(time.Duration, time.Duration, time.Duration, func(time.Duration), string, string) {}
rootCmd.Flags().Lookup(optionFollow).Value.Set("true")
rootCmd.Run(rootCmd, []string{"name0"})
generateCodesService = savedGenerateCodesService
rootCmd.Flags().Lookup(optionFollow).Value.Set("false")
// Completion
rootCmd.ValidArgsFunction(rootCmd, []string{}, "na")
// Completion with args
rootCmd.ValidArgsFunction(rootCmd, []string{"secret"}, "na")
// No collections file
os.Remove(collectionFile.filename)
rootCmd.Run(rootCmd, []string{secretList[0].name})
// Completion without collections
rootCmd.ValidArgsFunction(rootCmd, []string{}, "na")
// Excessive args
rootCmd.Run(rootCmd, []string{"secretname", "extraarg"})
// Provide secret option
rootCmd.Flags().Set(optionSecret, "seed")
rootCmd.Run(rootCmd, []string{})
// Provide invalid secret option
rootCmd.Flags().Set(optionSecret, "seed1")
rootCmd.Run(rootCmd, []string{})
// File option
rootCmd.Flags().Set(optionFile, collectionFile.filename)
rootCmd.Flags().Lookup(optionFile).Changed = true
rootCmd.PersistentPreRun(rootCmd, []string{"secret"})
// Stdio option
rootCmd.Flags().Set(optionStdio, "true")
rootCmd.PersistentPreRun(rootCmd, []string{"secret"})
collectionFile.loader = loadCollectionFromDefaultFile
collectionFile.useStdio = false
rootCmd.Flags().Set(optionStdio, "")
// Time option
rootCmd.Flags().Set(optionTime, "2019-06-01T20:00:00-05:00")
rootCmd.Run(rootCmd, []string{})
// Give secret and secret name
rootCmd.Flags().Set(optionSecret, "seed")
rootCmd.Run(rootCmd, []string{"secretname"})
rootCmd.Flags().Set(optionSecret, "")
// Invalid time option
rootCmd.Flags().Set(optionTime, "invalidtime")
rootCmd.Run(rootCmd, []string{})
rootCmd.Flags().Set(optionTime, "")
var f *pflag.Flag
var savedFlagValue pflag.Value
// optionFile error
f = rootCmd.Flags().Lookup(optionFile)
savedFlagValue = f.Value
f.Value = new(flagValue)
f.Changed = true
rootCmd.PersistentPreRun(rootCmd, []string{"secret"})
f.Value = savedFlagValue
// optionStdio error
f = rootCmd.Flags().Lookup(optionStdio)
savedFlagValue = f.Value
f.Value = new(flagValue)
rootCmd.PersistentPreRun(rootCmd, []string{"secret"})
f.Value = savedFlagValue
// optionSecret error
f = rootCmd.Flags().Lookup(optionSecret)
savedFlagValue = f.Value
f.Value = new(flagValue)
rootCmd.Run(rootCmd, []string{})
f.Value = savedFlagValue
// optionBackward error
f = rootCmd.Flags().Lookup(optionBackward)
savedFlagValue = f.Value
f.Value = new(flagValue)
rootCmd.Run(rootCmd, []string{})
f.Value = savedFlagValue
// optionForward error
f = rootCmd.Flags().Lookup(optionForward)
savedFlagValue = f.Value
f.Value = new(flagValue)
rootCmd.Run(rootCmd, []string{})
f.Value = savedFlagValue
// optionTime error
f = rootCmd.Flags().Lookup(optionTime)
savedFlagValue = f.Value
f.Value = new(flagValue)
rootCmd.Run(rootCmd, []string{})
f.Value = savedFlagValue
// optionFollow error
f = rootCmd.Flags().Lookup(optionFollow)
savedFlagValue = f.Value
f.Value = new(flagValue)
rootCmd.Run(rootCmd, []string{})
f.Value = savedFlagValue
Execute()
savedArgs := os.Args
os.Args = []string{"totp", "--invalidoption"}
Execute()
os.Args = savedArgs
}
func TestExecOnInterval(t *testing.T) {
execCount := 0
preAndExecNormal := func() bool { return false }
preAndExecEarlyExit := func() bool { execCount++; return execCount == 2 }
// Normal execution
execCount = 0
callOnInterval(2*time.Millisecond, 1*time.Millisecond, preAndExecNormal)
// Exit at preExec
execCount = 1
callOnInterval(2*time.Millisecond, 1*time.Millisecond, preAndExecNormal)
// Exit at top exec
execCount = 1
callOnInterval(2*time.Millisecond, 1*time.Millisecond, preAndExecEarlyExit)
// Exit at loop exec
execCount = 0
callOnInterval(2*time.Millisecond, 1*time.Millisecond, preAndExecEarlyExit)
// No callbacks
callOnInterval(2*time.Millisecond, 1*time.Millisecond, nil)
}
func TestDurationToNextInterval(t *testing.T) {
now, _ := time.Parse(time.RFC3339, "2019-06-23T20:00:01-05:00")
expectedResult := time.Duration(29 * time.Second)
actualResult := durationToNextInterval(now)
if expectedResult != actualResult {
t.Errorf("durationToNextInterval(%s) expected %s but returned %s", now, expectedResult, actualResult)
}
now, _ = time.Parse(time.RFC3339, "2019-06-23T20:00:31-05:00")
expectedResult = time.Duration(29 * time.Second)
actualResult = durationToNextInterval(now)
if expectedResult != actualResult {
t.Errorf("durationToNextInterval(%s) expected %s but returned %s", now, expectedResult, actualResult)
}
now, _ = time.Parse(time.RFC3339, "2019-06-23T20:00:31.001-05:00")
expectedResult = time.Duration(28*time.Second + 999*time.Millisecond)
actualResult = durationToNextInterval(now)
if expectedResult != actualResult {
t.Errorf("durationToNextInterval(%s) expected %s but returned %s", now, expectedResult, actualResult)
}
}
func TestGenerateCodes(t *testing.T) {
var d time.Duration
generateCodes(d, 2*time.Millisecond, 1*time.Millisecond,
func(d time.Duration) {}, "", "seed")
}

11
cmd/totp.go Normal file
View File

@@ -0,0 +1,11 @@
package main
import (
"os"
"github.com/arcanericky/totp/commands"
)
func main() {
os.Exit(commands.Execute())
}

View File

@@ -1,23 +0,0 @@
package cmd
import (
"fmt"
"runtime"
"github.com/spf13/cobra"
)
var versionText = "unspecified"
var versionCmd = &cobra.Command{
Use: "version",
Short: "Show topt version",
Long: "Show topt version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("totp version %s %s/%s\n", versionText, runtime.GOOS, runtime.GOARCH)
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}

View File

@@ -4,30 +4,38 @@ import (
"encoding/json"
"errors"
"io"
"io/ioutil"
"os"
"time"
"github.com/pquerna/otp/totp"
)
var errSecretNotFound = errors.New("secret not found")
var errNoFilename = errors.New("no save target")
var errSecretNameEmpty = errors.New("secret name empty")
var errSecretValueEmpty = errors.New("secret value empty")
var ErrSecretNotFound = errors.New("secret not found")
var ErrNoFilename = errors.New("no save target")
var ErrSecretNameEmpty = errors.New("secret name empty")
var ErrSecretValueEmpty = errors.New("secret value empty")
// Secret is a struct containing data necessary for working with secrets,
// namely, the name of the secret name and the secret value
type Secret struct {
DateAdded time.Time
// DateAdded is the date a secret was added to the collection
DateAdded time.Time
// DateModified is the date a secret was last modified
DateModified time.Time
Name string
Value string
// Name is the name of the secret used for retrieval
Name string
// Value is the secret (seed) value
Value string
}
// Collection is a struct that holds TOTP data
type Collection struct {
Secrets map[string]Secret
// Secrets is a map of secrets using the secret name as the key
Secrets map[string]Secret
filename string
writer io.Writer
}
@@ -45,73 +53,66 @@ type CollectionInterface interface {
// Save serializes (marshals) the Collections struct and writes it to
// a file
func (c *Collection) Save() error {
var err error
var writerErr error
serializedSettings, err := c.Serialize()
if err != nil {
return err
}
if err == nil {
if c.writer == nil && len(c.filename) == 0 {
err = errNoFilename
}
if c.writer != nil {
_, writerErr = c.writer.Write(serializedSettings)
}
if len(c.filename) != 0 {
err = ioutil.WriteFile(c.filename, serializedSettings, 0600)
}
if err == nil {
err = writerErr
}
if c.writer != nil {
_, err = c.writer.Write(serializedSettings)
} else if len(c.filename) != 0 {
err = os.WriteFile(c.filename, serializedSettings, 0600)
} else {
err = ErrNoFilename
}
return err
}
// DeleteSecret deletes an Entry by name
// DeleteSecret deletes an entry by name
func (c *Collection) DeleteSecret(name string) (Secret, error) {
var err error
retSecret, ok := c.Secrets[name]
if ok {
delete(c.Secrets, name)
} else {
err = errSecretNotFound
if !ok {
return Secret{}, ErrSecretNotFound
}
return retSecret, err
delete(c.Secrets, name)
return retSecret, nil
}
// UpdateSecret updates (if it exists) or adds a new Entry with the
// UpdateSecret updates (if it exists) or adds a new entry with the
// name and value given
func (c *Collection) UpdateSecret(name, value string) (Secret, error) {
var retSecret Secret
var err error
var ok bool
if len(name) == 0 {
err = errSecretNameEmpty
} else if len(value) == 0 {
err = errSecretValueEmpty
return Secret{}, ErrSecretNameEmpty
}
if len(value) == 0 {
return Secret{}, ErrSecretValueEmpty
}
_, err := totp.GenerateCode(value, time.Now())
if err != nil {
return Secret{}, err
}
retSecret, ok := c.Secrets[name]
if ok {
// entry indicates an update
retSecret.Value = value
retSecret.DateModified = time.Now()
c.Secrets[name] = retSecret
} else {
_, err = totp.GenerateCode(value, time.Now())
if err == nil {
retSecret, ok = c.Secrets[name]
if ok {
retSecret.Value = value
retSecret.DateModified = time.Now()
c.Secrets[name] = retSecret
} else {
dateAdded := time.Now()
newSecret := Secret{Name: name, Value: value, DateAdded: dateAdded, DateModified: dateAdded}
c.Secrets[name] = newSecret
retSecret = newSecret
}
// no entry indicates an add
dateAdded := time.Now()
retSecret = Secret{
Name: name,
Value: value,
DateAdded: dateAdded,
DateModified: dateAdded,
}
c.Secrets[name] = retSecret
}
return retSecret, err
@@ -119,37 +120,31 @@ func (c *Collection) UpdateSecret(name, value string) (Secret, error) {
// RenameSecret renames a secret
func (c *Collection) RenameSecret(oldName, newName string) (Secret, error) {
var retSecret Secret
var ok bool
var err error
if len(newName) != 0 {
retSecret, ok = c.Secrets[oldName]
if ok {
retSecret.Name = newName
retSecret.DateModified = time.Now()
c.Secrets[newName] = retSecret
delete(c.Secrets, oldName)
} else {
err = errSecretNotFound
}
} else {
err = errSecretNameEmpty
if len(newName) == 0 {
return Secret{}, ErrSecretNameEmpty
}
return retSecret, err
retSecret, ok := c.Secrets[oldName]
if !ok {
return Secret{}, ErrSecretNotFound
}
retSecret.Name = newName
retSecret.DateModified = time.Now()
c.Secrets[newName] = retSecret
delete(c.Secrets, oldName)
return retSecret, nil
}
// GetSecret returns an Secret with the name argument
// GetSecret returns a secret with the name argument
func (c *Collection) GetSecret(name string) (Secret, error) {
var err error
retSecret, ok := c.Secrets[name]
if !ok {
err = errSecretNotFound
return Secret{}, ErrSecretNotFound
}
return retSecret, err
return retSecret, nil
}
// GetSecrets returns a slice containing all the secrets
@@ -164,14 +159,13 @@ func (c *Collection) GetSecrets() []Secret {
// GenerateCodeWithTime creates a TOTP code with the named secret's value
func (c *Collection) GenerateCodeWithTime(name string, time time.Time) (string, error) {
var code string
secret, err := c.GetSecret(name)
if err == nil {
code, err = totp.GenerateCode(secret.Value, time)
if err != nil {
return "", err
}
return code, err
return totp.GenerateCode(secret.Value, time)
}
// GenerateCode creates a TOTP code with the named secret's value
@@ -211,15 +205,13 @@ func NewCollection() *Collection {
// NewCollectionWithData creates a new Collection instance with data from a byte slice
func NewCollectionWithData(data []byte) (*Collection, error) {
c := NewCollection()
err := c.Deserialize(data)
return c, err
return c, c.Deserialize(data)
}
// NewCollectionWithReader creates a new collection from a Reader interface
func NewCollectionWithReader(reader io.Reader) (*Collection, error) {
data, err := ioutil.ReadAll(reader)
data, err := io.ReadAll(reader)
if err != nil {
return NewCollection(), err
}
@@ -227,16 +219,27 @@ func NewCollectionWithReader(reader io.Reader) (*Collection, error) {
return NewCollectionWithData(data)
}
// NewCollectionWithFile creates a new Collection instance with data from a file
func NewCollectionWithFile(filename string) (*Collection, error) {
c := NewCollection()
// NewCollectionWithFile creates a new Collection instance with data from a file.
// If the file open fails, a new Collection instance is returned along with the
// file open error, which guarantees a usable but empty collection is returned.
//
// Returning data to be used along with an error is bad design but changing this
// would be a breaking API change.
func NewCollectionWithFile(filename string) (c *Collection, err error) {
f, err := os.Open(filename)
if err == nil {
c, err = NewCollectionWithReader(f)
if err != nil {
c := NewCollection()
c.filename = filename
return c, err
}
f.Close()
defer func() {
if e := f.Close(); e != nil && err == nil {
err = e
}
}()
c, err = NewCollectionWithReader(f)
c.filename = filename
return c, err

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
package cmd
package commands
import (
"testing"
@@ -19,11 +19,12 @@ func createTestData(t *testing.T) []secretItem {
// Create some test data
secretList := []secretItem{
{name: "name0", value: "seed"},
{name: "name1", value: "seed"},
{name: "name2", value: "seedseed"},
{name: "name3", value: "seed"},
{name: "name4", value: "seed"},
{name: "name0", value: "SEED"},
{name: "name1", value: "SEED"},
{name: "name2", value: "SEEDSEED"},
{name: "name3", value: "SEED"},
{name: "name4", value: "SEED"},
{name: "testname", value: "TESTSECRET"},
}
for _, i := range secretList {
@@ -33,7 +34,7 @@ func createTestData(t *testing.T) []secretItem {
}
}
c.Save()
_ = c.Save()
return secretList
}

21
commands/config.go Normal file
View File

@@ -0,0 +1,21 @@
package commands
import (
"github.com/spf13/cobra"
)
func getConfigCmd(rootCmd *cobra.Command) *cobra.Command {
var cobraCmd = &cobra.Command{
Use: cmdConfig,
Short: "Configure totp",
Long: `Configure totp`,
}
cobraCmd.AddCommand(getConfigListCmd())
cobraCmd.AddCommand(getConfigRenameCmd(rootCmd))
cobraCmd.AddCommand(getConfigUpdateCmd(rootCmd))
cobraCmd.AddCommand(getConfigDeleteCmd())
cobraCmd.AddCommand(getConfigResetCmd())
return cobraCmd
}

73
commands/configdelete.go Normal file
View File

@@ -0,0 +1,73 @@
package commands
import (
"bufio"
"fmt"
"os"
"github.com/spf13/cobra"
)
func deleteSecret(name string) {
s, err := collectionFile.loader()
if err != nil {
fmt.Fprintln(os.Stderr, "Error loading settings:", err)
return
}
if _, err := s.DeleteSecret(name); err != nil {
fmt.Fprintln(os.Stderr, "Error deleting secret:", err)
return
}
if err := s.Save(); err != nil {
fmt.Fprintln(os.Stderr, "Error saving settings:", err)
return
}
if _, err := printResultf("Deleted secret %s\n", name); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
}
func getConfigDeleteCmd() *cobra.Command {
var (
confirmAll bool
cobraCmd = &cobra.Command{
Use: "delete",
Aliases: []string{"remove", "erase", "rm", "del"},
Short: "Delete a secret",
Long: `Delete a secret`,
Run: func(_ *cobra.Command, args []string) {
if len(args) != 1 {
fmt.Fprintln(os.Stderr, "Must provide a secret name to delete.")
return
}
secretName := args[0]
if !confirmAll {
confirm, err := userConfirm(bufio.NewReader(os.Stdin),
fmt.Sprintf("This will delete secret %s.", secretName))
if err != nil {
fmt.Fprintln(os.Stderr, "Error getting response:", err)
return
}
if !confirm {
fmt.Println("Skipping delete")
return
}
}
deleteSecret(secretName)
},
}
)
cobraCmd.Flags().BoolP(optionStdio, "", false, "load with stdin, save with stdout")
cobraCmd.Flags().BoolVarP(&confirmAll, optionYes, "y", false, "confirm all prompts")
return cobraCmd
}

View File

@@ -1,4 +1,4 @@
package cmd
package commands
import (
"os"
@@ -8,10 +8,16 @@ import (
)
func TestConfigDelete(t *testing.T) {
defaults()
collectionFile.filename = "testcollection.json"
secretList := createTestData(t)
configDeleteCmd := getConfigDeleteCmd()
configDeleteCmd.Run(nil, []string{"secret"})
_ = configDeleteCmd.Flags().Set(optionYes, "true")
// Secret does not exit
configDeleteCmd.Run(nil, []string{"secret"})

124
commands/configlist.go Normal file
View File

@@ -0,0 +1,124 @@
package commands
import (
"fmt"
"io"
"os"
"sort"
"strings"
"github.com/arcanericky/totp"
"github.com/spf13/cobra"
)
func titleLine(len int) string {
var builder strings.Builder
builder.Grow(len)
for i := 0; i < len; i++ {
builder.WriteString("-")
}
return builder.String()
}
func listSecretNames(writer io.Writer, secrets []totp.Secret) {
for _, s := range secrets {
fmt.Fprintln(writer, s.Name)
}
}
func listInfo(writer io.Writer, secrets []totp.Secret, all bool) {
const (
nameTitle = "Name"
secretTitle = "Secret"
addedDateTitle = "Date Added"
modifiedDateTitle = "Date Modified"
)
maxNameLen := len(nameTitle)
maxSecretLen := len(secretTitle)
for _, s := range secrets {
nameLen := len(s.Name)
if nameLen > maxNameLen {
maxNameLen = nameLen
}
secretLen := len(s.Value)
if secretLen > maxSecretLen {
maxSecretLen = secretLen
}
}
const timeFormat = "Jan _2 2006 15:04:05"
timeFormatLen := len(timeFormat)
timeFormatLine := titleLine(len(timeFormat))
if all {
fmt.Fprintf(writer, "%-*s %-*s %-*s %-*s\n",
maxNameLen, nameTitle,
maxSecretLen, secretTitle,
timeFormatLen, addedDateTitle,
timeFormatLen, modifiedDateTitle)
fmt.Fprintf(writer, "%s %s %s %s\n", titleLine(maxNameLen), titleLine(maxSecretLen), timeFormatLine, timeFormatLine)
for _, s := range secrets {
fmt.Fprintf(writer, "%-*s %-*s %s %s\n", maxNameLen, s.Name, maxSecretLen, s.Value, s.DateAdded.Format(timeFormat), s.DateModified.Format(timeFormat))
}
return
}
fmt.Fprintf(writer, "%-*s %-*s %-*s\n",
maxNameLen, nameTitle,
timeFormatLen, addedDateTitle,
timeFormatLen, modifiedDateTitle)
fmt.Fprintf(writer, "%s %s %s\n", titleLine(maxNameLen), timeFormatLine, timeFormatLine)
for _, s := range secrets {
fmt.Fprintf(writer, "%-*s %s %s\n", maxNameLen, s.Name, s.DateAdded.Format(timeFormat), s.DateModified.Format(timeFormat))
}
}
func listSecrets(names, all bool) {
c, err := collectionFile.loader()
if err != nil {
fmt.Fprintln(os.Stderr, "Error loading collection", err)
return
}
secrets := c.GetSecrets()
sort.Slice(secrets, func(i, j int) bool {
return secrets[i].Name < secrets[j].Name
})
if names {
listSecretNames(os.Stdout, secrets)
} else {
listInfo(os.Stdout, secrets, all)
}
}
func getConfigListCmd() *cobra.Command {
var (
names bool
all bool
cobraCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls", "l"},
Short: "List secrets",
Long: `List secrets`,
Run: func(listCmd *cobra.Command, _ []string) {
if names && all {
fmt.Fprintln(os.Stderr, "Only one of --names or --all can be used.")
return
}
listSecrets(names, all)
},
}
)
cobraCmd.Flags().BoolVarP(&names, "names", "n", false, "list only secret names")
cobraCmd.Flags().BoolVarP(&all, "all", "a", false, "list all secret info")
cobraCmd.Flags().BoolP(optionStdio, "", false, "load data from stdin")
return cobraCmd
}

View File

@@ -1,4 +1,4 @@
package cmd
package commands
import (
"os"
@@ -10,9 +10,23 @@ func TestConfigList(t *testing.T) {
createTestData(t)
configListCmd := getConfigListCmd()
configListCmd.Run(configListCmd, []string{})
configListCmd.Flags().Set("names", "true")
// names only
_ = configListCmd.Flags().Set("names", "true")
configListCmd.Run(configListCmd, []string{})
// all
configListCmd = getConfigListCmd()
_ = configListCmd.Flags().Set("all", "true")
configListCmd.Run(configListCmd, []string{})
// names and all
configListCmd = getConfigListCmd()
_ = configListCmd.Flags().Set("all", "true")
_ = configListCmd.Flags().Set("names", "true")
configListCmd.Run(configListCmd, []string{})
configListCmd.ResetFlags()

53
commands/configrename.go Normal file
View File

@@ -0,0 +1,53 @@
package commands
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
)
func renameSecret(source, target string) {
if isReservedCommand(target) {
fmt.Fprintln(os.Stderr, "The name \""+target+"\" is reserved for the "+target+" command")
return
}
s, _ := collectionFile.loader()
if _, err := s.RenameSecret(source, target); err != nil {
fmt.Fprintln(os.Stderr, "Error renaming secret:", err)
return
}
if err := s.Save(); err != nil {
fmt.Fprintln(os.Stderr, "Error saving settings:", err)
return
}
if _, err := printResultf("Renamed secret %s to %s\n", source, target); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
}
func getConfigRenameCmd(rootCmd *cobra.Command) *cobra.Command {
var cobraCmd = &cobra.Command{
Use: "rename",
Aliases: []string{"ren", "mv"},
Short: "Rename a secret",
Long: `Rename a secret`,
Run: func(_ *cobra.Command, args []string) {
if len(args) != 2 {
fmt.Fprintln(os.Stderr, "Must provide source and target.")
return
}
renameSecret(args[0], args[1])
},
}
cobraCmd.Flags().BoolP(optionStdio, "", false, "load with stdin, save with stdout")
cobraCmd.SetUsageTemplate(strings.Replace(rootCmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}}\n {{.CommandPath}} [old secret name] [new secret name]", 1))
return cobraCmd
}

View File

@@ -1,4 +1,4 @@
package cmd
package commands
import (
"os"
@@ -12,6 +12,8 @@ func TestConfigRename(t *testing.T) {
secrets := createTestData(t)
configRenameCmd := getConfigRenameCmd(getRootCmd())
configRenameCmd.Run(nil, []string{})
// Valid parameters
@@ -28,15 +30,16 @@ func TestConfigRename(t *testing.T) {
}
// Test rename to config
configRenameCmd.Run(nil, []string{newName, configCmd.Use})
configCmdUse := "config"
configRenameCmd.Run(nil, []string{newName, configCmdUse})
c, err = totp.NewCollectionWithFile(collectionFile.filename)
if err != nil {
t.Error("Could not load collection for rename test from file")
}
_, err = c.GetSecret(configCmd.Use)
_, err = c.GetSecret(configCmdUse)
if err == nil {
t.Error("Secret should not have been renamed to \"" + configCmd.Use + "\"")
t.Error("Secret should not have been renamed to \"" + configCmdUse + "\"")
}
// No collections file

50
commands/configreset.go Normal file
View File

@@ -0,0 +1,50 @@
package commands
import (
"bufio"
"fmt"
"os"
"github.com/spf13/cobra"
)
func configReset(filename string) error {
if err := os.Remove(filename); err != nil {
fmt.Fprintf(os.Stderr, "Error removing collection file %s: %s\n", filename, err)
return err
}
fmt.Printf("Collection file %s removed\n", filename)
return nil
}
func getConfigResetCmd() *cobra.Command {
var (
confirmAll bool
cobraCmd = &cobra.Command{
Use: "reset",
Short: "Reset the TOTP colllection",
Long: "Reset the TOTP colllection",
Run: func(_ *cobra.Command, _ []string) {
if !confirmAll {
confirm, err := userConfirm(bufio.NewReader(os.Stdin), "This will remove all secrets.")
if err != nil {
fmt.Fprintln(os.Stderr, "Error getting response:", err)
return
}
if !confirm {
fmt.Println("Skipping reset")
return
}
}
_ = configReset(collectionFile.filename)
},
}
)
cobraCmd.Flags().BoolVarP(&confirmAll, optionYes, "y", false, "confirm all prompts")
return cobraCmd
}

View File

@@ -0,0 +1,27 @@
package commands
import (
"os"
"testing"
)
func TestConfigReset(t *testing.T) {
collectionFile.filename = "testcollection.json"
createTestData(t)
configResetCmd := getConfigResetCmd()
_ = configResetCmd.Flags().Set(optionYes, "true")
configResetCmd.Run(nil, []string{})
_, err := os.Stat(collectionFile.filename)
if !os.IsNotExist(err) {
t.Error("Failed to remove the collection file")
}
configResetCmd = getConfigResetCmd()
configResetCmd.Run(nil, []string{})
if err := configReset("nosuchfilename.example"); err == nil {
t.Error("Failed to generate error removing invalid collection file")
}
}

61
commands/configupdate.go Normal file
View File

@@ -0,0 +1,61 @@
package commands
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
)
func updateSecret(name, value string) {
if isReservedCommand(name) {
fmt.Fprintln(os.Stderr, "The name \""+name+"\" is reserved for the "+name+" command")
return
}
// ignore error because file may not exist
s, _ := collectionFile.loader()
secret, err := s.UpdateSecret(name, value)
if err != nil {
fmt.Fprintln(os.Stderr, "Error updating secret:", err)
return
}
if err := s.Save(); err != nil {
fmt.Fprintln(os.Stderr, "Error saving settings:", err)
return
}
action := "Updated"
if secret.DateAdded == secret.DateModified {
action = "Added"
}
if _, err := printResultf("%s secret %s\n", action, name); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
}
func getConfigUpdateCmd(rootCmd *cobra.Command) *cobra.Command {
var cobraCmd = &cobra.Command{
Use: "update",
Aliases: []string{"add"},
Short: "Add or update a secret",
Long: `Add or update a secret`,
Run: func(_ *cobra.Command, args []string) {
if len(args) != 2 {
fmt.Fprintln(os.Stderr, "Must provide name and secret")
return
}
updateSecret(args[0], args[1])
},
}
cobraCmd.Flags().BoolP(optionStdio, "", false, "load with stdin, save with stdout")
cobraCmd.SetUsageTemplate(strings.Replace(rootCmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}}\n {{.CommandPath}} [secret name] [secret value]", 1))
return cobraCmd
}

View File

@@ -1,4 +1,4 @@
package cmd
package commands
import (
"os"
@@ -12,6 +12,8 @@ func TestConfigUpdate(t *testing.T) {
createTestData(t)
configUpdateCmd := getConfigUpdateCmd(getRootCmd())
// Valid parameters
secretName := "testsecret"
configUpdateCmd.Run(nil, []string{secretName, "seed"})
@@ -26,7 +28,7 @@ func TestConfigUpdate(t *testing.T) {
}
// Test update secret
newSecret := "seedseed"
newSecret := "SEEDSEED"
configUpdateCmd.Run(nil, []string{secretName, newSecret})
c, err = totp.NewCollectionWithFile(collectionFile.filename)
if err != nil {
@@ -39,7 +41,7 @@ func TestConfigUpdate(t *testing.T) {
}
// Test using secret named 'config'
secretName = configCmd.Use
secretName = "config"
configUpdateCmd.Run(nil, []string{secretName, "seed"})
c, err = totp.NewCollectionWithFile(collectionFile.filename)
if err != nil {
@@ -48,7 +50,7 @@ func TestConfigUpdate(t *testing.T) {
secret, err = c.GetSecret(secretName)
if err == nil {
t.Error("Secret named \"" + configCmd.Use + "\" should not have been saved")
t.Error("Secret named \"" + secretName + "\" should not have been saved")
}
// No parameters passed

26
commands/confirm.go Normal file
View File

@@ -0,0 +1,26 @@
package commands
import (
"bufio"
"fmt"
"strings"
)
func userConfirm(reader *bufio.Reader, prompt string) (bool, error) {
for {
fmt.Printf("%s Continue? [y/n]: ", prompt)
response, err := reader.ReadString('\n')
if err != nil {
return false, err
}
response = strings.ToLower(strings.TrimSpace(response))
if response == "y" || response == "yes" {
return true, nil
} else if response == "n" || response == "no" {
return false, nil
}
}
}

68
commands/confirm_test.go Normal file
View File

@@ -0,0 +1,68 @@
package commands
import (
"bufio"
"strings"
"testing"
)
func Test_userConfirm(t *testing.T) {
type args struct {
reader *bufio.Reader
prompt string
}
tests := []struct {
name string
args args
want bool
wantErr bool
}{
{
name: "yes",
args: args{
reader: bufio.NewReader(strings.NewReader("yes\n")),
prompt: "",
},
want: true,
wantErr: false,
},
{
name: "y",
args: args{
reader: bufio.NewReader(strings.NewReader("y\n")),
prompt: "",
},
want: true,
wantErr: false,
},
{
name: "no",
args: args{
reader: bufio.NewReader(strings.NewReader("no\n")),
prompt: "",
},
want: false,
wantErr: false,
},
{
name: "n",
args: args{
reader: bufio.NewReader(strings.NewReader("n\n")),
prompt: "",
},
want: false,
wantErr: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := userConfirm(tt.args.reader, tt.args.prompt)
if (err != nil) != tt.wantErr {
t.Errorf("userConfirm() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("userConfirm() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -1,4 +1,4 @@
package cmd
package commands
import (
"os"
@@ -8,7 +8,13 @@ import (
"github.com/arcanericky/totp"
)
const defaultBaseCollectionFile = "totp-config.json"
const (
defaultBaseCollectionFile = "totp-config.json"
cmdVersion = "version"
cmdConfig = "config"
cmdCompletion = "completion"
)
var collectionFile struct {
filename string
@@ -28,14 +34,20 @@ func loadCollectionFromDefaultFile() (*totp.Collection, error) {
}
func setCollectionFile(goos string) {
if totpFile := os.Getenv("TOTP_CONFIG"); totpFile != "" {
collectionFile.filename = totpFile
return
}
if goos == "windows" {
collectionFile.filename = filepath.Join(os.Getenv("LOCALAPPDATA"), defaultBaseCollectionFile)
} else {
collectionFile.filename = filepath.Join(os.Getenv("HOME"), "."+defaultBaseCollectionFile)
return
}
collectionFile.filename = filepath.Join(os.Getenv("HOME"), "."+defaultBaseCollectionFile)
}
var reservedCommands = []string{configCmd.Use, versionCmd.Use}
var reservedCommands = []string{cmdConfig, cmdVersion, cmdCompletion}
func isReservedCommand(name string) bool {
for _, c := range reservedCommands {
@@ -47,7 +59,7 @@ func isReservedCommand(name string) bool {
return false
}
func init() {
func defaults() {
setCollectionFile(runtime.GOOS)
collectionFile.loader = loadCollectionFromDefaultFile
}

View File

@@ -1,4 +1,4 @@
package cmd
package commands
import (
"os"
@@ -7,7 +7,6 @@ import (
)
func TestDefaults(t *testing.T) {
// Doesn't set and check exactly under on Linux but good enough for test
setCollectionFile("windows")
if collectionFile.filename != filepath.Join(os.Getenv("LOCALAPPDATA"), defaultBaseCollectionFile) {
t.Error("Windows collection file not set properly")
@@ -18,8 +17,14 @@ func TestDefaults(t *testing.T) {
t.Error("Runtime OS collection file not set properly")
}
os.Setenv("TOTP_CONFIG", "testcollectionfile.json")
setCollectionFile("windows")
if collectionFile.filename != os.Getenv("TOTP_CONFIG") {
t.Error("Collection file not set properly with environment variable")
}
// Not sure how to unit test but at least run it for now
loadCollectionFromStdin()
_, _ = loadCollectionFromStdin()
for _, c := range reservedCommands {
if isReservedCommand(c) != true {

View File

@@ -1,9 +1,9 @@
package cmd
package commands
import "fmt"
func printResultf(format string, a ...interface{}) (n int, err error) {
if collectionFile.useStdio == false {
if !collectionFile.useStdio {
return fmt.Printf(format, a...)
}

View File

@@ -1,4 +1,4 @@
package cmd
package commands
import (
"testing"
@@ -6,7 +6,7 @@ import (
func TestPrintResult(t *testing.T) {
text := "test text"
printResultf(text)
_, _ = printResultf(text)
collectionFile.useStdio = true
printResultf(text)
_, _ = printResultf(text)
}

63
commands/qrcode.go Normal file
View File

@@ -0,0 +1,63 @@
package commands
import (
"errors"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/pquerna/otp/totp"
"github.com/skip2/go-qrcode"
)
func getQrString(name, secret string) string {
secret = strings.ToUpper(secret)
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
// otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
return fmt.Sprintf("otpauth://totp/%s?secret=%s&issuer=%s", name, secret, name)
}
func outputQrCode(writer io.Writer, name, secret string) error {
qrString := getQrString(name, secret)
q, err := qrcode.New(qrString, qrcode.Medium)
if err != nil {
fmt.Fprintln(os.Stderr, "Error generating qr code:", err)
return err
}
fmt.Fprint(writer, q.ToSmallString(false))
return nil
}
func qrCode(writer io.Writer, name, secret string) error {
if len(name) == 0 {
fmt.Fprintln(os.Stderr, "Name required for QR code generation")
return errors.New("name required")
}
if len(secret) != 0 {
_, err := totp.GenerateCode(secret, time.Now())
if err != nil {
fmt.Fprintln(os.Stderr, "Invalid secret:", err)
return err
}
return outputQrCode(writer, name, secret)
}
c, err := collectionFile.loader()
if err != nil {
fmt.Fprintln(os.Stderr, "Error loading collection:", err)
return err
}
s, err := c.GetSecret(name)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get collection entry for %s: %s\n", name, err)
return err
}
return outputQrCode(writer, s.Name, s.Value)
}

246
commands/qrcode_test.go Normal file
View File

@@ -0,0 +1,246 @@
package commands
import (
"bytes"
"encoding/base64"
"math/rand"
"os"
"testing"
)
var testQrCode = `4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI
4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI
4paI4paI4paICuKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKW
iOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKW
iOKWiOKWiOKWiOKWiOKWiOKWiArilojilojilojilogg4paE4paE4paE4paE4paEIOKWiOKWhOKW
hOKWgOKWiOKWiCDiloDiloQg4paI4paE4paA4paI4paA4paE4paE4paA4paIIOKWhOKWhOKWhOKW
hOKWhCDilojilojilojilogK4paI4paI4paI4paIIOKWiCAgIOKWiCDilojiloDiloAg4paI4paA
4paI4paEIOKWgOKWhOKWgOKWiOKWgOKWhOKWiCAg4paIIOKWiCAgIOKWiCDilojilojilojilogK
4paI4paI4paI4paIIOKWiOKWhOKWhOKWhOKWiCDilogg4paEIOKWhOKWgCDilojiloDiloTiloTi
loTiloDilojilojiloTiloTilojilogg4paI4paE4paE4paE4paIIOKWiOKWiOKWiOKWiAriloji
lojilojilojiloTiloTiloTiloTiloTiloTiloTilogg4paAIOKWiCDilojiloTilojiloTiloAg
4paA4paE4paAIOKWiOKWhOKWiOKWhOKWhOKWhOKWhOKWhOKWhOKWhOKWiOKWiOKWiOKWiAriloji
lojilojilojiloTilojiloDilogg4paI4paE4paEIOKWiOKWgOKWiOKWiOKWiOKWhCDiloAg4paA
4paE4paE4paE4paI4paA4paI4paE4paE4paE4paEIOKWgOKWiCDilojilojilojilogK4paI4paI
4paI4paIIOKWgCAgIOKWgOKWhOKWgOKWiOKWiOKWiOKWgOKWhCDiloTiloQg4paA4paE4paA4paA
IOKWiOKWhCDilojiloAg4paE4paE4paIIOKWiOKWiOKWiOKWiOKWiArilojilojilojilojiloDi
loTilojiloTilogg4paE4paA4paE4paA4paIIOKWhOKWhOKWgOKWiOKWhOKWhOKWgOKWiOKWiCDi
loQg4paI4paAICDilojiloTiloDilojiloTilojilojilojilogK4paI4paI4paI4paI4paIIOKW
hCDiloDiloTiloTilojiloQg4paE4paE4paI4paIICDiloAgIOKWiCDiloDilogg4paE4paIIOKW
gOKWgOKWiOKWhCDiloDilojilojilojilogK4paI4paI4paI4paI4paI4paE4paA4paI4paE4paA
4paE4paI4paA4paA4paE4paIIOKWiOKWgOKWgCDiloTiloTiloTiloTiloQgIOKWhCDiloTiloDi
loTiloQg4paA4paA4paI4paI4paI4paICuKWiOKWiOKWiOKWiOKWiOKWgOKWiCAg4paE4paE4paA
4paEIOKWgOKWhCDilojiloDiloTiloAg4paA4paI4paAIOKWiCDiloAg4paI4paEIOKWhOKWiOKW
hOKWiOKWiOKWiOKWiOKWiArilojilojilojilogg4paE4paA4paE4paI4paA4paE4paI4paA4paA
4paIIOKWiOKWhOKWiOKWiOKWgCDiloDiloTiloTiloQg4paA4paE4paIIOKWgOKWiOKWgOKWgOKW
gOKWhOKWiOKWiOKWiOKWiArilojilojilojilojilojilojilojiloTiloTiloTiloTiloTilogg
4paE4paE4paE4paA4paA4paI4paE4paAIOKWiOKWgCDiloDiloTiloAgIOKWiCDiloTilogg4paI
4paI4paI4paI4paICuKWiOKWiOKWiOKWiOKWhOKWhOKWiOKWiOKWiOKWiOKWhOKWiCAgIOKWiOKW
gCDilogg4paAIOKWiOKWhOKWiOKWhOKWhOKWgCDiloTiloTiloQg4paEIOKWhOKWgOKWiOKWiOKW
iOKWiArilojilojilojilogg4paE4paE4paE4paE4paEIOKWiOKWhOKWiOKWiCAg4paI4paAIOKW
iCAg4paA4paE4paAIOKWiCDilojiloTilogg4paI4paIIOKWiOKWiOKWiOKWiOKWiArilojiloji
lojilogg4paIICAg4paIIOKWiOKWhOKWiCDiloTilogg4paI4paI4paI4paE4paA4paE4paI4paE
4paA4paA4paEICAgIOKWhOKWgOKWiOKWiOKWiOKWiOKWiOKWiArilojilojilojilogg4paI4paE
4paE4paE4paIIOKWiOKWiOKWhOKWiOKWiCDiloAgIOKWiOKWgOKWgOKWiCAg4paI4paAIOKWgOKW
hOKWiCDilojiloTilojilojilojilojilojilogK4paI4paI4paI4paI4paE4paE4paE4paE4paE
4paE4paE4paI4paE4paI4paE4paE4paE4paE4paE4paI4paI4paE4paI4paI4paI4paE4paE4paI
4paE4paE4paE4paE4paI4paI4paE4paI4paE4paI4paI4paI4paICuKWiOKWiOKWiOKWiOKWiOKW
iOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKW
iOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiAriloDiloDi
loDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDi
loDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDi
loAK`
func RandStringBytes(n int) string {
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}
func Test_getQrString(t *testing.T) {
type args struct {
name string
secret string
}
tests := []struct {
name string
args args
want string
}{
{
name: "success",
args: args{
name: "testname",
secret: "testsecret",
},
want: "otpauth://totp/testname?secret=TESTSECRET&issuer=testname",
},
{
name: "success",
args: args{
name: "testname",
secret: "TESTSECRET",
},
want: "otpauth://totp/testname?secret=TESTSECRET&issuer=testname",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getQrString(tt.args.name, tt.args.secret); got != tt.want {
t.Errorf("getQrString() = %v, want %v", got, tt.want)
}
})
}
}
func Test_qrCode(t *testing.T) {
collectionFile.filename = "testcollection.json"
createTestData(t)
collectionFile.loader = loadCollectionFromDefaultFile
decoded, _ := base64.StdEncoding.DecodeString(testQrCode)
type args struct {
name string
secret string
}
tests := []struct {
name string
filename string
args args
wantWriter string
wantErr bool
}{
{
name: "name and secret",
filename: "testcollection.json",
args: args{
name: "testname",
secret: "testsecret",
},
wantWriter: string(decoded),
wantErr: false,
},
{
name: "name from collection",
filename: "testcollection.json",
args: args{
name: "testname",
secret: "",
},
wantWriter: string(decoded),
wantErr: false,
},
{
name: "empty name",
filename: "testcollection.json",
args: args{
name: "",
secret: "testsecret",
},
wantWriter: "",
wantErr: true,
},
{
name: "no collecton entry",
filename: "testcollection.json",
args: args{
name: "invalidname",
secret: "",
},
wantWriter: "",
wantErr: true,
},
{
name: "no collecton file",
filename: "invalidcollectionfile.json",
args: args{
name: "invalidname",
secret: "",
},
wantWriter: "",
wantErr: true,
},
{
name: "invalid secret",
filename: "testcollection.json",
args: args{
name: "testname",
secret: "seed0",
},
wantWriter: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
collectionFile.filename = tt.filename
writer := &bytes.Buffer{}
if err := qrCode(writer, tt.args.name, tt.args.secret); (err != nil) != tt.wantErr {
t.Errorf("qrCode() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotWriter := writer.String(); gotWriter != tt.wantWriter {
t.Errorf("qrCode() = %v, want %v", gotWriter, tt.wantWriter)
}
})
}
os.Remove(collectionFile.filename)
}
func Test_outputQrCode(t *testing.T) {
decoded, _ := base64.StdEncoding.DecodeString(testQrCode)
type args struct {
name string
secret string
}
tests := []struct {
name string
args args
wantWriter string
wantErr bool
}{
{
name: "valid name and secret",
args: args{
name: "testname",
secret: "TESTSECRET",
},
wantWriter: string(decoded),
wantErr: false,
},
{
name: "valid name and lowercase secret",
args: args{
name: "testname",
secret: "testsecret",
},
wantWriter: string(decoded),
wantErr: false,
},
{
name: "data too long",
args: args{
name: RandStringBytes(2048),
secret: RandStringBytes(2048),
},
wantWriter: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
writer := &bytes.Buffer{}
if err := outputQrCode(writer, tt.args.name, tt.args.secret); (err != nil) != tt.wantErr {
t.Errorf("outputQrCode() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotWriter := writer.String(); gotWriter != tt.wantWriter {
t.Errorf("outputQrCode() = %v, want %v", gotWriter, tt.wantWriter)
}
})
}
}

291
commands/root.go Normal file
View File

@@ -0,0 +1,291 @@
package commands
import (
"fmt"
"io"
"os"
"strings"
"time"
api "github.com/arcanericky/totp"
"github.com/pquerna/otp/totp"
"github.com/spf13/cobra"
)
const (
optionBackward = "backward"
optionFile = "file"
optionFollow = "follow"
optionForward = "forward"
optionQr = "qrcode"
optionSecret = "secret"
optionStdio = "stdio"
optionTime = "time"
optionYes = "yes"
)
type generateCodesAPI func(time.Duration, time.Duration, time.Duration, func(time.Duration), string, string)
type runVars struct {
secret string
backward time.Duration
forward time.Duration
timeString string
follow bool
useStdio bool
cfgFile string
qr bool
}
var generateCodesService generateCodesAPI
func getSecretNamesForCompletion(toComplete string) []string {
var (
secretNames []string
err error
c *api.Collection
)
c, err = collectionFile.loader()
if err != nil {
fmt.Fprintln(os.Stderr, "Error loading collection:", err)
} else {
secrets := c.GetSecrets()
for _, s := range secrets {
if strings.HasPrefix(s.Name, toComplete) {
secretNames = append(secretNames, s.Name)
}
}
}
return secretNames
}
func generateCode(writer io.Writer, name string, secret string, t time.Time) error {
const errGen = "Error generating code:"
if len(secret) != 0 {
code, err := totp.GenerateCode(secret, t)
if err != nil {
fmt.Fprintln(os.Stderr, errGen, err)
return err
}
fmt.Fprintln(writer, code)
return nil
}
c, err := collectionFile.loader()
if err != nil {
fmt.Fprintln(os.Stderr, "Error loading collection:", err)
return err
}
code, err := c.GenerateCodeWithTime(name, t)
if err != nil {
fmt.Fprintln(os.Stderr, errGen, err)
return err
}
fmt.Fprintln(writer, code)
return nil
}
func durationToNextInterval(now time.Time) time.Duration {
var sleepSeconds int
s := now.Second()
switch {
case s == 0, s < 30:
sleepSeconds = 30 - s
case s >= 30:
sleepSeconds = 60 - s
}
return time.Duration(sleepSeconds)*time.Second -
time.Duration(now.Nanosecond())*time.Nanosecond
}
func callOnInterval(runtime time.Duration, interval time.Duration, exec func() bool) {
stopper := make(chan bool)
if runtime > 0 {
go func() {
time.Sleep(runtime)
stopper <- true
}()
}
if exec != nil && exec() {
return
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-stopper:
return
case <-ticker.C:
if exec != nil && exec() {
return
}
}
}
}
func generateCodes(timeOffset time.Duration, durationToRun time.Duration, intervalTime time.Duration, sleep func(time.Duration), secretName, secret string) {
sleep(durationToNextInterval(time.Now().Add(timeOffset)) + 10*time.Millisecond)
callOnInterval(durationToRun, intervalTime,
func() bool {
if err := generateCode(os.Stdout, secretName, secret, time.Now().Add(timeOffset)); err != nil {
fmt.Fprintln(os.Stderr, err)
return true
}
return false
})
}
func run(cmd *cobra.Command, args []string, cfg runVars) {
// var err error
secretLen := len(cfg.secret)
argsLen := len(args)
errMsg := ""
switch {
// No secret, no secret name
case secretLen == 0 && argsLen == 0:
errMsg = "Secret name or secret is required."
// Secret given but additional arguments were also given
case !cfg.qr && secretLen > 0 && argsLen > 0:
errMsg = "Secret was given so additional arguments are not needed."
// No secret given and too many args
case secretLen == 0 && argsLen > 1:
errMsg = "Too many arguments. Only one secret name is required."
}
if errMsg != "" {
fmt.Fprintf(os.Stderr, errMsg+"\n\n")
if err := cmd.Help(); err != nil {
fmt.Fprintln(os.Stderr, err)
}
return
}
if cfg.qr {
secretName := ""
if argsLen == 1 {
secretName = args[0]
}
_ = qrCode(os.Stdout, secretName, cfg.secret)
return
}
// Override if time was given
var (
codeTime time.Time
err error
)
if len(cfg.timeString) > 0 {
codeTime, err = time.Parse(time.RFC3339, cfg.timeString)
if err != nil {
fmt.Fprintln(os.Stderr, "Error parsing the time option:", err)
return
}
} else {
codeTime = time.Now()
// codeOffset is 0
}
// Load the secret name
secretName := ""
if argsLen == 1 {
secretName = args[0]
}
// If here then a stored shared secret is wanted
if err := generateCode(os.Stdout, secretName, cfg.secret, codeTime.Add(cfg.forward-cfg.backward)); err != nil {
// generateCode will output error text
return
}
if cfg.follow {
generateCodesService(time.Until(codeTime)-cfg.backward+cfg.forward, 0, 30*time.Second, time.Sleep, secretName, cfg.secret)
}
}
func getRootCmd() *cobra.Command {
var cfg runVars
var cobraCmd = &cobra.Command{
Use: "totp",
Short: "TOTP Generator",
Long: `TOTP Generator`,
Args: cobra.ArbitraryArgs,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if cmd.Flags().Changed(optionFile) {
collectionFile.filename = cfg.cfgFile
}
if cmd.Flags().Lookup(optionStdio) != nil {
if cfg.useStdio {
collectionFile.loader = loadCollectionFromStdin
collectionFile.useStdio = true
}
}
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return getSecretNamesForCompletion(toComplete), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
run(cmd, args, cfg)
},
}
var duration time.Duration
generateCodesService = generateCodes
cobraCmd.PersistentFlags().StringVarP(&cfg.cfgFile, optionFile, "f", "", "secret collection file")
cobraCmd.Flags().StringVarP(&cfg.secret, optionSecret, "s", "", "TOTP secret value")
cobraCmd.Flags().BoolVarP(&cfg.useStdio, optionStdio, "", false, "load with stdin")
cobraCmd.Flags().StringVarP(&cfg.timeString, optionTime, "", "", "RFC3339 time for TOTP (2019-06-23T20:00:00-05:00)")
cobraCmd.Flags().DurationVarP(&cfg.backward, optionBackward, "", duration, "move time backward (ex. \"30s\")")
cobraCmd.Flags().DurationVarP(&cfg.forward, optionForward, "", duration, "move time forward (ex. \"1m\")")
cobraCmd.Flags().BoolVarP(&cfg.follow, optionFollow, "", false, "continuous output")
cobraCmd.Flags().BoolVarP(&cfg.qr, optionQr, "", false, "output QR code")
cobraCmd.SetUsageTemplate(strings.Replace(cobraCmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}}\n {{.CommandPath}} [secret name]", 1))
cobraCmd.AddCommand(getVersionCmd())
cobraCmd.AddCommand(getConfigCmd(cobraCmd))
return cobraCmd
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() int {
retVal := 0
defaults()
rootCmd := getRootCmd()
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
retVal = 1
}
return retVal
}

421
commands/root_test.go Normal file
View File

@@ -0,0 +1,421 @@
package commands
import (
"bytes"
"os"
"testing"
"time"
"github.com/spf13/cobra"
)
func TestRoot(t *testing.T) {
Execute()
collectionFile.filename = "testcollection.json"
secretList := createTestData(t)
rootCmd := getRootCmd()
// No parameters
rootCmd.Run(rootCmd, []string{})
// Valid entry and secret
rootCmd.Run(rootCmd, []string{secretList[0].name})
// Non-existing entry
rootCmd.Run(rootCmd, []string{"invalidsecret"})
// Test follow condition
savedGenerateCodesService := generateCodesService
generateCodesService = func(time.Duration, time.Duration, time.Duration, func(time.Duration), string, string) {}
_ = rootCmd.Flags().Lookup(optionFollow).Value.Set("true")
rootCmd.Run(rootCmd, []string{"name0"})
generateCodesService = savedGenerateCodesService
_ = rootCmd.Flags().Lookup(optionFollow).Value.Set("false")
// Completion
rootCmd.ValidArgsFunction(rootCmd, []string{}, "na")
// Completion with args
rootCmd.ValidArgsFunction(rootCmd, []string{"secret"}, "na")
// No collections file
os.Remove(collectionFile.filename)
rootCmd.Run(rootCmd, []string{secretList[0].name})
// Completion without collections
rootCmd.ValidArgsFunction(rootCmd, []string{}, "na")
// Excessive args
rootCmd.Run(rootCmd, []string{"secretname", "extraarg"})
// Provide secret option
_ = rootCmd.Flags().Set(optionSecret, "seed")
rootCmd.Run(rootCmd, []string{})
// Provide invalid secret option
_ = rootCmd.Flags().Set(optionSecret, "seed1")
rootCmd.Run(rootCmd, []string{})
// File option
_ = rootCmd.Flags().Set(optionFile, collectionFile.filename)
rootCmd.Flags().Lookup(optionFile).Changed = true
rootCmd.PersistentPreRun(rootCmd, []string{"secret"})
// Stdio option
_ = rootCmd.Flags().Set(optionStdio, "true")
rootCmd.PersistentPreRun(rootCmd, []string{"secret"})
collectionFile.loader = loadCollectionFromDefaultFile
collectionFile.useStdio = false
_ = rootCmd.Flags().Set(optionStdio, "")
// Time option
_ = rootCmd.Flags().Set(optionTime, "2019-06-01T20:00:00-05:00")
rootCmd.Run(rootCmd, []string{})
// Give secret and secret name
_ = rootCmd.Flags().Set(optionSecret, "seed")
rootCmd.Run(rootCmd, []string{"secretname"})
_ = rootCmd.Flags().Set(optionSecret, "")
// Invalid time option
_ = rootCmd.Flags().Set(optionTime, "invalidtime")
rootCmd.Run(rootCmd, []string{})
_ = rootCmd.Flags().Set(optionTime, "")
os.Remove(collectionFile.filename)
}
func Test_run(t *testing.T) {
collectionFile.filename = "testcollection.json"
_ = createTestData(t)
type args struct {
cmd *cobra.Command
args []string
cfg runVars
}
tests := []struct {
name string
args args
}{
{
name: "qr code",
args: args{
cmd: &cobra.Command{},
args: []string{"testname"},
cfg: runVars{qr: true},
},
},
{
name: "secret name or secret required",
args: args{
cmd: &cobra.Command{},
args: []string{},
cfg: runVars{secret: ""},
},
},
{
name: "secret + additional args",
args: args{
cmd: &cobra.Command{},
args: []string{"name"},
cfg: runVars{
qr: false,
secret: "seed",
},
},
},
{
name: "too many arguments",
args: args{
cmd: &cobra.Command{},
args: []string{"name", "extra-arg"},
cfg: runVars{
qr: false,
secret: "",
},
},
},
{
name: "secret from collection",
args: args{
cmd: &cobra.Command{},
args: []string{"name3"},
cfg: runVars{
qr: false,
secret: "",
},
},
},
{
name: "secret from collection now found",
args: args{
cmd: &cobra.Command{},
args: []string{"invalidsecretname"},
cfg: runVars{
qr: false,
secret: "",
},
},
},
{
name: "time parse error",
args: args{
cmd: &cobra.Command{},
args: []string{"testname"},
cfg: runVars{
timeString: "invalidtime",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
run(tt.args.cmd, tt.args.args, tt.args.cfg)
})
}
os.Remove(collectionFile.filename)
}
func Test_generateCodes(t *testing.T) {
var duration time.Duration
type args struct {
timeOffset time.Duration
durationToRun time.Duration
intervalTime time.Duration
sleep func(time.Duration)
secretName string
secret string
}
tests := []struct {
name string
args args
}{
{
name: "valid seed",
args: args{
timeOffset: duration,
durationToRun: 2 * time.Millisecond,
intervalTime: 1 * time.Millisecond,
sleep: func(d time.Duration) {},
secretName: "",
secret: "seed",
},
},
{
name: "invalid seed",
args: args{
timeOffset: duration,
durationToRun: 2 * time.Millisecond,
intervalTime: 1 * time.Millisecond,
sleep: func(d time.Duration) {},
secretName: "",
secret: "invalidseed",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
generateCodes(tt.args.timeOffset, tt.args.durationToRun, tt.args.intervalTime, tt.args.sleep, tt.args.secretName, tt.args.secret)
})
}
}
func Test_callOnInterval(t *testing.T) {
execCount := 0
preAndExecNormal := func() bool { return false }
preAndExecEarlyExit := func() bool { execCount++; return execCount == 2 }
type args struct {
runtime time.Duration
interval time.Duration
exec func() bool
}
tests := []struct {
name string
startExecCount int
args args
}{
{
name: "normal execution",
startExecCount: 0,
args: args{
runtime: 2 * time.Millisecond,
interval: 1 * time.Millisecond,
exec: preAndExecNormal,
},
},
{
name: "exit at preExec",
startExecCount: 1,
args: args{
runtime: 2 * time.Millisecond,
interval: 1 * time.Millisecond,
exec: preAndExecNormal,
},
},
{
name: "exit at top exec",
startExecCount: 1,
args: args{
runtime: 2 * time.Millisecond,
interval: 1 * time.Millisecond,
exec: preAndExecEarlyExit,
},
},
{
name: "exit at loop exec",
startExecCount: 0,
args: args{
runtime: 2 * time.Millisecond,
interval: 1 * time.Millisecond,
exec: preAndExecEarlyExit,
},
},
{
name: "no callbacks",
startExecCount: 0,
args: args{
runtime: 2 * time.Millisecond,
interval: 1 * time.Millisecond,
exec: nil,
},
},
}
for _, tt := range tests {
execCount = tt.startExecCount
t.Run(tt.name, func(t *testing.T) {
callOnInterval(tt.args.runtime, tt.args.interval, tt.args.exec)
})
}
}
func Test_durationToNextInterval(t *testing.T) {
type args struct {
now string
}
tests := []struct {
name string
args args
want time.Duration
}{
{
name: "29 seconds",
args: args{
now: "2019-06-23T20:00:01-05:00",
},
want: time.Duration(29 * time.Second),
},
{
name: "29 seconds 2",
args: args{
now: "2019-06-23T20:00:31-05:00",
},
want: time.Duration(29 * time.Second),
},
{
name: "28 seconds",
args: args{
now: "2019-06-23T20:00:31.001-05:00",
},
want: time.Duration(28*time.Second + 999*time.Millisecond),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now, _ := time.Parse(time.RFC3339, tt.args.now)
if got := durationToNextInterval(now); got != tt.want {
t.Errorf("durationToNextInterval() = %v, want %v", got, tt.want)
}
})
}
}
func Test_generateCode(t *testing.T) {
now, _ := time.Parse(time.RFC3339, "2019-06-23T20:00:01-05:00")
type args struct {
name string
secret string
t time.Time
}
tests := []struct {
name string
args args
wantWriter string
wantErr bool
}{
{
name: "valid secret",
args: args{
name: "name",
secret: "seed",
t: now,
},
wantWriter: "335072\n",
wantErr: false,
},
{
name: "invalid secret",
args: args{
name: "name",
secret: "seed0",
t: now,
},
wantWriter: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
writer := &bytes.Buffer{}
if err := generateCode(writer, tt.args.name, tt.args.secret, tt.args.t); (err != nil) != tt.wantErr {
t.Errorf("generateCode() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotWriter := writer.String(); gotWriter != tt.wantWriter {
t.Errorf("generateCode() = %v, want %v", gotWriter, tt.wantWriter)
}
})
}
}
func Test_getSecretNamesForCompletion(t *testing.T) {
collectionFile.filename = "testcollection.json"
collectionFile.loader = loadCollectionFromDefaultFile
_ = createTestData(t)
type args struct {
toComplete string
}
tests := []struct {
name string
args args
want []string
}{
{
name: "names for completion",
args: args{
toComplete: "n",
},
want: []string{"name0", "name1", "name2", "name3", "name4"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getSecretNamesForCompletion(tt.args.toComplete)
for _, want := range tt.want {
i := 0
match := false
for i = range got {
if want == got[i] {
match = true
break
}
}
if !match {
t.Errorf("want: %s, got: %s", tt.want, got)
}
}
})
}
os.Remove(collectionFile.filename)
}

23
commands/version.go Normal file
View File

@@ -0,0 +1,23 @@
package commands
import (
"fmt"
"runtime"
"github.com/spf13/cobra"
)
var versionText = "unspecified"
func getVersionCmd() *cobra.Command {
var cobraCmd = &cobra.Command{
Use: cmdVersion,
Short: "Show totp version",
Long: "Show totp version",
Run: func(_ *cobra.Command, _ []string) {
fmt.Printf("totp version %s %s/%s\n", versionText, runtime.GOOS, runtime.GOARCH)
},
}
return cobraCmd
}

View File

@@ -1,9 +1,10 @@
package cmd
package commands
import (
"testing"
)
func TestVersion(t *testing.T) {
versionCmd := getVersionCmd()
versionCmd.Run(nil, []string{})
}

11
go.mod
View File

@@ -3,8 +3,13 @@ module github.com/arcanericky/totp
go 1.18
require (
github.com/boombuler/barcode v1.0.1 // indirect
github.com/pquerna/otp v1.3.0
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/cobra v1.5.0
)
require (
github.com/boombuler/barcode v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)

567
go.sum
View File

@@ -1,575 +1,24 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

187
scripts/totp-test.sh Executable file
View File

@@ -0,0 +1,187 @@
#!/bin/bash
set -e
TOTP=./totp-test
COLLECTION=testcollection.json
TEST_NBR=1
# Build
echo "Building ${TOTP}"
go build -o ${TOTP} -ldflags "-X main.version=$(./scripts/get-version.sh)" ./cmd/...
# Basic commands
echo "${TEST_NBR}: Testing basic commands"
${TOTP}
${TOTP} --help
# Test version
RESULT=$(${TOTP} version | head --bytes=12)
echo "Result: ${RESULT}"
if [[ ! ${RESULT} = "totp version" ]]; then
echo "FAIL: Version command"
exit 1
fi
# Test secret name or secret required
RESULT=$(${TOTP} 2>&1 | head --lines=1)
echo "Result: ${RESULT}"
if [[ ! ${RESULT} = "Secret name or secret is required." ]]; then
echo "FAIL: Secret name or secret required"
exit 1
fi
# Test secret was given so additional arguments are not needed
RESULT=$(${TOTP} --secret SEED additional arguments 2>&1 | head --lines=1)
echo "Result: ${RESULT}"
if [[ ! ${RESULT} = "Secret was given so additional arguments are not needed." ]]; then
echo "FAIL: Secret was given so additional arguments are not needed"
exit 1
fi
# Test too many arguments. Only one secret name is required
RESULT=$(${TOTP} too many arguments 2>&1 | head --lines=1)
echo "Result: ${RESULT}"
if [[ ! ${RESULT} = "Too many arguments. Only one secret name is required." ]]; then
echo "FAIL: Too many arguments. Only one secret name is required"
exit 1
fi
# Test completion output
RESULT=$(${TOTP} completion bash | head --lines=1)
echo "Result: ${RESULT}"
if [[ ! ${RESULT} = "# bash completion V2 for totp -*- shell-script -*-" ]]; then
echo "FAIL: Completion output"
exit 1
fi
# Test collection reset
((TEST_NBR++))
echo "${TEST_NBR}: Testing config reset"
touch ${COLLECTION}
${TOTP} config reset --file ${COLLECTION} --yes
if test -f "${COLLECTION}"; then
echo "FAIL: ${TEST_NBR}. Collection file not removed"
exit 1
fi
# Test generate TOTP with secret
((TEST_NBR++))
echo "${TEST_NBR}: Testing generate TOTP w/ secret on CLI"
TIME="2019-06-23T20:00:00-05:00"
SECRET=SEED
RESULT=$(${TOTP} --time ${TIME} --secret ${SECRET})
echo "Result: ${RESULT}"
if [[ ! ${RESULT} =~ ^335072$ ]]; then
echo "FAIL: ${TEST_NBR}. Incorrect TOTP generated"
exit 1
fi
# Test add secret
((TEST_NBR++))
ENTRY=entryname
SECRET=SEED
echo "${TEST_NBR}: Testing config update for adding"
${TOTP} config update --file ${COLLECTION} ${ENTRY} ${SECRET}
RESULT=$(${TOTP} config list --all --file ${COLLECTION} | tail -1)
echo "Result: ${RESULT}"
if [[ ! ${RESULT} =~ ^${ENTRY}\ ${SECRET}\ ]]; then
echo "FAIL: ${TEST_NBR}. Entry not added"
exit 1
fi
# Test generate TOTP
((TEST_NBR++))
echo "${TEST_NBR}: Testing generate TOTP"
TIME="2019-06-23T20:00:00-05:00"
RESULT=$(${TOTP} --file ${COLLECTION} --time "${TIME}" ${ENTRY})
echo "Result: ${RESULT}"
if [[ ! ${RESULT} =~ ^335072$ ]]; then
echo "FAIL: ${TEST_NBR}. Incorrect TOTP generated"
exit 1
fi
# Test generate backward TOTP
((TEST_NBR++))
echo "${TEST_NBR}: Testing backward"
TIME="2019-06-23T20:00:00-05:00"
RESULT=$(${TOTP} --file ${COLLECTION} --time "${TIME}" --backward 300s ${ENTRY})
echo "Result: ${RESULT}"
if [[ ! ${RESULT} =~ ^962630$ ]]; then
echo "FAIL: Incorrect backward TOTP generated"
exit 1
fi
# Test generate forward TOTP
((TEST_NBR++))
echo "${TEST_NBR}: Testing forward"
TIME="2019-06-23T20:00:00-05:00"
RESULT=$(${TOTP} --file ${COLLECTION} --time "${TIME}" --forward 300s ${ENTRY})
echo "Result: ${RESULT}"
if [[ ! ${RESULT} =~ ^869438$ ]]; then
echo "FAIL: ${TEST_NBR}. Incorrect forward TOTP generated"
exit 1
fi
# Test stdin config
((TEST_NBR++))
echo "${TEST_NBR}: Testing stdin"
ENTRY=entryname
TIME="2019-06-23T20:00:00-05:00"
RESULT=$(cat ${COLLECTION} | ${TOTP} --stdio --time ${TIME} entryname)
echo "Result: ${RESULT}"
if [[ ! ${RESULT} =~ ^335072$ ]]; then
echo "FAIL: ${TEST_NBR}. Incorrect TOTP generated"
exit 1
fi
# Test update secret
((TEST_NBR++))
ENTRY=entryname
SECRET=SEEDSEED
echo "${TEST_NBR}: Testing config update for updating"
${TOTP} config update --file ${COLLECTION} ${ENTRY} ${SECRET}
RESULT=$(${TOTP} config list --all --file ${COLLECTION} | tail -1)
echo "Result: ${RESULT}"
if [[ ! ${RESULT} =~ ^${ENTRY}\ ${SECRET}\ ]]; then
echo "FAIL: ${TEST_NBR}. Entry not added"
exit 1
fi
# Test rename secret
((TEST_NBR++))
ENTRY=entryname
NEWENTRY=newentryname
SECRET=SEEDSEED
echo "${TEST_NBR}: Testing config rename"
${TOTP} config rename --file ${COLLECTION} ${ENTRY} ${NEWENTRY}
RESULT=$(${TOTP} config list --all --file ${COLLECTION} | tail -1)
echo "Result: ${RESULT}"
if [[ ! ${RESULT} =~ ^${NEWENTRY}\ ${SECRET}\ ]]; then
echo "FAIL: ${TEST_NBR}. Entry not added"
exit 1
fi
# Test delete secret
((TEST_NBR++))
ENTRY=newentryname
echo "${TEST_NBR}: Testing config delete"
${TOTP} config delete --file ${COLLECTION} --yes ${ENTRY}
RESULT=$(${TOTP} config list --file ${COLLECTION} | wc -l)
echo "Result: ${RESULT}"
if [[ ! ${RESULT} =~ ^2 ]]; then
echo "FAIL: ${TEST_NBR}. Entry not deleted"
exit 1
fi
echo "Removing ${COLLECTION}"
${TOTP} config reset --file ${COLLECTION} --yes
echo "Removing ${TOTP}"
rm ${TOTP}
echo Success

View File

@@ -1,11 +0,0 @@
package main
import (
"os"
"github.com/arcanericky/totp/cmd"
)
func main() {
os.Exit(cmd.Execute())
}