256 lines
5.7 KiB
Go
256 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"text/tabwriter"
|
|
|
|
"github.com/creachadair/otp/otpauth"
|
|
"github.com/pcarrier/gauth/gauth"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
func main() {
|
|
accountName := ""
|
|
argument := ""
|
|
|
|
if len(os.Args) > 1 {
|
|
accountName = os.Args[1]
|
|
}
|
|
|
|
if len(os.Args) > 2 {
|
|
if os.Args[2] == "-b" || os.Args[2] == "-bare" {
|
|
argument = "bare"
|
|
} else if os.Args[2] == "-a" || os.Args[2] == "-add" {
|
|
argument = "add"
|
|
} else if os.Args[2] == "-r" || os.Args[2] == "-remove" {
|
|
argument = "remove"
|
|
} else if os.Args[2] == "-s" || os.Args[2] == "-secret" {
|
|
argument = "secret"
|
|
}
|
|
}
|
|
|
|
if accountName != "" {
|
|
switch argument {
|
|
case "bare":
|
|
printBareCode(accountName, getUrls())
|
|
return
|
|
case "add":
|
|
addCode(accountName)
|
|
return
|
|
case "remove":
|
|
removeCode(accountName)
|
|
return
|
|
case "secret":
|
|
printSecret(accountName, getUrls())
|
|
return
|
|
default:
|
|
printAllCodes(getUrls())
|
|
return
|
|
}
|
|
}
|
|
|
|
printAllCodes(getUrls())
|
|
}
|
|
|
|
func getPassword() ([]byte, error) {
|
|
fmt.Printf("Encryption password: ")
|
|
defer fmt.Println()
|
|
return term.ReadPassword(int(syscall.Stdin))
|
|
}
|
|
|
|
func getConfigPath() string {
|
|
cfgPath := os.Getenv("GAUTH_CONFIG")
|
|
if cfgPath == "" {
|
|
user, err := user.Current()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
cfgPath = filepath.Join(user.HomeDir, ".config", "gauth.csv")
|
|
}
|
|
|
|
return cfgPath
|
|
}
|
|
|
|
func getUrls() []*otpauth.URL {
|
|
cfgPath := getConfigPath()
|
|
|
|
cfgContent, err := gauth.LoadConfigFile(cfgPath, getPassword)
|
|
if err != nil {
|
|
log.Fatalf("Loading config: %v", err)
|
|
}
|
|
|
|
urls, err := gauth.ParseConfig(cfgContent)
|
|
if err != nil {
|
|
log.Fatalf("Decoding configuration file: %v", err)
|
|
}
|
|
|
|
return urls
|
|
}
|
|
|
|
func printBareCode(accountName string, urls []*otpauth.URL) {
|
|
for _, url := range urls {
|
|
if strings.EqualFold(strings.ToLower(accountName), strings.ToLower(url.Account)) {
|
|
_, curr, _, err := gauth.Codes(url)
|
|
if err != nil {
|
|
log.Fatalf("Generating codes for %q: %v", url.Account, err)
|
|
}
|
|
fmt.Print(curr)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func addCode(accountName string) {
|
|
cfgPath := getConfigPath()
|
|
|
|
// Check for encryption and ask for password if necessary
|
|
_, isEncrypted, err := gauth.ReadConfigFile(cfgPath)
|
|
|
|
password, err := []byte(nil), nil
|
|
|
|
if isEncrypted {
|
|
password, err = getPassword()
|
|
|
|
if err != nil {
|
|
log.Fatalf("reading passphrase: %v", err)
|
|
}
|
|
}
|
|
|
|
// Get decoded config
|
|
rawConfig, err := gauth.LoadConfigFile(cfgPath, func() ([]byte, error) { return password, err })
|
|
if err != nil {
|
|
log.Fatalf("Loading config: %v", err)
|
|
}
|
|
|
|
newConfig := strings.TrimSuffix(string(rawConfig), "\n")
|
|
|
|
// Check if account already exists
|
|
for _, line := range strings.Split(newConfig, "\n") {
|
|
if strings.HasPrefix(strings.ToLower(line), strings.ToLower(accountName)) {
|
|
fmt.Printf("Account \"%s\" already exists. Nothing has been added.", accountName)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Read new key
|
|
fmt.Printf("Key for %s: ", accountName)
|
|
reader := bufio.NewReader(os.Stdin)
|
|
key, _ := reader.ReadString('\n')
|
|
|
|
// Append new key
|
|
newConfig += "\n" + accountName + ":" + key + "\n"
|
|
|
|
// Try parsing the new config and print the current OTP
|
|
parsedConfig, err := gauth.ParseConfig([]byte(newConfig))
|
|
if err != nil {
|
|
log.Fatalf("Parsing new config: %v", err)
|
|
}
|
|
|
|
fmt.Printf("Current OTP for %s: ", accountName)
|
|
printBareCode(accountName, parsedConfig)
|
|
|
|
// write new config
|
|
err = gauth.WriteConfigFile(cfgPath, password, []byte(newConfig))
|
|
if err != nil {
|
|
log.Fatalf("Error writing new config: %v", err)
|
|
}
|
|
}
|
|
|
|
func removeCode(accountName string) {
|
|
cfgPath := getConfigPath()
|
|
|
|
// Check for encryption and ask for password if necessary
|
|
_, isEncrypted, err := gauth.ReadConfigFile(cfgPath)
|
|
|
|
password, err := []byte(nil), nil
|
|
|
|
if isEncrypted {
|
|
password, err = getPassword()
|
|
|
|
if err != nil {
|
|
log.Fatalf("reading passphrase: %v", err)
|
|
}
|
|
}
|
|
|
|
// Get decoded config
|
|
rawConfig, err := gauth.LoadConfigFile(cfgPath, func() ([]byte, error) { return password, err })
|
|
if err != nil {
|
|
log.Fatalf("Loading config: %v", err)
|
|
}
|
|
|
|
newConfig := ""
|
|
anythingRemoved := false
|
|
|
|
// Iterate over config lines and search for the one to be removed
|
|
for _, line := range strings.Split(string(rawConfig), "\n") {
|
|
trim := strings.TrimSpace(line)
|
|
if trim == "" {
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(strings.ToLower(trim), strings.ToLower(accountName)) {
|
|
anythingRemoved = true
|
|
continue
|
|
}
|
|
|
|
newConfig += trim + "\n"
|
|
|
|
}
|
|
|
|
if !anythingRemoved {
|
|
fmt.Printf("Account \"%s\" was not found. Nothing has been removed.", accountName)
|
|
return
|
|
}
|
|
|
|
// Prompt for confirmation
|
|
fmt.Printf("Are you sure you want to remove %s [y/N]: ", accountName)
|
|
reader := bufio.NewReader(os.Stdin)
|
|
confirmation, _ := reader.ReadString('\n')
|
|
|
|
confirmation = strings.TrimSpace(confirmation)
|
|
|
|
if strings.ToLower(confirmation) != "y" {
|
|
return
|
|
}
|
|
|
|
// Write the new config
|
|
err = gauth.WriteConfigFile(cfgPath, password, []byte(newConfig))
|
|
if err != nil {
|
|
log.Fatalf("Error writing new config: %v", err)
|
|
}
|
|
|
|
fmt.Printf("%s has been removed.", accountName)
|
|
}
|
|
|
|
func printSecret(accountName string, urls []*otpauth.URL) {
|
|
for _, url := range urls {
|
|
if strings.EqualFold(strings.ToLower(accountName), strings.ToLower(url.Account)) {
|
|
fmt.Print(url.RawSecret)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func printAllCodes(urls []*otpauth.URL) {
|
|
_, progress := gauth.IndexNow() // TODO: do this per-code
|
|
|
|
tw := tabwriter.NewWriter(os.Stdout, 0, 8, 1, ' ', 0)
|
|
fmt.Fprintln(tw, "\tprev\tcurr\tnext")
|
|
for _, url := range urls {
|
|
prev, curr, next, err := gauth.Codes(url)
|
|
if err != nil {
|
|
log.Fatalf("Generating codes for %q: %v", url.Account, err)
|
|
}
|
|
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", url.Account, prev, curr, next)
|
|
}
|
|
tw.Flush()
|
|
fmt.Printf("[%-29s]\n", strings.Repeat("=", progress))
|
|
}
|