Files
gocryptotrader/config/config_encryption.go
Ryan O'Hara-Reid 0c5d75b22c (Engine) Variety of engine updates (#390)
* drop common uuid v4 func and imported package as needed

* removed common functions regarding json marshal and unmarshal and used the json package directly. WRT unmarshal it was calling reflect and converted to string which is also checked in the JSON package so it was doing a double up, this will be a tiny gain as it was directly used in the requester package for all our outbound requests.

* add in string

* explicitly throw away return error value

* atleast return the error that websocket initialise returns

* return error when not connected

* fix comment

* Adds comments

* move package declarations

* drop append whenever we call supported

* remove unused import

* Change incorrect spelling

* fix tests

* fix go import issue
2019-12-03 10:06:08 +11:00

211 lines
4.7 KiB
Go

package config
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
log "github.com/thrasher-corp/gocryptotrader/logger"
"golang.org/x/crypto/scrypt"
)
const (
// EncryptConfirmString has a the general confirmation string to allow us to
// see if the file is correctly encrypted
EncryptConfirmString = "THORS-HAMMER"
// SaltPrefix string
SaltPrefix = "~GCT~SO~SALTY~"
// SaltRandomLength is the number of random bytes to append after the prefix string
SaltRandomLength = 12
errAESBlockSize = "config file data is too small for the AES required block size"
)
var (
storedSalt []byte
sessionDK []byte
)
// PromptForConfigEncryption asks for encryption key
func (c *Config) PromptForConfigEncryption(configPath string, dryrun bool) bool {
fmt.Println("Would you like to encrypt your config file (y/n)?")
input := ""
_, err := fmt.Scanln(&input)
if err != nil {
return false
}
if !common.YesOrNo(input) {
c.EncryptConfig = fileEncryptionDisabled
err := c.SaveConfig(configPath, dryrun)
if err != nil {
log.Errorf(log.ConfigMgr, "cannot save config %s", err)
}
return false
}
return true
}
// PromptForConfigKey asks for configuration key
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
}
var p1 []byte
err := pwPrompt(&p1)
if err != nil {
return nil, err
}
if !initialSetup {
cryptoKey = p1
break
}
var p2 []byte
fmt.Println("Please re-enter your password: ")
err = pwPrompt(&p2)
if err != nil {
return nil, err
}
if bytes.Equal(p1, p2) {
cryptoKey = p1
break
}
fmt.Printf("Passwords did not match, please try again.")
}
return cryptoKey, nil
}
// EncryptConfigFile encrypts configuration data that is parsed in with a key
// and returns it as a byte array with an error
func EncryptConfigFile(configData, key []byte) ([]byte, error) {
var err error
if len(sessionDK) == 0 {
sessionDK, err = makeNewSessionDK(key)
if err != nil {
return nil, err
}
}
block, err := aes.NewCipher(sessionDK)
if err != nil {
return nil, err
}
ciphertext := make([]byte, aes.BlockSize+len(configData))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], configData)
appendedFile := []byte(EncryptConfirmString)
appendedFile = append(appendedFile, storedSalt...)
appendedFile = append(appendedFile, ciphertext...)
return appendedFile, nil
}
// DecryptConfigFile decrypts configuration data with the supplied key and
// returns the un-encrypted file as a byte array with an error
func DecryptConfigFile(configData, key []byte) ([]byte, error) {
configData = RemoveECS(configData)
origKey := key
if ConfirmSalt(configData) {
salt := make([]byte, len(SaltPrefix)+SaltRandomLength)
salt = configData[0:len(salt)]
dk, err := getScryptDK(key, salt)
if err != nil {
return nil, err
}
configData = configData[len(salt):]
key = dk
}
blockDecrypt, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(configData) < aes.BlockSize {
return nil, errors.New(errAESBlockSize)
}
iv := configData[:aes.BlockSize]
configData = configData[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(blockDecrypt, iv)
stream.XORKeyStream(configData, configData)
result := configData
sessionDK, err = makeNewSessionDK(origKey)
if err != nil {
return nil, err
}
return result, nil
}
// ConfirmConfigJSON confirms JSON in file
func ConfirmConfigJSON(file []byte, result interface{}) error {
return json.Unmarshal(file, &result)
}
// ConfirmSalt checks whether the encrypted data contains a salt
func ConfirmSalt(file []byte) bool {
return bytes.Contains(file, []byte(SaltPrefix))
}
// ConfirmECS confirms that the encryption confirmation string is found
func ConfirmECS(file []byte) bool {
return bytes.Contains(file, []byte(EncryptConfirmString))
}
// RemoveECS removes encryption confirmation string
func RemoveECS(file []byte) []byte {
return bytes.Trim(file, EncryptConfirmString)
}
func getScryptDK(key, salt []byte) ([]byte, error) {
if len(key) == 0 {
return nil, errors.New("key is empty")
}
return scrypt.Key(key, salt, 32768, 8, 1, 32)
}
func makeNewSessionDK(key []byte) ([]byte, error) {
var err error
storedSalt, err = crypto.GetRandomSalt([]byte(SaltPrefix), SaltRandomLength)
if err != nil {
return nil, err
}
dk, err := getScryptDK(key, storedSalt)
if err != nil {
return nil, err
}
return dk, nil
}