2017-01-03 12:14:59 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"os"
|
2017-01-04 20:20:20 +00:00
|
|
|
"sort"
|
2017-01-04 21:44:18 +00:00
|
|
|
"strings"
|
2017-01-03 12:14:59 +00:00
|
|
|
"time"
|
|
|
|
|
2021-02-17 09:58:04 +00:00
|
|
|
"github.com/Luzifer/rconfig/v2"
|
2017-01-03 12:14:59 +00:00
|
|
|
"github.com/hashicorp/vault/api"
|
|
|
|
homedir "github.com/mitchellh/go-homedir"
|
2017-01-04 20:20:20 +00:00
|
|
|
"github.com/sethgrid/curse"
|
2017-01-03 12:14:59 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
cfg = struct {
|
|
|
|
Field string `flag:"field" default:"secret" description:"Field inside the key to search the TOTP secret in"`
|
|
|
|
NoTime bool `flag:"no-time,n" default:"false" description:"Omit printing the validity time"`
|
|
|
|
OneShot bool `flag:"one-shot,1" default:"false" description:"Prints token only once instead continuously"`
|
|
|
|
TestSecret string `flag:"test" default:"" description:"Use a test secret from CLI instead of using a secret from Vault"`
|
|
|
|
VaultAddress string `flag:"vault-addr" env:"VAULT_ADDR" default:"https://127.0.0.1:8200" description:"Vault API address"`
|
|
|
|
VaultToken string `flag:"vault-token" env:"VAULT_TOKEN" vardefault:"vault-token" description:"Vault Token to use for accessing Vault instance"`
|
|
|
|
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
|
|
|
}{}
|
|
|
|
|
|
|
|
version = "dev"
|
|
|
|
)
|
|
|
|
|
|
|
|
func vaultTokenFromDisk() string {
|
|
|
|
vf, err := homedir.Expand("~/.vault-token")
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
data, err := ioutil.ReadFile(vf)
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return string(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
rconfig.SetVariableDefaults(map[string]string{
|
|
|
|
"vault-token": vaultTokenFromDisk(),
|
|
|
|
})
|
|
|
|
|
|
|
|
if err := rconfig.Parse(&cfg); err != nil {
|
|
|
|
log.Fatalf("Unable to parse commandline options: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfg.VersionAndExit {
|
|
|
|
fmt.Printf("vault-totp %s\n", version)
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfg.VaultToken == "" && cfg.TestSecret == "" {
|
|
|
|
log.Fatalf("You need to specify a vault-token")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
if len(rconfig.Args()) < 2 && cfg.TestSecret == "" {
|
|
|
|
log.Fatalf("Please specify a vault key to read the secret from.\n\nUsage: vault-totp [opts] <vault-key>")
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
2017-01-04 20:20:20 +00:00
|
|
|
secrets []token
|
|
|
|
err error
|
2017-01-03 12:14:59 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
if cfg.TestSecret == "" {
|
2017-01-04 20:20:20 +00:00
|
|
|
secrets, err = getSecretsFromVault()
|
2017-01-03 12:14:59 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("Error to retrieve secret: %s", err)
|
|
|
|
}
|
|
|
|
} else {
|
2017-01-04 20:20:20 +00:00
|
|
|
secrets = []token{{Name: "Test", Secret: cfg.TestSecret}}
|
2017-01-03 12:14:59 +00:00
|
|
|
}
|
|
|
|
|
2017-01-04 20:20:20 +00:00
|
|
|
sort.Sort(tokenList(secrets))
|
|
|
|
|
|
|
|
output, err := curse.New()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
firstRun := true
|
2017-01-03 12:14:59 +00:00
|
|
|
for range time.Tick(250 * time.Millisecond) {
|
2017-01-04 20:20:20 +00:00
|
|
|
if !firstRun {
|
|
|
|
output.MoveUp(len(secrets))
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, s := range secrets {
|
|
|
|
output.EraseCurrentLine()
|
2017-01-04 21:44:18 +00:00
|
|
|
o, err := s.BuildOutput(len(secrets) > 1, tokenList(secrets).LongestName())
|
2017-01-04 20:20:20 +00:00
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("%s: ERROR (%s)\n", s.Name, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Println(o)
|
2017-01-03 12:14:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if cfg.OneShot {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2017-01-04 20:20:20 +00:00
|
|
|
firstRun = false
|
|
|
|
}
|
2017-01-03 12:14:59 +00:00
|
|
|
}
|
|
|
|
|
2017-01-04 20:20:20 +00:00
|
|
|
func getSecretsFromVault() ([]token, error) {
|
2017-01-03 12:14:59 +00:00
|
|
|
client, err := api.NewClient(&api.Config{
|
|
|
|
Address: cfg.VaultAddress,
|
|
|
|
})
|
|
|
|
if err != nil {
|
2017-01-04 20:20:20 +00:00
|
|
|
return nil, fmt.Errorf("Unable to create client: %s", err)
|
2017-01-03 12:14:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
client.SetToken(cfg.VaultToken)
|
|
|
|
|
2017-01-04 21:44:18 +00:00
|
|
|
resp := []token{}
|
|
|
|
keyPool := []string{}
|
|
|
|
|
|
|
|
key := rconfig.Args()[1]
|
|
|
|
|
|
|
|
if strings.HasSuffix(key, "*") {
|
|
|
|
scanPool := []string{strings.TrimRight(key, "*")}
|
|
|
|
|
|
|
|
for len(scanPool) > 0 {
|
|
|
|
s, err := client.Logical().List(scanPool[0])
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Unable to list keys %q: %s", key, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if s == nil {
|
|
|
|
return nil, fmt.Errorf("There is no key %q", scanPool[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
if s.Data["keys"] != nil {
|
|
|
|
for _, sk := range s.Data["keys"].([]interface{}) {
|
|
|
|
sks := sk.(string)
|
|
|
|
if strings.HasSuffix(sks, "/") {
|
|
|
|
scanPool = append(scanPool, scanPool[0]+sks)
|
|
|
|
} else {
|
|
|
|
keyPool = append(keyPool, scanPool[0]+sks)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-04 21:48:50 +00:00
|
|
|
scanPool = scanPool[1:]
|
2017-01-04 21:44:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
keyPool = append(keyPool, key)
|
2017-01-03 12:14:59 +00:00
|
|
|
}
|
|
|
|
|
2017-01-04 21:44:18 +00:00
|
|
|
for _, k := range keyPool {
|
|
|
|
data, err := client.Logical().Read(k)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Unable to read from key %q: %s", k, err)
|
|
|
|
}
|
|
|
|
|
2021-02-17 09:53:06 +00:00
|
|
|
if data == nil {
|
|
|
|
return nil, fmt.Errorf("Key %q not found", k)
|
|
|
|
}
|
|
|
|
|
2017-01-04 21:44:18 +00:00
|
|
|
if data.Data[cfg.Field] == nil {
|
|
|
|
return nil, fmt.Errorf("The key %q does not have a field named %q.", k, cfg.Field)
|
|
|
|
}
|
|
|
|
|
2017-01-04 21:47:23 +00:00
|
|
|
name := k
|
|
|
|
if data.Data["name"] != nil {
|
|
|
|
name = data.Data["name"].(string)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp = append(resp, token{Name: name, Secret: data.Data[cfg.Field].(string)})
|
2017-01-03 12:14:59 +00:00
|
|
|
}
|
|
|
|
|
2017-01-04 21:44:18 +00:00
|
|
|
return resp, nil
|
2017-01-03 12:14:59 +00:00
|
|
|
}
|