Skip to content

Commit

Permalink
Config: Add -edit and encryption upgrade to cmd/config
Browse files Browse the repository at this point in the history
This simplifies the handling for encryption prompts by moving it to a
field on config, allowing us to simplify all the places were were
passing around config

Also moves password entry to being secure (echo-off)
  • Loading branch information
gbjk committed Nov 26, 2024
1 parent fba6178 commit 3496c91
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 248 deletions.
141 changes: 87 additions & 54 deletions cmd/config/config.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package main

import (
"errors"
"flag"
"fmt"
"os"
"slices"
"strings"

"github.com/buger/jsonparser"
"github.com/thrasher-corp/gocryptotrader/common/file"
"github.com/thrasher-corp/gocryptotrader/config"
)
Expand All @@ -18,73 +20,110 @@ func main() {

defaultCfgFile := config.DefaultFilePath()

var in, out, key string
var in, out, keyStr string
var inplace bool

fs := flag.NewFlagSet("config", flag.ExitOnError)
fs.Usage = func() { usage(fs) }
fs.StringVar(&in, "infile", defaultCfgFile, "The config input file to process")
fs.StringVar(&out, "outfile", "[infile].out", "The config output file")
fs.StringVar(&key, "key", "", "The key to use for AES encryption")
_ = fs.Parse(os.Args[1:])
fs.StringVar(&in, "in", defaultCfgFile, "The config input file to process")
fs.StringVar(&out, "out", "[in].out", "The config output file")
fs.BoolVar(&inplace, "edit", false, "Edit; Save result to the original file")
fs.StringVar(&keyStr, "key", "", "The key to use for AES encryption")

cmd, args := parseCommand(os.Args[1:])
if cmd == "" {
usage(fs)
os.Exit(2)
}

if out == "[infile].out" {
if err := fs.Parse(args); err != nil {
fatal(err.Error())
}

if inplace {
out = in
} else if out == "[in].out" {
out = in + ".out"
}

switch parseCommand(fs) {
key := []byte(keyStr)
var err error
switch cmd {
case "upgrade":
upgradeFile(in, out)
err = upgradeFile(in, out, key)
case "decrypt":
encryptWrapper(in, out, key, decryptFile)
err = encryptWrapper(in, out, key, false, decryptFile)
case "encrypt":
encryptWrapper(in, out, key, encryptFile)
err = encryptWrapper(in, out, key, true, encryptFile)
}

if err != nil {
fatal(err.Error())
}

fmt.Println("Success! File written to " + out)
}

func upgradeFile(in, out string) {
if config.IsFileEncrypted(in) {
fatal("Cannot upgrade an encrypted file. Please decrypt first")
func upgradeFile(in, out string, key []byte) error {
c := &config.Config{
EncryptionKeyProvider: func(_ bool) ([]byte, error) {
if len(key) != 0 {
return key, nil
}
return config.PromptForConfigKey(false)
},
}
c := &config.Config{}

if err := c.ReadConfigFromFile(in, true); err != nil {
fatal(err.Error())
}
if err := c.SaveConfigToFile(out); err != nil {
fatal(err.Error())
return err
}

return c.SaveConfigToFile(out)
}

func encryptWrapper(in, out, key string, fn func(in string, key []byte) []byte) {
if key == "" {
key = getKey()
type encryptFunc func(string, []byte) ([]byte, error)

func encryptWrapper(in, out string, key []byte, confirmKey bool, fn encryptFunc) error {
if len(key) == 0 {
var err error
if key, err = config.PromptForConfigKey(confirmKey); err != nil {
return err
}
}
outData, err := fn(in, key)
if err != nil {
return err
}
outData := fn(in, []byte(key))
if err := file.Write(out, outData); err != nil {
fatal("Unable to write output file " + out + "; Error: " + err.Error())
return fmt.Errorf("unable to write output file %s; Error: %w", out, err)
}
return nil
}

func encryptFile(in string, key []byte) []byte {
func encryptFile(in string, key []byte) ([]byte, error) {
if config.IsFileEncrypted(in) {
fatal("File is already encrypted")
return nil, errors.New("file is already encrypted")
}
outData, err := config.EncryptConfigFile(readFile(in), key)
if err != nil {
fatal("Unable to encrypt config data. Error: " + err.Error())
return nil, fmt.Errorf("unable to encrypt config data. Error: %w", err)
}
return outData
return outData, nil
}

func decryptFile(in string, key []byte) []byte {
func decryptFile(in string, key []byte) ([]byte, error) {
if !config.IsFileEncrypted(in) {
fatal("File is already decrypted")
return nil, errors.New("file is already decrypted")
}
outData, err := config.DecryptConfigFile(readFile(in), key)
if err != nil {
fatal("Unable to decrypt config data. Error: " + err.Error())
return nil, fmt.Errorf("unable to decrypt config data. Error: %w", err)
}
if json, err := jsonparser.Set(outData, []byte("-1"), "encryptConfig"); err == nil {
// err probably means it didn't decrypt, but we don't tell the user that for security
outData = json
}
return outData
return outData, nil
}

func readFile(in string) []byte {
Expand All @@ -95,45 +134,39 @@ func readFile(in string) []byte {
return fileData
}

func getKey() string {
result, err := config.PromptForConfigKey(false)
if err != nil {
fatal("Unable to obtain encryption/decryption key: " + err.Error())
}
return string(result)
}

func fatal(msg string) {
fmt.Fprintln(os.Stderr, msg)
os.Exit(2)
}

// parseCommand will return the single non-flag parameter
// If none is provided, too many, or unrecognised, usage() will be called and exit 1
func parseCommand(fs *flag.FlagSet) string {
switch fs.NArg() {
// parseCommand will return the single non-flag parameter from os.Args, and return the remaining args
// If none is provided, too many, usage() will be called and exit 1
func parseCommand(a []string) (cmd string, args []string) {
cmds, rem := []string{}, []string{}
for _, s := range a {
if slices.Contains(commands, s) {
cmds = append(cmds, s)
} else {
rem = append(rem, s)
}
}
switch len(cmds) {
case 0:
fmt.Fprintln(os.Stderr, "No command provided")
case 1:
command := fs.Arg(0)
if slices.Contains(commands, command) {
return command
}
fmt.Fprintln(os.Stderr, "Unknown command provided: "+command)
case 1: //
return cmds[0], rem
default:
fmt.Fprintln(os.Stderr, "Too many commands provided: "+strings.Join(fs.Args(), " "))
fmt.Fprintln(os.Stderr, "Too many commands provided: "+strings.Join(cmds, ", "))
}
usage(fs)
os.Exit(2)
return ""
return "", nil
}

// usage prints command usage and exits 1
func usage(fs *flag.FlagSet) {
//nolint:dupword // deliberate duplication of commands
fmt.Fprintln(os.Stderr, `
Usage:
config <command> [arguments]
config [arguments] <command>
The commands are:
encrypt encrypt infile and write to outfile
Expand Down
37 changes: 19 additions & 18 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1486,7 +1486,7 @@ func (c *Config) ReadConfigFromFile(path string, dryrun bool) error {
}
defer f.Close()

if err := c.readConfig(f, func() ([]byte, error) { return PromptForConfigKey(false) }); err != nil {
if err := c.readConfig(f); err != nil {
return err
}

Expand All @@ -1497,19 +1497,17 @@ func (c *Config) ReadConfigFromFile(path string, dryrun bool) error {
return c.saveWithEncryptPrompt(path)
}

type keyProvider func() ([]byte, error)

// readConfig loads config from a io.Reader into the config object
// versions manager will upgrade/downgrade if appropriate
// If encrypted, prompts for encryption key
func (c *Config) readConfig(d io.Reader, keyProvider keyProvider) error {
func (c *Config) readConfig(d io.Reader) error {
j, err := io.ReadAll(d)
if err != nil {
return err
}

if IsEncrypted(j) {
if j, err = c.decryptConfig(j, keyProvider); err != nil {
if j, err = c.decryptConfig(j); err != nil {
return err
}
}
Expand Down Expand Up @@ -1537,14 +1535,17 @@ func (c *Config) saveWithEncryptPrompt(path string) error {
}

// decryptConfig reads encrypted configuration and requests key from provider
func (c *Config) decryptConfig(j []byte, keyProvider keyProvider) ([]byte, error) {
func (c *Config) decryptConfig(j []byte) ([]byte, error) {
for range maxAuthFailures {
key, err := keyProvider()
f := c.EncryptionKeyProvider
if f == nil {
f = PromptForConfigKey
}
key, err := f(false)
if err != nil {
log.Errorf(log.ConfigMgr, "PromptForConfigKey err: %s", err)
continue
}

d, err := c.decryptConfigData(j, key)
if err != nil {
log.Errorln(log.ConfigMgr, "Could not decrypt and deserialise data with given key. Invalid password?", err)
Expand Down Expand Up @@ -1575,13 +1576,12 @@ func (c *Config) SaveConfigToFile(configPath string) error {
}
}
}()
return c.Save(provider, func() ([]byte, error) { return PromptForConfigKey(true) })
return c.Save(provider)
}

// Save saves your configuration to the writer as a JSON object
// with encryption, if configured
// Save saves your configuration to the writer as a JSON object with encryption, if configured
// If there is an error when preparing the data to store, the writer is never requested
func (c *Config) Save(writerProvider func() (io.Writer, error), keyProvider func() ([]byte, error)) error {
func (c *Config) Save(writerProvider func() (io.Writer, error)) error {
payload, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
Expand All @@ -1590,14 +1590,15 @@ func (c *Config) Save(writerProvider func() (io.Writer, error), keyProvider func
if c.EncryptConfig == fileEncryptionEnabled {
// Ensure we have the key from session or from user
if len(c.sessionDK) == 0 {
var key []byte
key, err = keyProvider()
if err != nil {
f := c.EncryptionKeyProvider
if f == nil {
f = PromptForConfigKey
}
var key, sessionDK, storedSalt []byte
if key, err = f(true); err != nil {
return err
}
var sessionDK, storedSalt []byte
sessionDK, storedSalt, err = makeNewSessionDK(key)
if err != nil {
if sessionDK, storedSalt, err = makeNewSessionDK(key); err != nil {
return err
}
c.sessionDK, c.storedSalt = sessionDK, storedSalt
Expand Down
Loading

0 comments on commit 3496c91

Please sign in to comment.