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 }