From fed53672406d7d0c1f9950643bdfc82e39f1457e Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Fri, 3 Mar 2017 17:32:48 +1100 Subject: [PATCH] Add config encryption support --- common.go | 7 ++ config.go | 81 ++++++++++++++---- configRoutes.go | 4 +- config_encryption.go | 192 ++++++++++++++++++------------------------- config_example.json | 1 + main.go | 18 +--- 6 files changed, 157 insertions(+), 146 deletions(-) diff --git a/common.go b/common.go index 50f459cb..47c255c6 100644 --- a/common.go +++ b/common.go @@ -168,6 +168,13 @@ func IsEnabled(isEnabled bool) string { } } +func YesOrNo(input string) bool { + if StringToLower(input) == "y" || StringToLower(input) == "yes" { + return true + } + return false +} + func CalculateAmountWithFee(amount, fee float64) float64 { return amount + CalculateFee(amount, fee) } diff --git a/config.go b/config.go index ed54060f..70d37899 100644 --- a/config.go +++ b/config.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "encoding/json" "errors" "fmt" @@ -13,6 +12,10 @@ import ( const ( CONFIG_FILE = "config.json" + + CONFIG_FILE_ENCRYPTION_PROMPT = 0 + CONFIG_FILE_ENCRYPTION_ENABLED = 1 + CONFIG_FILE_ENCRYPTION_DISABLED = -1 ) var ( @@ -20,16 +23,19 @@ var ( ErrExchangeAvailablePairsEmpty = "Exchange %s: Available pairs is empty." ErrExchangeEnabledPairsEmpty = "Exchange %s: Enabled pairs is empty." ErrExchangeBaseCurrenciesEmpty = "Exchange %s: Base currencies is empty." - WarningExchangeAuthAPIDefaultOrEmptyValues = "WARNING -- Exchange %s: Authenticated API support disabled due to default/empty APIKey/Secret/ClientID values." ErrExchangeNotFound = "Exchange %s: Not found." ErrNoEnabledExchanges = "No Exchanges enabled." ErrCryptocurrenciesEmpty = "Cryptocurrencies variable is empty." + ErrFailureOpeningConfig = "Fatal error opening config.json file. Error: %s" + ErrCheckingConfigValues = "Fatal error checking config values. Error: %s" + ErrSavingConfigBytesMismatch = "Config file %q bytes comparison doesn't match, read %s expected %s." WarningSMSGlobalDefaultOrEmptyValues = "WARNING -- SMS Support disabled due to default or empty Username/Password values." WarningSSMSGlobalSMSContactDefaultOrEmptyValues = "WARNING -- SMS contact #%d Name/Number disabled due to default or empty values." WarningSSMSGlobalSMSNoContacts = "WARNING -- SMS Support disabled due to no enabled contacts." WarningWebserverCredentialValuesEmpty = "WARNING -- Webserver support disabled due to empty Username/Password values." WarningWebserverListenAddressInvalid = "WARNING -- Webserver support disabled due to invalid listen address." WarningWebserverRootWebFolderNotFound = "WARNING -- Webserver support disabled due to missing web folder." + WarningExchangeAuthAPIDefaultOrEmptyValues = "WARNING -- Exchange %s: Authenticated API support disabled due to default/empty APIKey/Secret/ClientID values." ) type Webserver struct { @@ -55,6 +61,7 @@ type ConfigPost struct { type Config struct { Name string + EncryptConfig int Cryptocurrencies string SMS SMSGlobal `json:"SMSGlobal"` Webserver Webserver `json:"Webserver"` @@ -168,7 +175,6 @@ func CheckExchangeConfigValues() error { } func CheckWebserverValues() error { - if bot.config.Webserver.AdminUsername == "" || bot.config.Webserver.AdminPassword == "" { return errors.New(WarningWebserverCredentialValuesEmpty) } @@ -189,38 +195,79 @@ func CheckWebserverValues() error { return nil } -func ReadConfig() (Config, error) { +func ReadConfig() error { file, err := ioutil.ReadFile(CONFIG_FILE) - if err != nil { - return Config{}, err + return err } - cfg := Config{} - err = json.Unmarshal(file, &cfg) - return cfg, err + if !ConfirmECS(file) { + err := json.Unmarshal(file, &bot.config) + if err != nil { + return err + } + + if bot.config.EncryptConfig == CONFIG_FILE_ENCRYPTION_DISABLED { + return nil + } + + if bot.config.EncryptConfig == CONFIG_FILE_ENCRYPTION_PROMPT { + if PromptForConfigEncryption() { + bot.config.EncryptConfig = CONFIG_FILE_ENCRYPTION_ENABLED + SaveConfig() + } + } + } else { + key, err := PromptForConfigKey() + if err != nil { + return err + } + + data, err := DecryptConfigFile(file, key) + if err != nil { + return err + } + + err = json.Unmarshal(data, &bot.config) + if err != nil { + return err + } + } + return nil } func SaveConfig() error { - log.Println("Saving config") + log.Println("Saving config.") payload, err := json.MarshalIndent(bot.config, "", " ") - if err != nil { - return err + if bot.config.EncryptConfig == CONFIG_FILE_ENCRYPTION_ENABLED { + key, err := PromptForConfigKey() + if err != nil { + return err + } + + payload, err = EncryptConfigFile(payload, key) + if err != nil { + return err + } } err = ioutil.WriteFile(CONFIG_FILE, payload, 0644) - if err != nil { return err } - retrieved, err := ioutil.ReadFile(CONFIG_FILE) + return nil +} + +func LoadConfig() error { + err := ReadConfig() if err != nil { - return err + return fmt.Errorf(ErrFailureOpeningConfig, err) } - if !bytes.Equal(retrieved, payload) { - return fmt.Errorf("file %q content doesn't match, read %s expected %s\n", CONFIG_FILE, retrieved, payload) + err = CheckExchangeConfigValues() + if err != nil { + return fmt.Errorf(ErrCheckingConfigValues, err) } return nil diff --git a/configRoutes.go b/configRoutes.go index c2daab85..6355d16d 100644 --- a/configRoutes.go +++ b/configRoutes.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "log" "net/http" ) @@ -38,9 +37,8 @@ func SaveAllSettings(w http.ResponseWriter, r *http.Request) { if err != nil { panic(err) } - bot.config, err = ReadConfig() + err = LoadConfig() if err != nil { - log.Println("Fatal error checking config values. Error:", err) panic(err) } setupBotExchanges() diff --git a/config_encryption.go b/config_encryption.go index 098756db..8ae8e733 100644 --- a/config_encryption.go +++ b/config_encryption.go @@ -5,138 +5,106 @@ import ( "crypto/aes" "crypto/cipher" "crypto/rand" - "encoding/json" + "errors" "fmt" "io" - "io/ioutil" "log" - "os" ) const ( ENCRYPTION_CONFIRMATION_STRING = "THORS-HAMMER" ) -type Encryption struct { - CryptoKey []byte - EncryptedFile []byte - Nonce []byte - encrypPerm bool - decryptPerm bool -} +func PromptForConfigEncryption() bool { + log.Println("Would you like to encrypt your config file (y/n)?") -func (e *Encryption) SetUp() { - for len(e.CryptoKey) != 32 { - fmt.Println("Please enter a unique 32Char key to set up encryption: \n") - - _, err := fmt.Scanln(&e.CryptoKey) - if err != nil { - panic(err) - } - - if len(e.CryptoKey) > 32 || len(e.CryptoKey) < 32 { - fmt.Println("Please Re-enter a 32char key..\n") - } - } - - e.Nonce = make([]byte, 12) - if _, err := io.ReadFull(rand.Reader, e.Nonce); err != nil { - panic(err) - } - - e.encrypPerm = false - e.decryptPerm = false -} - -func (e *Encryption) Encrypt() []byte { - block, err := aes.NewCipher(e.CryptoKey) + input := "" + _, err := fmt.Scanln(&input) if err != nil { - panic(err) + return false } - aesgcm, err := cipher.NewGCM(block) - if err != nil { - panic(err) - } - - configfile := e.ReadFile(CONFIG_FILE) - - if e.ConfirmJSON(configfile) != true { - log.Println("File cannot be encrypted.\n") - if e.ConfirmECS(configfile) != true { - log.Println("File Corrupted.") - panic(err) - } - log.Println("File already encrypted.") - return configfile - } - e.EncryptedFile = aesgcm.Seal(nil, e.Nonce, configfile, nil) - - appendedFile := []byte(ENCRYPTION_CONFIRMATION_STRING) - appendedFile = append(appendedFile, e.EncryptedFile...) - return appendedFile -} - -func (e *Encryption) Decrypt() []byte { - blockDecrypt, err := aes.NewCipher(e.CryptoKey) - if err != nil { - panic(err) - } - - aesgcmDecrypt, err := cipher.NewGCM(blockDecrypt) - if err != nil { - panic(err) - } - - configfile := e.ReadFile(CONFIG_FILE) - if e.ConfirmECS(configfile) != true { - log.Println("File cannot be decrypted..\n") - if e.ConfirmJSON(configfile) != true { - log.Println("File corrupted.") - panic(err) - } - log.Println("File already decrypted.") - return configfile - } - - unencryptedFile, err := aesgcmDecrypt.Open(nil, e.Nonce, e.RemoveECS(configfile), nil) - if err != nil { - log.Println("File Corrupted") - panic(err) - } - return unencryptedFile -} - -func (e *Encryption) ReadFile(filename string) []byte { - file, err := ioutil.ReadFile(filename) - if err != nil { - panic(err) - } - return file -} - -func (e *Encryption) SaveFile(file []byte) { - mode := int(0777) - perm := os.FileMode(mode) - err := ioutil.WriteFile(CONFIG_FILE, file, perm) - if err != nil { - panic(err) - } -} - -func (e *Encryption) ConfirmJSON(file []byte) bool { - err := json.Unmarshal(file, nil) //Needs Revision - if err != nil { - return true //Fix after revision + if !YesOrNo(input) { + bot.config.EncryptConfig = CONFIG_FILE_ENCRYPTION_DISABLED + SaveConfig() + return false } return true } -func (e *Encryption) ConfirmECS(file []byte) bool { +func PromptForConfigKey() ([]byte, error) { + var cryptoKey []byte + + for len(cryptoKey) != 32 { + log.Println("Please enter your 32 character AES key:") + + _, err := fmt.Scanln(&cryptoKey) + if err != nil { + return nil, err + } + + if len(cryptoKey) > 32 || len(cryptoKey) < 32 { + fmt.Println("Please re-enter a 32char key:") + } + } + + nonce := make([]byte, 12) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + return cryptoKey, nil +} + +func EncryptConfigFile(configData, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + 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(ENCRYPTION_CONFIRMATION_STRING) + appendedFile = append(appendedFile, ciphertext...) + return appendedFile, nil +} + +func DecryptConfigFile(configData, key []byte) ([]byte, error) { + configData = RemoveECS(configData) + blockDecrypt, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + if len(configData) < aes.BlockSize { + return nil, errors.New("The config file data is too small for the AES required block size.") + } + + iv := configData[:aes.BlockSize] + configData = configData[aes.BlockSize:] + + stream := cipher.NewCFBDecrypter(blockDecrypt, iv) + stream.XORKeyStream(configData, configData) + result := configData + return result, nil +} + +func ConfirmWalletJSON(file []byte, result interface{}) error { + return JSONDecode(file, result) +} + +func ConfirmECS(file []byte) bool { subslice := []byte(ENCRYPTION_CONFIRMATION_STRING) return bytes.Contains(file, subslice) } -func (e *Encryption) RemoveECS(file []byte) []byte { +func RemoveECS(file []byte) []byte { return bytes.Trim(file, ENCRYPTION_CONFIRMATION_STRING) } diff --git a/config_example.json b/config_example.json index 1d3149a8..dc614013 100644 --- a/config_example.json +++ b/config_example.json @@ -1,5 +1,6 @@ { "Name": "Skynet", + "EncryptConfig": 0, "DisplayCurrency":"USD", "Cryptocurrencies": "BTC,XBT,LTC,XRP,XDG,DOGE,STR,NMC,STR,XDG,XRP,XVN", "SMSGlobal": { diff --git a/main.go b/main.go index 1dde04d8..6b8898ff 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "errors" "log" "net/http" "os" @@ -63,20 +62,11 @@ func setupBotExchanges() { func main() { HandleInterrupt() - log.Println("Loading config file config.json..") + log.Printf("Loading config file %s..\n", CONFIG_FILE) - err := errors.New("") - bot.config, err = ReadConfig() + err := LoadConfig() if err != nil { - log.Printf("Fatal error opening config.json file. Error: %s", err) - return - } - log.Println("Config file loaded. Checking settings.. ") - - err = CheckExchangeConfigValues() - if err != nil { - log.Println("Fatal error checking config values. Error:", err) - return + log.Fatal(err) } log.Printf("Bot '%s' started.\n", bot.config.Name) @@ -129,7 +119,7 @@ func main() { err = RetrieveConfigCurrencyPairs(bot.config) if err != nil { - log.Println("Fatal error retrieving config currency AvailablePairs. Error: ", err) + log.Fatalf("Fatal error retrieving config currency AvailablePairs. Error: ", err) } if bot.config.Webserver.Enabled {