Skip to content

Commit

Permalink
Features: Adding keys, removing keys and printing secrets (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
GuardKenzie authored Aug 14, 2023
1 parent 2ce9fbe commit 642039d
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 11 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ Usage
$ gauth Google -b
477615

- Run `gauth KEYNAME -s` to retrieve an accounts secret from the config.

$ gauth Google -s
your_secret_for_google

- `gauth` is convenient to use in `watch`.

$ watch -n1 gauth
Expand All @@ -53,6 +58,22 @@ Usage
from an existing Google Authenticator setup, on a phone to which you do not
have root access), then [gauthQR](https://github.com/jbert/gauthQR) may be useful.


Adding and removing keys
------------------------

- Run `gauth KEYNAME -a` to add a new key.

$ gauth Google -a
Key for Google: examplekey
Current OTP for Google: 306726

- Run `gauth KEYNAME -r` to remove an existing key.

$ gauth Google -r
Are you sure you want to remove Google [y/N]: y
Google has been removed.

Encryption
----------

Expand Down
177 changes: 168 additions & 9 deletions gauth.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bufio"
"fmt"
"log"
"os"
Expand All @@ -17,24 +18,45 @@ import (

func main() {
accountName := ""
isBareCode := false
argument := ""

if len(os.Args) > 1 {
accountName = os.Args[1]
}

if len(os.Args) > 2 {
if os.Args[2] == "-b" || os.Args[2] == "-bare" {
isBareCode = true
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"
}
}

urls := getUrls()

if isBareCode && accountName != "" {
printBareCode(accountName, urls)
} else {
printAllCodes(urls)
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) {
Expand All @@ -43,7 +65,7 @@ func getPassword() ([]byte, error) {
return term.ReadPassword(int(syscall.Stdin))
}

func getUrls() []*otpauth.URL {
func getConfigPath() string {
cfgPath := os.Getenv("GAUTH_CONFIG")
if cfgPath == "" {
user, err := user.Current()
Expand All @@ -53,6 +75,12 @@ func getUrls() []*otpauth.URL {
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)
Expand All @@ -79,6 +107,137 @@ func printBareCode(accountName string, urls []*otpauth.URL) {
}
}

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

Expand Down
75 changes: 73 additions & 2 deletions gauth/gauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,31 @@ func CodesAtTimeStep(u *otpauth.URL, timeStep uint64) (prev, curr, next string,
return
}

// ReadConfigFile reads the config file at path and returns its contents and
// whether it is encrypted or not
func ReadConfigFile(path string) ([]byte, bool, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, false, err
}

if bytes.HasPrefix(data, []byte("Salted__")) {
return data, true, nil // encrypted
}

return data, false, nil
}

// LoadConfigFile reads and decrypts, if necessary, the CSV config at path.
// The getPass function is called to obtain a password if needed.
func LoadConfigFile(path string, getPass func() ([]byte, error)) ([]byte, error) {
data, err := ioutil.ReadFile(path)
data, isEncrypted, err := ReadConfigFile(path)

if err != nil {
return nil, err
}

if !bytes.HasPrefix(data, []byte("Salted__")) {
if !isEncrypted {
return data, nil // not encrypted
}

Expand Down Expand Up @@ -122,6 +138,61 @@ func LoadConfigFile(path string, getPass func() ([]byte, error)) ([]byte, error)
return rest[:len(rest)-pad], nil
}

// WriteConfigFile encrypts the provided newConfig using passwd, if necessary,
// and writes it to path
func WriteConfigFile(path string, passwd []byte, newConfig []byte) error {
data, isEncrypted, err := ReadConfigFile(path)

if err != nil {
return err
}

if isEncrypted {
// Encrypt newConfig using the same salt as in the old config
salt := data[8:16]
salting := sha256.New()
salting.Write(passwd)
salting.Write(salt)
sum := salting.Sum(nil)
key := sum[:16]
iv := sum[16:]

block, err := aes.NewCipher(key)

if err != nil {
return fmt.Errorf("creating cipher: %v", err)
}

mode := cipher.NewCBCEncrypter(block, iv)

// Add needed CBC block padding
padLength := 16 - (len(newConfig) % 16)
pad := make([]byte, padLength)

for i := range pad {
pad[i] = byte(padLength)
}

newConfig = append(newConfig, pad...)

// Encrypt and construct the new data to be written
mode.CryptBlocks(newConfig, newConfig)

saltedPrefix := []byte("Salted__")
saltedPrefix = append(saltedPrefix, salt...)

newConfig = append(saltedPrefix, newConfig...)
}

err = ioutil.WriteFile(path, newConfig, 0)

if err != nil {
return fmt.Errorf("writing config: %v", err)
}

return err
}

// ParseConfig parses the contents of data as a gauth configuration file. Each
// line of the file specifies a single configuration.
//
Expand Down

0 comments on commit 642039d

Please sign in to comment.