Skip to content

Commit

Permalink
Add support for stdio (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
arcanericky authored Jun 5, 2019
1 parent bf57cae commit 2ab44c2
Show file tree
Hide file tree
Showing 22 changed files with 275 additions and 75 deletions.
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,72 @@ $ totp config --help
```
$ . <(totp config completion)
```
## 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.

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**

Note the `--file` option can achieve the same results as this example. This is meant to teach how stdio works with `totp`.

Create a collection

```
totp config add --stdio secretname myvalue < /dev/null > totp.json
```

View the collection

```
totp config list --stdio < totp.json
```

Generate a TOTP code

```
totp secretname --stdio < totp.json
```

**Encrypting Shared Secret Collection**

Using what was learned above, a contrived example for encrypting data with [GnuPG](https://gnupg.org/) follows.

Create an encrypted collection
```
totp config add --stdio secretname myvalue < /dev/null | \
gpg --batch --yes --passphrase mypassphrase --output totp-collection.gpg --symmetric
```

View the collection

```
gpg --quiet --batch --passphrase mypassphrase --decrypt totp-collection.gpg | \
totp config list --stdio
```

Add another secret

```
gpg --quiet --batch --passphrase mypassphrase --decrypt totp-collection.gpg | \
totp config add --stdio newname newvalue | \
gpg --batch --yes --passphrase mypassphrase --output totp-collection.gpg --symmetric
```

View the modified collection

```
gpg --quiet --batch --passphrase mypassphrase --decrypt totp-collection.gpg | \
totp config list --stdio
```

Generate a TOTP code

```
gpg --quiet --batch --passphrase mypassphrase --decrypt totp-collection.gpg | totp --stdio secretname
```

## Building

Expand Down Expand Up @@ -111,3 +177,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.

## Credits

This utility uses the [otp package by pquerna](https://github.com/pquerna/otp). Without this library, I probably woudn't have bothered creating this.
2 changes: 1 addition & 1 deletion cmd/collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func createTestData(t *testing.T) []secretItem {
t.Helper()

// Create test data
c, _ := totp.NewCollectionWithFile(defaultCollectionFile)
c, _ := totp.NewCollectionWithFile(collectionFile.filename)

// Create some test data
secretList := []secretItem{
Expand Down
4 changes: 2 additions & 2 deletions cmd/configdelete.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"os"

"github.com/arcanericky/totp"
"github.com/spf13/cobra"
)

Expand All @@ -24,7 +23,7 @@ var configDeleteCmd = &cobra.Command{
}

func deleteSecret(name string) {
s, err := totp.NewCollectionWithFile(defaultCollectionFile)
s, err := collectionFile.loader()
if err != nil {
fmt.Fprintln(os.Stderr, "Error loading settings", err)
} else {
Expand All @@ -41,4 +40,5 @@ func deleteSecret(name string) {

func init() {
configCmd.AddCommand(configDeleteCmd)
configDeleteCmd.Flags().BoolP(optionStdio, "", false, "load with stdin, save with stdout")
}
6 changes: 3 additions & 3 deletions cmd/configdelete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

func TestConfigDelete(t *testing.T) {
defaultCollectionFile = "testcollection"
collectionFile.filename = "testcollection"

secretList := createTestData(t)

Expand All @@ -20,7 +20,7 @@ func TestConfigDelete(t *testing.T) {

// Successful delete
configDeleteCmd.Run(nil, []string{secretList[3].name})
c, err := totp.NewCollectionWithFile(defaultCollectionFile)
c, err := totp.NewCollectionWithFile(collectionFile.filename)
if err != nil {
t.Error("Could not load collection for delete test from file")
}
Expand All @@ -31,6 +31,6 @@ func TestConfigDelete(t *testing.T) {
}

// No collections file
os.Remove(defaultCollectionFile)
os.Remove(collectionFile.filename)
deleteSecret(secretList[3].name)
}
3 changes: 2 additions & 1 deletion cmd/configlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func listAllInfo(secrets []totp.Secret) {
}

func listSecrets(cmd *cobra.Command) {
c, err := totp.NewCollectionWithFile(defaultCollectionFile)
c, err := collectionFile.loader()
if err != nil {
fmt.Fprintln(os.Stderr, "Error loading collection", err)
} else {
Expand All @@ -93,4 +93,5 @@ func listSecrets(cmd *cobra.Command) {
func init() {
configCmd.AddCommand(configListCmd)
configListCmd.Flags().BoolP("names", "n", false, "list only secret names")
configListCmd.Flags().BoolP(optionStdio, "", false, "load data from stdin")
}
4 changes: 2 additions & 2 deletions cmd/configlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
)

func TestConfigList(t *testing.T) {
defaultCollectionFile = "testcollection"
collectionFile.filename = "testcollection"

createTestData(t)

Expand All @@ -20,6 +20,6 @@ func TestConfigList(t *testing.T) {
configListCmd.Run(configListCmd, []string{})

// No collections file
os.Remove(defaultCollectionFile)
os.Remove(collectionFile.filename)
configListCmd.Run(configListCmd, []string{})
}
4 changes: 2 additions & 2 deletions cmd/configrename.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"os"
"strings"

"github.com/arcanericky/totp"
"github.com/spf13/cobra"
)

Expand All @@ -30,7 +29,7 @@ func renameSecret(source, target string) {
return
}

s, _ := totp.NewCollectionWithFile(defaultCollectionFile)
s, _ := collectionFile.loader()
_, err := s.RenameSecret(source, target)

if err != nil {
Expand All @@ -43,5 +42,6 @@ func renameSecret(source, target string) {

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))
}
8 changes: 4 additions & 4 deletions cmd/configrename_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

func TestConfigRename(t *testing.T) {
defaultCollectionFile = "testcollection"
collectionFile.filename = "testcollection"

secrets := createTestData(t)

Expand All @@ -17,7 +17,7 @@ func TestConfigRename(t *testing.T) {
// Valid parameters
newName := "newName"
configRenameCmd.Run(nil, []string{secrets[0].name, newName})
c, err := totp.NewCollectionWithFile(defaultCollectionFile)
c, err := totp.NewCollectionWithFile(collectionFile.filename)
if err != nil {
t.Error("Could not load collection for rename test from file")
}
Expand All @@ -29,7 +29,7 @@ func TestConfigRename(t *testing.T) {

// Test rename to config
configRenameCmd.Run(nil, []string{newName, configCmd.Use})
c, err = totp.NewCollectionWithFile(defaultCollectionFile)
c, err = totp.NewCollectionWithFile(collectionFile.filename)
if err != nil {
t.Error("Could not load collection for rename test from file")
}
Expand All @@ -40,6 +40,6 @@ func TestConfigRename(t *testing.T) {
}

// No collections file
os.Remove(defaultCollectionFile)
os.Remove(collectionFile.filename)
configRenameCmd.Run(nil, []string{secrets[0].name, "newname"})
}
2 changes: 1 addition & 1 deletion cmd/configreset.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ var configResetCmd = &cobra.Command{
}

func configReset() {
os.Remove(defaultCollectionFile)
os.Remove(collectionFile.filename)
fmt.Println("Collection file removed")
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/configreset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import (
)

func TestConfigReset(t *testing.T) {
defaultCollectionFile = "testcollection"
collectionFile.filename = "testcollection"

createTestData(t)

configResetCmd.Run(nil, []string{})

_, err := os.Stat(defaultCollectionFile)
_, err := os.Stat(collectionFile.filename)
if !os.IsNotExist(err) {
t.Error("Failed to remove the collection file")
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/configupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"os"
"strings"

"github.com/arcanericky/totp"
"github.com/spf13/cobra"
)

Expand All @@ -30,7 +29,7 @@ func updateSecret(name, value string) {
return
}

s, _ := totp.NewCollectionWithFile(defaultCollectionFile)
s, _ := collectionFile.loader()
secret, err := s.UpdateSecret(name, value)

if err != nil {
Expand All @@ -49,5 +48,6 @@ func updateSecret(name, value string) {

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))
}
10 changes: 5 additions & 5 deletions cmd/configupdate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import (
)

func TestConfigUpdate(t *testing.T) {
defaultCollectionFile = "testcollection"
collectionFile.filename = "testcollection"

createTestData(t)

// Valid parameters
secretName := "testsecret"
configUpdateCmd.Run(nil, []string{secretName, "seed"})
c, err := totp.NewCollectionWithFile(defaultCollectionFile)
c, err := totp.NewCollectionWithFile(collectionFile.filename)
if err != nil {
t.Error("Could not load collection for update test from file")
}
Expand All @@ -28,7 +28,7 @@ func TestConfigUpdate(t *testing.T) {
// Test update secret
newSecret := "seedseed"
configUpdateCmd.Run(nil, []string{secretName, newSecret})
c, err = totp.NewCollectionWithFile(defaultCollectionFile)
c, err = totp.NewCollectionWithFile(collectionFile.filename)
if err != nil {
t.Error("Could not load collection for update test from file")
}
Expand All @@ -41,7 +41,7 @@ func TestConfigUpdate(t *testing.T) {
// Test using secret named 'config'
secretName = configCmd.Use
configUpdateCmd.Run(nil, []string{secretName, "seed"})
c, err = totp.NewCollectionWithFile(defaultCollectionFile)
c, err = totp.NewCollectionWithFile(collectionFile.filename)
if err != nil {
t.Error("Could not load collection for update test from file")
}
Expand All @@ -58,6 +58,6 @@ func TestConfigUpdate(t *testing.T) {
configUpdateCmd.Run(nil, []string{"testsecret", "seed1"})

// No collections file
os.Remove(defaultCollectionFile)
os.Remove(collectionFile.filename)
configUpdateCmd.Run(nil, []string{"testsecret", "seed"})
}
37 changes: 28 additions & 9 deletions cmd/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,37 @@ import (
"os"
"path/filepath"
"runtime"

"github.com/arcanericky/totp"
)

const defaultBaseCollectionFile = "totp-config.json"

var defaultCollectionFile string
var collectionFile struct {
filename string
useStdio bool
loader func() (*totp.Collection, error)
}

func loadCollectionFromStdin() (*totp.Collection, error) {
c, err := totp.NewCollectionWithReader(os.Stdin)
c.SetWriter(os.Stdout)

return c, err
}

func loadCollectionFromDefaultFile() (*totp.Collection, error) {
return totp.NewCollectionWithFile(collectionFile.filename)
}

func setCollectionFile(goos string) {
if goos == "windows" {
collectionFile.filename = filepath.Join(os.Getenv("LOCALAPPDATA"), defaultBaseCollectionFile)
} else {
collectionFile.filename = filepath.Join(os.Getenv("HOME"), "."+defaultBaseCollectionFile)
}
}

var reservedCommands = []string{configCmd.Use, versionCmd.Use}

func isReservedCommand(name string) bool {
Expand All @@ -21,14 +47,7 @@ func isReservedCommand(name string) bool {
return false
}

func setCollectionFile(goos string) {
if goos == "windows" {
defaultCollectionFile = filepath.Join(os.Getenv("LOCALAPPDATA"), defaultBaseCollectionFile)
} else {
defaultCollectionFile = filepath.Join(os.Getenv("HOME"), "."+defaultBaseCollectionFile)
}
}

func init() {
setCollectionFile(runtime.GOOS)
collectionFile.loader = loadCollectionFromDefaultFile
}
7 changes: 5 additions & 2 deletions cmd/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ import (
func TestDefaults(t *testing.T) {
// Doesn't set and check exactly under on Linux but good enough for test
setCollectionFile("windows")
if defaultCollectionFile != filepath.Join(os.Getenv("LOCALAPPDATA"), defaultBaseCollectionFile) {
if collectionFile.filename != filepath.Join(os.Getenv("LOCALAPPDATA"), defaultBaseCollectionFile) {
t.Error("Windows collection file not set properly")
}

setCollectionFile(runtime.GOOS)
if defaultCollectionFile != filepath.Join(os.Getenv("HOME"), "."+defaultBaseCollectionFile) {
if collectionFile.filename != filepath.Join(os.Getenv("HOME"), "."+defaultBaseCollectionFile) {
t.Error("Runtime OS collection file not set properly")
}

// Not sure how to unit test but at least run it for now
loadCollectionFromStdin()

for _, c := range reservedCommands {
if isReservedCommand(c) != true {
t.Error("Error checking valid reserved commands")
Expand Down
Loading

0 comments on commit 2ab44c2

Please sign in to comment.