github.com/opentofu/opentofu@v1.7.1/internal/encryption/base.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package encryption
     7  
     8  import (
     9  	"encoding/json"
    10  	"fmt"
    11  
    12  	"github.com/opentofu/opentofu/internal/encryption/config"
    13  	"github.com/opentofu/opentofu/internal/encryption/keyprovider"
    14  	"github.com/opentofu/opentofu/internal/encryption/method"
    15  	"github.com/opentofu/opentofu/internal/encryption/method/unencrypted"
    16  
    17  	"github.com/hashicorp/hcl/v2"
    18  )
    19  
    20  const (
    21  	encryptionVersion = "v0"
    22  )
    23  
    24  type baseEncryption struct {
    25  	enc        *encryption
    26  	target     *config.TargetConfig
    27  	enforced   bool
    28  	name       string
    29  	encMethods []method.Method
    30  	encMeta    map[keyprovider.Addr][]byte
    31  }
    32  
    33  func newBaseEncryption(enc *encryption, target *config.TargetConfig, enforced bool, name string) (*baseEncryption, hcl.Diagnostics) {
    34  	base := &baseEncryption{
    35  		enc:      enc,
    36  		target:   target,
    37  		enforced: enforced,
    38  		name:     name,
    39  		encMeta:  make(map[keyprovider.Addr][]byte),
    40  	}
    41  	// Setup the encryptor
    42  	//
    43  	//     Instead of creating new encryption key data for each call to encrypt, we use the same encryptor for the given application (statefile or planfile).
    44  	//
    45  	// Why do we do this?
    46  	//
    47  	//   This allows us to always be in a state where we can encrypt data, which is particularly important when dealing with crashes. If the network is severed
    48  	//   mid-apply, we still need to be able to write an encrypted errored.tfstate or dump to stdout. Additionally it reduces the overhead of encryption in
    49  	//   general, as well as reducing cloud key provider costs.
    50  	//
    51  	// What are the security implications?
    52  	//
    53  	//   Plan file flow is fairly simple and is not impacted by this change. It only ever calls encrypt once at the end of plan generation.
    54  	//
    55  	//   State file is a bit more complex. The encrypted local state file (terraform.tfstate, .terraform.tfstate) will be written with the same
    56  	//   keys as any remote state. These files should be identical, which will make debugging easier.
    57  	//
    58  	//   The major concern with this is that many of the encryption methods used have a limit to how much data a key can safely encrypt. Pbkdf2 for example
    59  	//   has a limit of around 64GB before exhaustion is approached. Writing to the two local and one remote location specified above could not
    60  	//   approach that limit. However the cached state file (.terraform/terraform.tfstate) is persisted every 30 seconds during long applies. For an
    61  	//   extremely large state file (100MB) it would take an apply of over 5 hours to come close to the 64GB limit of pbkdf2 with some malicious actor recording
    62  	//   every single change to the filesystem (or inspecting deleted blocks).
    63  	//
    64  	// What other benfits does this provide?
    65  	//
    66  	//   This performs a e2e validation run of the config -> methods flow. It serves as a validation step and allows us to return detailed
    67  	//   diagnostics here and simple errors in the decrypt function below.
    68  	//
    69  	methods, diags := base.buildTargetMethods(base.encMeta)
    70  	base.encMethods = methods
    71  
    72  	return base, diags
    73  }
    74  
    75  type basedata struct {
    76  	Meta    map[keyprovider.Addr][]byte `json:"meta"`
    77  	Data    []byte                      `json:"encrypted_data"`
    78  	Version string                      `json:"encryption_version"` // This is both a sigil for a valid encrypted payload and a future compatability field
    79  }
    80  
    81  func IsEncryptionPayload(data []byte) (bool, error) {
    82  	es := basedata{}
    83  	err := json.Unmarshal(data, &es)
    84  	if err != nil {
    85  		return false, err
    86  	}
    87  
    88  	// This could be extended with full version checking later on
    89  	return es.Version != "", nil
    90  }
    91  
    92  func (s *baseEncryption) encrypt(data []byte, enhance func(basedata) interface{}) ([]byte, error) {
    93  	// buildTargetMethods above guarantees that there will be at least one encryption method.  They are not optional in the common target
    94  	// block, which is required to get to this code.
    95  	encryptor := s.encMethods[0]
    96  
    97  	if unencrypted.Is(encryptor) {
    98  		// ensure that the method is defined when Enforced is true
    99  		if s.enforced {
   100  			return nil, fmt.Errorf("unable to use unencrypted method for %q when enforced = true", s.name)
   101  		}
   102  		return data, nil
   103  	}
   104  
   105  	encd, err := encryptor.Encrypt(data)
   106  	if err != nil {
   107  		return nil, fmt.Errorf("encryption failed for %s: %w", s.name, err)
   108  	}
   109  
   110  	es := basedata{
   111  		Version: encryptionVersion,
   112  		Meta:    s.encMeta,
   113  		Data:    encd,
   114  	}
   115  	jsond, err := json.Marshal(enhance(es))
   116  	if err != nil {
   117  		return nil, fmt.Errorf("unable to encode encrypted data as json: %w", err)
   118  	}
   119  
   120  	return jsond, nil
   121  }
   122  
   123  // TODO Find a way to make these errors actionable / clear
   124  func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]byte, error) {
   125  	es := basedata{}
   126  	err := json.Unmarshal(data, &es)
   127  
   128  	if len(es.Version) == 0 || err != nil {
   129  		// Not a valid payload, might be already decrypted
   130  		verr := validator(data)
   131  		if verr != nil {
   132  			// Nope, just bad input
   133  
   134  			// Return the outer json error if we have one
   135  			if err != nil {
   136  				return nil, fmt.Errorf("invalid data format for decryption: %w, %w", err, verr)
   137  			}
   138  
   139  			// Must have been invalid json payload
   140  			return nil, fmt.Errorf("unable to determine data structure during decryption: %w", verr)
   141  		}
   142  
   143  		// Yep, it's already decrypted
   144  		for _, method := range s.encMethods {
   145  			if unencrypted.Is(method) {
   146  				if s.enforced {
   147  					return nil, fmt.Errorf("unable to use unencrypted method when enforced = true")
   148  				}
   149  				return data, nil
   150  			}
   151  		}
   152  		return nil, fmt.Errorf("encountered unencrypted payload without unencrypted method configured")
   153  	}
   154  
   155  	if es.Version != encryptionVersion {
   156  		return nil, fmt.Errorf("invalid encrypted payload version: %s != %s", es.Version, encryptionVersion)
   157  	}
   158  
   159  	// TODO Discuss if we should potentially cache this based on a json-encoded version of es.Meta and reduce overhead dramatically
   160  	methods, diags := s.buildTargetMethods(es.Meta)
   161  	if diags.HasErrors() {
   162  		// This cast to error here is safe as we know that at least one error exists
   163  		// This is also quite unlikely to happen as the constructor already has checked this code path
   164  		return nil, diags
   165  	}
   166  
   167  	errs := make([]error, 0)
   168  	for _, method := range methods {
   169  		if unencrypted.Is(method) {
   170  			// Not applicable
   171  			continue
   172  		}
   173  		uncd, err := method.Decrypt(es.Data)
   174  		if err == nil {
   175  			// Success
   176  			return uncd, nil
   177  		}
   178  		// Record the failure
   179  		errs = append(errs, fmt.Errorf("attempted decryption failed for %s: %w", s.name, err))
   180  	}
   181  
   182  	// This is good enough for now until we have better/distinct errors
   183  	errMessage := "decryption failed for all provided methods: "
   184  	sep := ""
   185  	for _, err := range errs {
   186  		errMessage += err.Error() + sep
   187  		sep = "\n"
   188  	}
   189  	return nil, fmt.Errorf(errMessage)
   190  }