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 14, 2024
1 parent 0ef8be3 commit f447b18
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 104 deletions.
136 changes: 82 additions & 54 deletions cmd/config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"errors"
"flag"
"fmt"
"os"
Expand All @@ -18,73 +19,106 @@ 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{
EncryptionKeyPrompt: func(_ bool) ([]byte, error) {

Check failure on line 68 in cmd/config/config.go

View workflow job for this annotation

GitHub Actions / lint

unknown field EncryptionKeyPrompt in struct literal of type config.Config (typecheck)

Check failure on line 68 in cmd/config/config.go

View workflow job for this annotation

GitHub Actions / GoCryptoTrader back-end (ubuntu-latest, amd64, true, false)

unknown field EncryptionKeyPrompt in struct literal of type config.Config

Check failure on line 68 in cmd/config/config.go

View workflow job for this annotation

GitHub Actions / GoCryptoTrader back-end (ubuntu-latest, 386, true, true)

unknown field EncryptionKeyPrompt in struct literal of type config.Config

Check failure on line 68 in cmd/config/config.go

View workflow job for this annotation

GitHub Actions / GoCryptoTrader back-end (macos-latest, amd64, true, true)

unknown field EncryptionKeyPrompt in struct literal of type config.Config

Check failure on line 68 in cmd/config/config.go

View workflow job for this annotation

GitHub Actions / GoCryptoTrader back-end (macos-13, amd64, true, true)

unknown field EncryptionKeyPrompt in struct literal of type config.Config

Check failure on line 68 in cmd/config/config.go

View workflow job for this annotation

GitHub Actions / GoCryptoTrader back-end (windows-latest, amd64, true, true)

unknown field EncryptionKeyPrompt in struct literal of type config.Config
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)
}
return outData
return outData, nil
}

func readFile(in string) []byte {
Expand All @@ -95,45 +129,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) (string, []string) {
cmd, rem := []string{}, []string{}
for _, s := range a {
if slices.Contains(commands, s) {
cmd = append(cmd, s)
} else {
rem = append(rem, s)
}
}
switch len(cmd) {
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 cmd[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(cmd, ", "))
}
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
31 changes: 17 additions & 14 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,8 +1590,11 @@ 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()
f := c.EncryptionKeyProvider
if f == nil {
f = PromptForConfigKey
}
key, err := f(true)
if err != nil {
return err
}
Expand Down
49 changes: 21 additions & 28 deletions config/config_encryption.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import (
"fmt"
"io"
"os"
"syscall"

"github.com/buger/jsonparser"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"golang.org/x/crypto/scrypt"
"golang.org/x/term"
)

const (
Expand Down Expand Up @@ -42,48 +44,39 @@ func promptForConfigEncryption() (bool, error) {
return common.YesOrNo(input), nil
}

// Unencrypted provides the default key provider implementation for unencrypted files
func Unencrypted() ([]byte, error) {
return nil, errors.New("encryption key was requested, no key provided")
}

// PromptForConfigKey asks for configuration key
// if initialSetup is true, the password needs to be repeated
func PromptForConfigKey(initialSetup bool) ([]byte, error) {
var cryptoKey []byte

for {
fmt.Println("Please enter in your password: ")
pwPrompt := func(i *[]byte) error {
_, err := fmt.Scanln(i)
return err
}
func PromptForConfigKey(confirmKey bool) ([]byte, error) {
for i := 0; i < 3; i++ {
fmt.Print("Please enter encryption key: ")
key, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println()

var p1 []byte
err := pwPrompt(&p1)
if err != nil {
return nil, err
}

if !initialSetup {
cryptoKey = p1
break
if len(key) == 0 {
continue
}

var p2 []byte
fmt.Println("Please re-enter your password: ")
err = pwPrompt(&p2)
if !confirmKey {
return key, nil
}

fmt.Print("Please re-enter key: ")
conf, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println()

if err != nil {
return nil, err
}

if bytes.Equal(p1, p2) {
cryptoKey = p1
break
if bytes.Equal(key, conf) {
return key, nil
}
fmt.Println("Passwords did not match, please try again.")
fmt.Println("Keys did not match, please try again.")
}
return cryptoKey, nil
return nil, errors.New("No key entered")
}

// EncryptConfigFile encrypts configuration data that is parsed in with a key
Expand Down
9 changes: 5 additions & 4 deletions config/config_encryption_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,17 +261,18 @@ func TestReadConfigWithPrompt(t *testing.T) {

func TestReadEncryptedConfigFromReader(t *testing.T) {
t.Parallel()
c := &Config{}
keyProvider := func() ([]byte, error) { return []byte("pass"), nil }
c := &Config{
EncryptionKeyProvider: func(_ bool) ([]byte, error) { return []byte("pass"), nil },
}
// Encrypted conf for: `{"name":"test"}` with key `pass`
confBytes := []byte{84, 72, 79, 82, 83, 45, 72, 65, 77, 77, 69, 82, 126, 71, 67, 84, 126, 83, 79, 126, 83, 65, 76, 84, 89, 126, 246, 110, 128, 3, 30, 168, 172, 160, 198, 176, 136, 62, 152, 155, 253, 176, 16, 48, 52, 246, 44, 29, 151, 47, 217, 226, 178, 12, 218, 113, 248, 172, 195, 232, 136, 104, 9, 199, 20, 4, 71, 4, 253, 249}
err := c.readConfig(bytes.NewReader(confBytes), keyProvider)
err := c.readConfig(bytes.NewReader(confBytes))
require.NoError(t, err)
assert.Equal(t, "test", c.Name)

// Change the salt
confBytes[20] = 0
err = c.readConfig(bytes.NewReader(confBytes), keyProvider)
err = c.readConfig(bytes.NewReader(confBytes))
require.ErrorIs(t, err, errDecryptFailed)
}

Expand Down
Loading

0 comments on commit f447b18

Please sign in to comment.