Skip to content

Commit

Permalink
feat(opentofu#297): allow stacking and add pbkdf2
Browse files Browse the repository at this point in the history
  • Loading branch information
StephanHCB committed Oct 23, 2023
1 parent 68f6d21 commit 7114bab
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 97 deletions.
5 changes: 3 additions & 2 deletions internal/states/statecrypto/cryptoconfig/config.go
Expand Up @@ -14,8 +14,9 @@ type Config struct {
// Methods selects the methods to place in the encryption stack
//
// supported values are
// "client-side/AES256-CFB/SHA256"
// ...
// "encrypt/AES256-CFB/SHA256"
// "derive-key/PBKDF2/AES256"
// ... (see the constants at the top of each method implementation)
//
// supplying an unsupported value raises an error.
//
Expand Down
9 changes: 8 additions & 1 deletion internal/states/statecrypto/cryptoconfig/method.go
Expand Up @@ -45,5 +45,12 @@ type MethodMetadata struct {
//
// You can reject invalid configurations here, but if you want another method to be
// able to provide a configuration value, then you should not check it during the construction phase.
Constructor func(Config) (Method, error)
// Example: it would be wrong to check that the configuration provides the encryption key,
// because that may be provided by a method further up in the stack.
//
// nextInStack is the next method in the stack, or nil if this method is the last
// in the stack. If your method expects to be the last in the stack, you should
// return an error if nextInStack is not nil. Similarly, if your method is for key derivation,
// you should return a meaningful error if nextInStack is nil.
Constructor func(configuration Config, nextInStack Method) (Method, error)
}
12 changes: 5 additions & 7 deletions internal/states/statecrypto/flow/config.go
Expand Up @@ -34,16 +34,14 @@ func EnabledForPlanFile(config cryptoconfig.Config) bool {
// constructor functions defined in the metadata of the listed methods.
//
// This will typically also validate static parameters, but not dynamic parameters such as keys.
func buildMethodStack(config cryptoconfig.Config) ([]cryptoconfig.Method, error) {
result := make([]cryptoconfig.Method, 0)
for _, name := range config.Methods {
method, err := methods.MethodByName(name, config)
func buildMethodStack(config cryptoconfig.Config) (current cryptoconfig.Method, err error) {
for i := len(config.Methods) - 1; i >= 0; i-- {
current, err = methods.MethodByName(config.Methods[i], config, current)
if err != nil {
return result, err
return nil, err
}
result = append(result, method)
}
return result, nil
return current, nil
}

func configFromEnv(envName string) (cryptoconfig.Config, error) {
Expand Down
10 changes: 3 additions & 7 deletions internal/states/statecrypto/flow/decryption.go
Expand Up @@ -66,11 +66,7 @@ func attemptDecryption(stateJson []byte, config cryptoconfig.Config) ([]byte, er
return stateJson, err
}

for _, method := range stack {
stateJson, config, err = method.Decrypt(stateJson, config)
if err != nil {
return stateJson, err
}
}
return stateJson, nil
stateJson, _, err = stack.Decrypt(stateJson, config)

return stateJson, err
}
10 changes: 3 additions & 7 deletions internal/states/statecrypto/flow/encryption.go
Expand Up @@ -38,11 +38,7 @@ func attemptEncryption(stateJson []byte, config cryptoconfig.Config) ([]byte, er
return stateJson, err
}

for _, method := range stack {
stateJson, config, err = method.Encrypt(stateJson, config)
if err != nil {
return stateJson, err
}
}
return stateJson, nil
stateJson, _, err = stack.Encrypt(stateJson, config)

return stateJson, err
}
102 changes: 93 additions & 9 deletions internal/states/statecrypto/flow/flow_test.go
Expand Up @@ -3,6 +3,7 @@ package flow
import (
"fmt"
"github.com/opentofu/opentofu/internal/states/statecrypto/cryptoconfig"
"log"
"testing"
)

Expand Down Expand Up @@ -36,14 +37,22 @@ func TestCreation_invalidConfigUnknownMethod(t *testing.T) {

// business scenarios

const validConfigWithKey1 = `{"encrypt_remote_state":true,"methods":["client-side/AES256-CFB/SHA256"],"parameters":{"key":"a0a1a2a3a4a5a6a7a8a9b0b1b2b3b4b5b6b7b8b9c0c1c2c3c4c5c6c7c8c9d0d1"}}`
const validConfigWithKey2 = `{"encrypt_remote_state":true,"methods":["client-side/AES256-CFB/SHA256"],"parameters":{"key":"89346775897897a35892735ffd34723489734ee238748293741abcdef0123456"}}`
const validConfigWithKey3 = `{"encrypt_remote_state":true,"methods":["client-side/AES256-CFB/SHA256"],"parameters":{"key":"33336775897897a35892735ffd34723489734ee238748293741abcdef0123456"}}`
const validConfigWithKey1 = `{"encrypt_remote_state":true,"methods":["encrypt/AES256-CFB/SHA256"],"parameters":{"key":"a0a1a2a3a4a5a6a7a8a9b0b1b2b3b4b5b6b7b8b9c0c1c2c3c4c5c6c7c8c9d0d1"}}`
const validConfigWithKey2 = `{"encrypt_remote_state":true,"methods":["encrypt/AES256-CFB/SHA256"],"parameters":{"key":"89346775897897a35892735ffd34723489734ee238748293741abcdef0123456"}}`
const validConfigWithKey3 = `{"encrypt_remote_state":true,"methods":["encrypt/AES256-CFB/SHA256"],"parameters":{"key":"33336775897897a35892735ffd34723489734ee238748293741abcdef0123456"}}`

const validConfigWithPassphrase1 = `{"encrypt_remote_state":true,"methods":["derive-key/PBKDF2/AES256","encrypt/AES256-CFB/SHA256"],"parameters":{"passphrase":"this is demo passphrase 1"}}`
const validConfigWithPassphrase2 = `{"encrypt_remote_state":true,"methods":["derive-key/PBKDF2/AES256","encrypt/AES256-CFB/SHA256"],"parameters":{"passphrase":"this is demo passphrase 2"}}`

const validPlaintext = `{"animals":[{"species":"cheetah","genus":"acinonyx"}]}`
const validEncryptedKey1 = `{"crypted":"e93e3e7ad3434055251f695865a13c11744b97e54cb7dee8f8fb40d1fb096b728f2a00606e7109f0720aacb15008b410cf2f92dd7989c2ff10b9712b6ef7d69ecdad1dccd2f1bddd127f0f0d87c79c3c062e03c2297614e2effa2fb1f4072d86df0dda4fc061"}`
const validEncryptedKey1 = `{"method":"encrypt/AES256-CFB/SHA256","payload":"e93e3e7ad3434055251f695865a13c11744b97e54cb7dee8f8fb40d1fb096b728f2a00606e7109f0720aacb15008b410cf2f92dd7989c2ff10b9712b6ef7d69ecdad1dccd2f1bddd127f0f0d87c79c3c062e03c2297614e2effa2fb1f4072d86df0dda4fc061"}`
const validEncryptedPassphrase1 = `{"method":"derive-key/PBKDF2/AES256","payload":{"method":"encrypt/AES256-CFB/SHA256","payload":"226ba6151c7751f2bd2ffcc2f666397f0cf8f6ea8ac2e336d7f3e07c8b8145f7e317f70a6bf4478b3c4469189ba1daccfe21ee76a88cbc66e460081d323b9344d5771b1a02ed7e477f69ad326c6c9d7d44d154b2e3d2c9b23fb34241be5cdcbdb4137321ddd2"},"salt":"a7da6b991820460619aaf1fb647ebde7"}`

const invalidConfigNoKey = `{"encrypt_remote_state":true,"methods":["encrypt/AES256-CFB/SHA256"],"parameters":{}}`

const invalidConfigNoKey = `{"encrypt_remote_state":true,"methods":["client-side/AES256-CFB/SHA256"],"parameters":{}}`
// TODO write test for these
const invalidConfigEncryptionNotLast = `{"encrypt_remote_state":true,"methods":["encrypt/AES256-CFB/SHA256","derive-key/PBKDF2/AES256"],"parameters":{"passphrase","this is a demo passphrase"}}`
const invalidConfigNoEncryption = `{"encrypt_remote_state":true,"methods":["derive-key/PBKDF2/AES256"],"parameters":{"passphrase","this is a demo passphrase"}}`

func compareSlices(got []byte, expected []byte) bool {
eEmpty := len(expected) == 0
Expand Down Expand Up @@ -79,6 +88,7 @@ func compareErrors(got error, expected string) string {
}

type roundtripTestCase struct {
name string
description string
primaryConfiguration string
fallbackConfiguration string
Expand All @@ -91,70 +101,142 @@ type roundtripTestCase struct {
func TestEncryptDecrypt(t *testing.T) {
// each test case first encrypts, then decrypts again
testCases := []roundtripTestCase{
// happy path cases
// happy path cases - no encryption
{
name: "no encryption",
description: "unencrypted operation - no encryption configuration present, no fallback",
input: validPlaintext,
},

// happy path cases - AES256
{
name: "aes256 normal",
description: "normal operation on encrypted data - main configuration for aes256, no fallback",
primaryConfiguration: validConfigWithKey1,
input: validPlaintext,
},
{
name: "aes256 initial encrypt",
description: "initial encryption - main configuration for aes256, no fallback - prints a warning but must work anyway",
primaryConfiguration: validConfigWithKey1,
input: validPlaintext,
injectOutput: validPlaintext,
},
{
name: "aes256 decrypt",
description: "decryption - no main configuration, fallback aes256",
fallbackConfiguration: validConfigWithKey1,
input: validPlaintext, // exact value irrelevant for this test case
injectOutput: validEncryptedKey1,
},
{
name: "aes256 already decrypted",
description: "unencrypted operation with fallback still present (decryption edge case) - no encryption configuration present, fallback aes256 - prints a warning but must still work anyway",
input: validPlaintext,
fallbackConfiguration: validConfigWithKey1,
},
{
name: "aes256 rotation",
description: "key rotation - main configuration for aes256 key 2, fallback aes256 key 1, read state with key 1 encryption - prints a warning but must work anyway",
primaryConfiguration: validConfigWithKey2,
fallbackConfiguration: validConfigWithKey1,
input: validPlaintext, // exact value irrelevant for this test case
injectOutput: validEncryptedKey1,
},
{
name: "aes256 already rotated",
description: "key rotation - main configuration for aes256 key 2, fallback aes256 key 1, read state with key 2 encryption",
primaryConfiguration: validConfigWithKey2,
fallbackConfiguration: validConfigWithKey1,
input: validPlaintext,
},
{
name: "aes256 initial encrypt during rotation",
description: "initial encryption happens during key rotation (key rotation edge case) - main configuration for aes256 key 1, fallback for aes256 key 2 - prints a warning but must still work anyway",
primaryConfiguration: validConfigWithKey1,
fallbackConfiguration: validConfigWithKey2,
input: validPlaintext, // exact value irrelevant for this test case
injectOutput: validPlaintext,
},

// error cases
// happy path cases - PBKDF2 + AES256
{
name: "pbkdf2 aes256 normal",
description: "normal operation on encrypted data - main configuration for pbkdf2_aes256, no fallback",
primaryConfiguration: validConfigWithPassphrase1,
input: validPlaintext,
},
{
name: "pbkdf2 aes256 initial encrypt",
description: "initial encryption - main configuration for pbkdf2_aes256, no fallback - prints a warning but must work anyway",
primaryConfiguration: validConfigWithPassphrase1,
input: validPlaintext,
injectOutput: validPlaintext,
},
{
name: "pbkdf2 aes256 decrypt",
description: "decryption - no main configuration, fallback pbkdf2_aes256",
fallbackConfiguration: validConfigWithPassphrase1,
input: validPlaintext, // exact value irrelevant for this test case
injectOutput: validEncryptedPassphrase1,
},
{
name: "pbkdf2 aes256 already decrypted",
description: "unencrypted operation with fallback still present (decryption edge case) - no encryption configuration present, fallback pbkdf2_aes256 - prints a warning but must still work anyway",
input: validPlaintext,
fallbackConfiguration: validConfigWithPassphrase1,
},
{
name: "pbkdf2 aes256 rotation",
description: "key rotation for pbkdf2_aes256 - prints a warning but must work anyway",
primaryConfiguration: validConfigWithPassphrase2,
fallbackConfiguration: validConfigWithPassphrase1,
input: validPlaintext, // exact value irrelevant for this test case
injectOutput: validEncryptedPassphrase1,
},
{
name: "pbkdf2 aes256 already rotated",
description: "key rotation for pbkdf2_aes256 - already rotated state - read state with phrase 2 encryption",
primaryConfiguration: validConfigWithPassphrase2,
fallbackConfiguration: validConfigWithPassphrase1,
input: validPlaintext,
},
{
name: "pbkdf2 aes256 initial encrypt during rotation",
description: "initial encryption happens during pbkdf2_aes256 key rotation (key rotation edge case) - prints a warning but must still work anyway",
primaryConfiguration: validConfigWithPassphrase1,
fallbackConfiguration: validConfigWithPassphrase2,
input: validPlaintext, // exact value irrelevant for this test case
injectOutput: validPlaintext,
},
{
name: "aes256 to pbkdf2 aes256 switch",
description: "transparent switch from aes256 encryption to pbkdf2_aes256 - decryption using the fallback configuration - prints a warning but must still work anyway",
primaryConfiguration: validConfigWithPassphrase2,
fallbackConfiguration: validConfigWithKey1,
input: validPlaintext, // exact value irrelevant for this test case
injectOutput: validEncryptedKey1,
},

// error cases - AES256
{
name: "aes256 wrong key",
description: "decryption fails due to wrong key - main configuration for aes256 key 3 - but state was encrypted with key 1",
primaryConfiguration: validConfigWithKey3,
input: validPlaintext, // exact value irrelevant for this test case
injectOutput: validEncryptedKey1,
expectedDecError: "hash of decrypted payload did not match at position 0",
},
{
name: "aes256 wrong fallback key",
description: "decryption fails due to wrong fallback key during decrypt lifecycle - no main configuration, fallback configuration for aes256 key 3 - but state was encrypted with key 1 - must fail and not use passthrough",
fallbackConfiguration: validConfigWithKey3,
input: validPlaintext, // exact value irrelevant for this test case
injectOutput: validEncryptedKey1,
expectedDecError: "hash of decrypted payload did not match at position 0",
},
{
name: "aes256 two wrong keys",
description: "decryption fails due to two wrong keys - main configuration for aes256 key 3, fallback for aes256 key 2 - but state was encrypted with key 1",
primaryConfiguration: validConfigWithKey3,
fallbackConfiguration: validConfigWithKey2,
Expand All @@ -163,8 +245,9 @@ func TestEncryptDecrypt(t *testing.T) {
expectedDecError: "hash of decrypted payload did not match at position 0",
},

// key error cases
// key error cases - AES256
{
name: "aes256 missing key",
description: "encryption fails due to missing key",
primaryConfiguration: invalidConfigNoKey,
input: validPlaintext, // exact value irrelevant for this test case
Expand All @@ -175,7 +258,8 @@ func TestEncryptDecrypt(t *testing.T) {
}

for _, tc := range testCases {
t.Run(fmt.Sprintf("test case: %s", tc.description), func(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
log.Printf("test case: %s: %s", tc.name, tc.description)
primaryConfig, err := configFromString(tc.primaryConfiguration)
if err != nil {
t.Fatalf("error parsing main configuration: %s", err)
Expand Down

0 comments on commit 7114bab

Please sign in to comment.