Skip to content

Commit

Permalink
Add a time machine (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
arcanericky authored Jun 7, 2019
1 parent 2ab44c2 commit 3491778
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 41 deletions.
51 changes: 43 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ Every copy of your two-factor credentials increases your risk profile. Using thi
**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**.

```
$ totp config add google seed
$ 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.

```
$ totp google
$ totp mysecretname
```

**List the secret entries** with the `config list` command.
Expand All @@ -36,22 +36,22 @@ $ totp config list
**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.

```
$ totp config update google newseed
$ totp config update mysecretname newseed
```

**Rename the secret entries** with the `config rename` command

```
$ totp config rename google google-main
$ totp config rename mysecretname mynewname
```

**Delete secret entries** with the `config delete` command

```
$ totp config delete google-main
$ totp config delete mynewname
```

**Remove all the secrets** and start over, use the `config reset` command
**Remove all the secrets** and start over using the `config reset` command

```
$ totp config reset
Expand All @@ -70,11 +70,40 @@ $ totp --help
$ totp config --help
```

**Bash completion** can be enabled by using `config completion`
**Bash completion** can be enabled by using `config completion`.

```
$ . <(totp config completion)
```

## 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.

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.

Examples with `--time`:

```
$ date '+%FT%T%:z'
2019-06-01T19:58:47-05:00
$ totp --time $(date '+%FT%T%:z') --secret seed
931665
$ totp --time 2019-06-01T20:00:00-05:00 --secret seed
526171
```

The `--forward` and `--backward` options move the current time forward and backward by their duration formatted arguments. See [Go's `time.ParseDuration()`](https://golang.org/pkg/time/#ParseDuration) documentation for more details on this format.

Examples with `--forward` and `--backward`

```
$ totp --time 2019-06-01T20:00:00-05:00 --backward 3m --secret seed
222296
$ totp --time 2019-06-01T20:00:00-05:00 --forward 30s --secret seed
820148
```

## Using the Sdio Option

If storing secrets in the clear isn't ideal for you, `totp` supports streaming the shared secret collection through stdin and stdout with the `--stdio` option. This allows you to roll your own encryption or support other methods of maintaining shared secrets.
Expand Down Expand Up @@ -160,6 +189,12 @@ For unit tests and code coverage reports:
$ make test
```

The coverage is output to `coverage.html`. Load it in browser for review. For example:

```
$ /opt/google/chrome/chrome file://$PWD/coverage.html
```

To build for a single platform (see the `Makefile` for the different targets)

```
Expand All @@ -176,7 +211,7 @@ Unit tests for new code are required. Use `make test` to verify coverage. Covera

## Inspiration

My [ga-cmd project](https://github.com/arcanericky/ga-cmd) is more popular than I expected. It's basically the same as `totp` with a much smaller executable, but the list of secrets must be edited manually. This `totp` project allows the user to maintain the secret collection through the `totp` command line interface, run on a variety of operating systems, and gives me a platform to practice my Go coding.
My [ga-cmd project](https://github.com/arcanericky/ga-cmd) is more popular than I expected. It's basically the same as `totp` with a much smaller executable, but the list of secrets must be edited manually and there aren't as many command line options. This `totp` project allows the user to maintain the secret collection through the `totp` command line interface, run on a variety of operating systems, and gives me a platform to practice my Go coding.

## Credits

Expand Down
113 changes: 80 additions & 33 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ import (
"github.com/spf13/cobra"
)

const optionFile = "file"
const optionSecret = "secret"
const optionStdio = "stdio"
const optionTime = "time"
const (
optionFile = "file"
optionSecret = "secret"
optionStdio = "stdio"
optionTime = "time"
optionBackward = "backward"
optionForward = "forward"
)

var rootCmd = &cobra.Command{
Use: "totp",
Expand Down Expand Up @@ -53,69 +57,107 @@ var rootCmd = &cobra.Command{
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
}

codeTime := time.Now()
// Default to time of now
codeTime := time.Now().Add(-backward)
codeTime = codeTime.Add(forward)

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

codeTime = codeTime.Add(-backward)
codeTime = codeTime.Add(forward)
}

// Providing a secret name overrides the --secret option but
// should probably generate an error if both are given
if len(secret) != 0 {
generateCodeWithSecret(secret, codeTime)
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
}

// If no secret or no secret name given, show help
if len(args) != 1 {
fmt.Fprintf(os.Stderr, "Need the name of a secret to generate a code.\n\n")
// 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
}

// If here then a stored shared secret is wanted
generateCode(args[0], codeTime)
},
}
// 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()

func generateCodeWithSecret(secret string, t time.Time) {
code, err := totp.GenerateCode(secret, t)
return
}

if err != nil {
fmt.Fprintln(os.Stderr, "Error generating code", err)
} else {
fmt.Println(code)
}
// Load the secret name
secretName := ""
if argsLen == 1 {
secretName = args[0]
}

// If here then a stored shared secret is wanted
generateCode(secretName, secret, codeTime)
},
}

func generateCode(name string, t time.Time) {
var s *api.Collection
func generateCode(name string, secret string, t time.Time) error {
var code string
var err error
var c *api.Collection

s, err = collectionFile.loader()

if err != nil {
fmt.Fprintln(os.Stderr, "Error loading collection", err)
if len(secret) != 0 {
code, err = totp.GenerateCode(secret, t)
} else {
code, err := s.GenerateCodeWithTime(name, t)
c, err = collectionFile.loader()

if err != nil {
fmt.Fprintln(os.Stderr, "Error generating code", err)
fmt.Fprintln(os.Stderr, "Error loading collection", err)
} else {
fmt.Println(code)
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
}

// Execute adds all child commands to the root command and sets flags appropriately.
Expand All @@ -132,10 +174,15 @@ func Execute() int {
}

func init() {
var duration time.Duration

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 (2006-01-02T15:04:05Z07:00)")
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.SetUsageTemplate(strings.Replace(rootCmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}}\n {{.CommandPath}} [secret name]", 1))
}
25 changes: 25 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ func TestRoot(t *testing.T) {
os.Remove(collectionFile.filename)
rootCmd.Run(rootCmd, []string{secretList[0].name})

// Excessive args
rootCmd.Run(rootCmd, []string{"secretname", "extraarg"})

// Provide secret option
rootCmd.Flags().Set(optionSecret, "seed")
rootCmd.Run(rootCmd, []string{})
Expand All @@ -56,6 +59,14 @@ func TestRoot(t *testing.T) {
rootCmd.Flags().Set(optionStdio, "true")
rootCmd.PersistentPreRun(rootCmd, []string{"secret"})

// 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"})

// Invalid time option
rootCmd.Flags().Set(optionTime, "invalidtime")
rootCmd.Run(rootCmd, []string{})
Expand Down Expand Up @@ -85,6 +96,20 @@ func TestRoot(t *testing.T) {
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
Expand Down

0 comments on commit 3491778

Please sign in to comment.