Files
gocryptotrader/config/config_encryption_test.go
Rauno Ots ee55ae5d0f Config: refactor config file loaders (#577)
* Config: fix don't create empty dir when resolving path

* Config: refactor config file loaders

* add a layer of abstraction so that config can be loaded from non-files
* use io.Reader / io.Writer abstraction to separate data operations from
file operations
* remove dryrun option from SaveConfig - now it always saves

* rename read and save methods to mention file operations

* log error when encryption prompt fails

* as the user didn't make a choice, we'd prompt again next time the file
is loaded
* add file.Writer tests
* skip permissions test for windows

* defer creating the writer on save to the last moment

* this avoids truncating file when there is error with password prompt
* add a test

* tests with StdIn cannot run in parallel
2020-11-04 11:46:13 +11:00

372 lines
9.1 KiB
Go

package config
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
)
func TestPromptForConfigEncryption(t *testing.T) {
t.Parallel()
confirm, err := promptForConfigEncryption()
if confirm {
t.Error("promptForConfigEncryption return incorrect bool")
}
if err == nil {
t.Error("Expected error as there is no input")
}
}
func TestPromptForConfigKey(t *testing.T) {
t.Parallel()
byteyBite, err := PromptForConfigKey(true)
if err == nil && len(byteyBite) > 1 {
t.Errorf("PromptForConfigKey: %s", err)
}
_, err = PromptForConfigKey(false)
if err == nil {
t.Error("Expected error")
}
}
func TestEncryptConfigFile(t *testing.T) {
_, err := EncryptConfigFile([]byte("test"), nil)
if err == nil {
t.Fatal("Expected error")
}
c := &Config{
sessionDK: []byte("a"),
}
_, err = c.encryptConfigFile([]byte("test"))
if err == nil {
t.Fatal("Expected error")
}
sessDk, salt, err := makeNewSessionDK([]byte("asdf"))
if err != nil {
t.Fatal(err)
}
c = &Config{
sessionDK: sessDk,
storedSalt: salt,
}
_, err = c.encryptConfigFile([]byte("test"))
if err != nil {
t.Fatal(err)
}
}
func TestDecryptConfigFile(t *testing.T) {
result, err := EncryptConfigFile([]byte("test"), []byte("key"))
if err != nil {
t.Fatal(err)
}
_, err = DecryptConfigFile(result, nil)
if err == nil {
t.Fatal("Expected error")
}
_, err = DecryptConfigFile([]byte("test"), nil)
if err == nil {
t.Fatal("Expected error")
}
_, err = DecryptConfigFile([]byte("test"), []byte("AAAAAAAAAAAAAAAA"))
if err == nil {
t.Fatalf("Expected %s", errAESBlockSize)
}
result, err = EncryptConfigFile([]byte("test"), []byte("key"))
if err != nil {
t.Fatal(err)
}
_, err = DecryptConfigFile(result, []byte("key"))
if err != nil {
t.Fatal(err)
}
}
func TestConfirmECS(t *testing.T) {
t.Parallel()
ECStest := []byte(EncryptConfirmString)
if !ConfirmECS(ECStest) {
t.Errorf("TestConfirmECS: Error finding ECS.")
}
}
func TestRemoveECS(t *testing.T) {
t.Parallel()
ECStest := []byte(EncryptConfirmString)
reader := bytes.NewReader(ECStest)
err := skipECS(reader)
if err != nil {
t.Error(err)
}
// Attempt read
var buf []byte
_, err = reader.Read(buf)
if err != io.EOF {
t.Errorf("TestConfirmECS: Error ECS not deleted.")
}
}
func TestMakeNewSessionDK(t *testing.T) {
t.Parallel()
_, _, err := makeNewSessionDK(nil)
if err == nil {
t.Fatal("makeNewSessionDK passed with nil key")
}
}
func TestEncryptTwiceReusesSaltButNewCipher(t *testing.T) {
c := &Config{}
c.EncryptConfig = 1
tempDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("Problem creating temp dir at %s: %s\n", tempDir, err)
}
defer os.RemoveAll(tempDir)
// Prepare input
passFile, err := ioutil.TempFile(tempDir, "*.pw")
if err != nil {
t.Fatalf("Problem creating temp file at %s: %s\n", tempDir, err)
}
passFile.WriteString("pass\npass\n")
passFile.Close()
// Temporarily replace Stdin with a custom input
oldIn := os.Stdin
defer func() { os.Stdin = oldIn }()
os.Stdin, err = os.Open(passFile.Name())
if err != nil {
t.Fatalf("Problem opening temp file at %s: %s\n", passFile.Name(), err)
}
// Save encrypted config
enc1 := filepath.Join(tempDir, "encrypted.dat")
err = c.SaveConfigToFile(enc1)
if err != nil {
t.Fatalf("Problem storing config in file %s: %s\n", enc1, err)
}
// Save again
enc2 := filepath.Join(tempDir, "encrypted2.dat")
err = c.SaveConfigToFile(enc2)
if err != nil {
t.Fatalf("Problem storing config in file %s: %s\n", enc2, err)
}
data1, err := ioutil.ReadFile(enc1)
if err != nil {
t.Fatalf("Problem reading file %s: %s\n", enc1, err)
}
data2, err := ioutil.ReadFile(enc2)
if err != nil {
t.Fatalf("Problem reading file %s: %s\n", enc2, err)
}
// legth of prefix + salt
l := len(EncryptConfirmString+SaltPrefix) + SaltRandomLength
// Even though prefix, including salt with the random bytes is the same
if !bytes.Equal(data1[:l], data2[:l]) {
t.Error("Salt is not reused.")
}
// the cipher text should not be
if bytes.Equal(data1, data2) {
t.Error("Encryption key must have been reused as cipher texts are the same")
}
}
func TestSaveAndReopenEncryptedConfig(t *testing.T) {
c := &Config{}
c.Name = "myCustomName"
c.EncryptConfig = 1
tempDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("Problem creating temp dir at %s: %s\n", tempDir, err)
}
defer os.RemoveAll(tempDir)
// Save encrypted config
enc := filepath.Join(tempDir, "encrypted.dat")
err = withInteractiveResponse(t, "pass\npass\n", func() error {
return c.SaveConfigToFile(enc)
})
if err != nil {
t.Fatalf("Problem storing config in file %s: %s\n", enc, err)
}
readConf := &Config{}
err = withInteractiveResponse(t, "pass\n", func() error {
// Load with no existing state, key is read from the prepared file
return readConf.ReadConfigFromFile(enc, true)
})
// Verify
if err != nil {
t.Fatalf("Problem reading config in file %s: %s\n", enc, err)
}
if c.Name != readConf.Name || c.EncryptConfig != readConf.EncryptConfig {
t.Error("Loaded conf not the same as original")
}
}
// setAnswersFile sets the given file as the current stdin
// returns the close function to defer for reverting the stdin
func setAnswersFile(t *testing.T, answerFile string) func() {
oldIn := os.Stdin
inputFile, err := os.Open(answerFile)
if err != nil {
t.Fatalf("Problem opening temp file at %s: %s\n", answerFile, err)
}
os.Stdin = inputFile
return func() {
inputFile.Close()
os.Stdin = oldIn
}
}
func TestReadConfigWithPrompt(t *testing.T) {
// Prepare temp dir
tempDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("Problem creating temp dir at %s: %s\n", tempDir, err)
}
defer os.RemoveAll(tempDir)
// Ensure we'll get the prompt when loading
c := &Config{
EncryptConfig: 0,
}
// Save config
testConfigFile := filepath.Join(tempDir, "config.json")
err = c.SaveConfigToFile(testConfigFile)
if err != nil {
t.Fatalf("Problem saving config file in %s: %s\n", tempDir, err)
}
// Run the test
c = &Config{}
err = withInteractiveResponse(t, "y\npass\npass\n", func() error {
return c.ReadConfigFromFile(testConfigFile, false)
})
if err != nil {
t.Fatalf("Problem reading config file at %s: %s\n", testConfigFile, err)
}
// Verify results
data, err := ioutil.ReadFile(testConfigFile)
if err != nil {
t.Fatalf("Problem reading saved file at %s: %s\n", testConfigFile, err)
}
if c.EncryptConfig != fileEncryptionEnabled {
t.Error("Config encryption flag should be set after prompts")
}
if !ConfirmECS(data) {
t.Error("Config file should be encrypted after prompts")
}
}
func TestReadEncryptedConfigFromReader(t *testing.T) {
keyProvider := func() ([]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}
conf, encrypted, err := ReadConfig(bytes.NewReader(confBytes), keyProvider)
if err != nil {
t.Errorf("TestReadConfig %s", err)
}
if !encrypted {
t.Errorf("Expected encrypted config %s", err)
}
if conf.Name != "test" {
t.Errorf("Conf not properly loaded %s", err)
}
// Change the salt
confBytes[20] = 0
conf, _, err = ReadConfig(bytes.NewReader(confBytes), keyProvider)
if err == nil {
t.Errorf("Expected unable to decrypt, but got %+v", conf)
}
}
// TestSaveConfigToFileWithErrorInPasswordPrompt should preserve the original file
func TestSaveConfigToFileWithErrorInPasswordPrompt(t *testing.T) {
c := &Config{
Name: "test",
EncryptConfig: fileEncryptionEnabled,
}
testData := []byte("testdata")
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
targetFile := f.Name()
defer os.Remove(targetFile)
_, err = io.Copy(f, bytes.NewReader(testData))
if err != nil {
t.Fatal(err)
}
err = f.Close()
if err != nil {
t.Fatal(err)
}
err = withInteractiveResponse(t, "\n\n", func() error {
err = c.SaveConfigToFile(targetFile)
if err == nil {
t.Error("Expected error")
}
return nil
})
if err != nil {
t.Fatal(err)
}
data, err := ioutil.ReadFile(targetFile)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(data, testData) {
t.Errorf("Expected contents %s, but was %s", testData, data)
}
}
func withInteractiveResponse(t *testing.T, response string, body func() error) error {
// Answers to the prompt
responseFile, err := ioutil.TempFile("", "*.in")
if err != nil {
return fmt.Errorf("problem creating temp file: %w", err)
}
_, err = responseFile.WriteString(response)
if err != nil {
return fmt.Errorf("problem writing to temp file at %s: %w", responseFile.Name(), err)
}
err = responseFile.Close()
if err != nil {
return fmt.Errorf("problem closing temp file at %s: %w", responseFile.Name(), err)
}
defer os.Remove(responseFile.Name())
// Temporarily replace Stdin with a custom input
cleanup := setAnswersFile(t, responseFile.Name())
defer cleanup()
return body()
}