mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
State Encryption Error Handling / Diagnostics (#1294)
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
parent
2485299cd4
commit
997e5fa46e
@ -27,13 +27,17 @@ type baseEncryption struct {
|
|||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBaseEncryption(enc *encryption, target *config.TargetConfig, enforced bool, name string) *baseEncryption {
|
func newBaseEncryption(enc *encryption, target *config.TargetConfig, enforced bool, name string) (*baseEncryption, hcl.Diagnostics) {
|
||||||
return &baseEncryption{
|
base := &baseEncryption{
|
||||||
enc: enc,
|
enc: enc,
|
||||||
target: target,
|
target: target,
|
||||||
enforced: enforced,
|
enforced: enforced,
|
||||||
name: name,
|
name: name,
|
||||||
}
|
}
|
||||||
|
// This performs a e2e validation run of the config -> methods flow. It serves as a validation step and allows us to
|
||||||
|
// return detailed diagnostics here and simple errors below
|
||||||
|
_, diags := base.buildTargetMethods(make(map[keyprovider.Addr][]byte))
|
||||||
|
return base, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
type basedata struct {
|
type basedata struct {
|
||||||
@ -42,7 +46,8 @@ type basedata struct {
|
|||||||
Version string `json:"encryption_version"` // This is both a sigil for a valid encrypted payload and a future compatability field
|
Version string `json:"encryption_version"` // This is both a sigil for a valid encrypted payload and a future compatability field
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *baseEncryption) encrypt(data []byte) ([]byte, hcl.Diagnostics) {
|
func (s *baseEncryption) encrypt(data []byte) ([]byte, error) {
|
||||||
|
// No configuration provided, don't do anything
|
||||||
if s.target == nil {
|
if s.target == nil {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
@ -55,6 +60,8 @@ func (s *baseEncryption) encrypt(data []byte) ([]byte, hcl.Diagnostics) {
|
|||||||
// Mutates es.Meta
|
// Mutates es.Meta
|
||||||
methods, diags := s.buildTargetMethods(es.Meta)
|
methods, diags := s.buildTargetMethods(es.Meta)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
|
// This cast to error here is safe as we know that at least one error exists
|
||||||
|
// This is also quite unlikely to happen as the constructor already has checked this code path
|
||||||
return nil, diags
|
return nil, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,40 +73,27 @@ func (s *baseEncryption) encrypt(data []byte) ([]byte, hcl.Diagnostics) {
|
|||||||
if encryptor == nil {
|
if encryptor == nil {
|
||||||
// ensure that the method is defined when Enforced is true
|
// ensure that the method is defined when Enforced is true
|
||||||
if s.enforced {
|
if s.enforced {
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
return nil, fmt.Errorf("encryption of %q is enforced, and therefore requires a method to be provided", s.name)
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Encryption method required",
|
|
||||||
Detail: fmt.Sprintf("%q is enforced, and therefore requires a method to be provided", s.name),
|
|
||||||
})
|
|
||||||
return nil, diags
|
|
||||||
}
|
}
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
encd, err := encryptor.Encrypt(data)
|
encd, err := encryptor.Encrypt(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, append(diags, &hcl.Diagnostic{
|
return nil, fmt.Errorf("encryption failed for %s: %w", s.name, err)
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Encryption failed for " + s.name,
|
|
||||||
Detail: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
es.Data = encd
|
es.Data = encd
|
||||||
jsond, err := json.Marshal(es)
|
jsond, err := json.Marshal(es)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, append(diags, &hcl.Diagnostic{
|
return nil, fmt.Errorf("unable to encode encrypted data as json: %w", err)
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unable to encode encrypted data as json",
|
|
||||||
Detail: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsond, diags
|
return jsond, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Find a way to make these errors actionable / clear
|
// TODO Find a way to make these errors actionable / clear
|
||||||
func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]byte, hcl.Diagnostics) {
|
func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]byte, error) {
|
||||||
if s.target == nil {
|
if s.target == nil {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
@ -107,90 +101,68 @@ func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]b
|
|||||||
es := basedata{}
|
es := basedata{}
|
||||||
err := json.Unmarshal(data, &es)
|
err := json.Unmarshal(data, &es)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, hcl.Diagnostics{&hcl.Diagnostic{
|
return nil, fmt.Errorf("invalid data format for decryption: %w", err)
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid data format for decryption",
|
|
||||||
Detail: err.Error(),
|
|
||||||
}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(es.Version) == 0 {
|
if len(es.Version) == 0 {
|
||||||
// Not a valid payload, might be already decrypted
|
// Not a valid payload, might be already decrypted
|
||||||
err = validator(data)
|
err = validator(data)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
// Yep, it's already decrypted
|
|
||||||
return data, nil
|
|
||||||
} else {
|
|
||||||
// Nope, just bad input
|
// Nope, just bad input
|
||||||
return nil, hcl.Diagnostics{&hcl.Diagnostic{
|
return nil, fmt.Errorf("unable to determine data structure during decryption: %w", err)
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unable to determine data structure during decryption",
|
|
||||||
}}
|
|
||||||
}
|
}
|
||||||
|
// Yep, it's already decrypted
|
||||||
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if es.Version != encryptionVersion {
|
if es.Version != encryptionVersion {
|
||||||
return nil, hcl.Diagnostics{&hcl.Diagnostic{
|
return nil, fmt.Errorf("invalid encrypted payload version: %s != %s", es.Version, encryptionVersion)
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid encrypted payload version",
|
|
||||||
Detail: fmt.Sprintf("%s != %s", es.Version, encryptionVersion),
|
|
||||||
}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
methods, diags := s.buildTargetMethods(es.Meta)
|
methods, diags := s.buildTargetMethods(es.Meta)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
|
// This cast to error here is safe as we know that at least one error exists
|
||||||
|
// This is also quite unlikely to happen as the constructor already has checked this code path
|
||||||
return nil, diags
|
return nil, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(methods) == 0 {
|
if len(methods) == 0 {
|
||||||
err = validator(data)
|
err = validator(data)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
// No methods/fallbacks specified and data is valid payload
|
|
||||||
return data, diags
|
|
||||||
} else {
|
|
||||||
// TODO improve this error message
|
// TODO improve this error message
|
||||||
return nil, append(diags, &hcl.Diagnostic{
|
return nil, err
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
// No methods/fallbacks specified and data is valid payload
|
||||||
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var methodDiags hcl.Diagnostics
|
errs := make([]error, 0)
|
||||||
for _, method := range methods {
|
for _, method := range methods {
|
||||||
if method == nil {
|
if method == nil {
|
||||||
// No method specified for this target
|
// No method specified for this target
|
||||||
err = validator(data)
|
err = validator(data)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return data, diags
|
return data, nil
|
||||||
}
|
}
|
||||||
// toDO improve this error message
|
// TODO improve this error message
|
||||||
methodDiags = append(methodDiags, &hcl.Diagnostic{
|
errs = append(errs, fmt.Errorf("payload is not already decrypted: %w", err))
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Attempted decryption failed for " + s.name,
|
|
||||||
Detail: err.Error(),
|
|
||||||
})
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
uncd, err := method.Decrypt(es.Data)
|
uncd, err := method.Decrypt(es.Data)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Success
|
// Success
|
||||||
return uncd, diags
|
return uncd, nil
|
||||||
}
|
}
|
||||||
// Record the failure
|
// Record the failure
|
||||||
methodDiags = append(methodDiags, &hcl.Diagnostic{
|
errs = append(errs, fmt.Errorf("attempted decryption failed for %s: %w", s.name, err))
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Attempted decryption failed for " + s.name,
|
|
||||||
Detail: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record the overall failure
|
// This is good enough for now until we have better/distinct errors
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
errMessage := "decryption failed for all provided methods: "
|
||||||
Severity: hcl.DiagError,
|
sep := ""
|
||||||
Summary: "Decryption failed",
|
for _, err := range errs {
|
||||||
Detail: "All methods of decryption provided failed for " + s.name,
|
errMessage += err.Error() + sep
|
||||||
})
|
sep = "\n"
|
||||||
|
}
|
||||||
return nil, append(diags, methodDiags...)
|
return nil, fmt.Errorf(errMessage)
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
package encryption
|
package encryption
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/opentofu/opentofu/internal/encryption/config"
|
"github.com/opentofu/opentofu/internal/encryption/config"
|
||||||
"github.com/opentofu/opentofu/internal/encryption/registry"
|
"github.com/opentofu/opentofu/internal/encryption/registry"
|
||||||
)
|
)
|
||||||
@ -14,17 +15,17 @@ import (
|
|||||||
// purpose. If no encryption configuration is present, it should return a pass through method that doesn't do anything.
|
// purpose. If no encryption configuration is present, it should return a pass through method that doesn't do anything.
|
||||||
type Encryption interface {
|
type Encryption interface {
|
||||||
// StateFile produces a StateEncryption overlay for encrypting and decrypting state files for local storage.
|
// StateFile produces a StateEncryption overlay for encrypting and decrypting state files for local storage.
|
||||||
StateFile() StateEncryption
|
StateFile() (StateEncryption, hcl.Diagnostics)
|
||||||
|
|
||||||
// PlanFile produces a PlanEncryption overlay for encrypting and decrypting plan files.
|
// PlanFile produces a PlanEncryption overlay for encrypting and decrypting plan files.
|
||||||
PlanFile() PlanEncryption
|
PlanFile() (PlanEncryption, hcl.Diagnostics)
|
||||||
|
|
||||||
// Backend produces a StateEncryption overlay for storing state files on remote backends, such as an S3 bucket.
|
// Backend produces a StateEncryption overlay for storing state files on remote backends, such as an S3 bucket.
|
||||||
Backend() StateEncryption
|
Backend() (StateEncryption, hcl.Diagnostics)
|
||||||
|
|
||||||
// RemoteState produces a ReadOnlyStateEncryption for reading remote states using the terraform_remote_state data
|
// RemoteState produces a ReadOnlyStateEncryption for reading remote states using the terraform_remote_state data
|
||||||
// source.
|
// source.
|
||||||
RemoteState(string) ReadOnlyStateEncryption
|
RemoteState(string) (ReadOnlyStateEncryption, hcl.Diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
type encryption struct {
|
type encryption struct {
|
||||||
@ -41,34 +42,27 @@ func New(reg registry.Registry, cfg *config.Config) Encryption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *encryption) StateFile() StateEncryption {
|
func (e *encryption) StateFile() (StateEncryption, hcl.Diagnostics) {
|
||||||
return &stateEncryption{
|
return newStateEncryption(e, e.cfg.StateFile.AsTargetConfig(), e.cfg.StateFile.Enforced, "statefile")
|
||||||
base: newBaseEncryption(e, e.cfg.StateFile.AsTargetConfig(), e.cfg.StateFile.Enforced, "statefile"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *encryption) PlanFile() PlanEncryption {
|
func (e *encryption) PlanFile() (PlanEncryption, hcl.Diagnostics) {
|
||||||
return &planEncryption{
|
return newPlanEncryption(e, e.cfg.PlanFile.AsTargetConfig(), e.cfg.PlanFile.Enforced, "planfile")
|
||||||
base: newBaseEncryption(e, e.cfg.PlanFile.AsTargetConfig(), e.cfg.PlanFile.Enforced, "planfile"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *encryption) Backend() StateEncryption {
|
func (e *encryption) Backend() (StateEncryption, hcl.Diagnostics) {
|
||||||
return &stateEncryption{
|
return newStateEncryption(e, e.cfg.StateFile.AsTargetConfig(), e.cfg.StateFile.Enforced, "backend")
|
||||||
base: newBaseEncryption(e, e.cfg.StateFile.AsTargetConfig(), e.cfg.StateFile.Enforced, "backend"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *encryption) RemoteState(name string) ReadOnlyStateEncryption {
|
func (e *encryption) RemoteState(name string) (ReadOnlyStateEncryption, hcl.Diagnostics) {
|
||||||
for _, remoteTarget := range e.cfg.Remote.Targets {
|
for _, remoteTarget := range e.cfg.Remote.Targets {
|
||||||
if remoteTarget.Name == name {
|
if remoteTarget.Name == name {
|
||||||
return &stateEncryption{
|
// TODO the addr here should be generated in one place.
|
||||||
// TODO the addr here should be generated in one place.
|
addr := "remote.remote_state_datasource." + remoteTarget.Name
|
||||||
base: newBaseEncryption(e, remoteTarget.AsTargetConfig(), false, "remote.remote_state_datasource."+remoteTarget.Name),
|
return newStateEncryption(
|
||||||
}
|
e, remoteTarget.AsTargetConfig(), false, addr,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &stateEncryption{
|
return newStateEncryption(e, e.cfg.Remote.Default, false, "remote.default")
|
||||||
base: newBaseEncryption(e, e.cfg.Remote.Default, false, "remote.default"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -64,11 +64,16 @@ func Example() {
|
|||||||
// Construct the encryption object
|
// Construct the encryption object
|
||||||
enc := encryption.New(reg, cfg)
|
enc := encryption.New(reg, cfg)
|
||||||
|
|
||||||
|
sfe, diags := enc.StateFile()
|
||||||
|
handleDiags(diags)
|
||||||
|
|
||||||
// Encrypt the data, for this example we will be using the string "test",
|
// Encrypt the data, for this example we will be using the string "test",
|
||||||
// but in a real world scenario this would be the plan file.
|
// but in a real world scenario this would be the plan file.
|
||||||
sourceData := []byte("test")
|
sourceData := []byte("test")
|
||||||
encrypted, diags := enc.StateFile().EncryptState(sourceData)
|
encrypted, err := sfe.EncryptState(sourceData)
|
||||||
handleDiags(diags)
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
if string(encrypted) == "test" {
|
if string(encrypted) == "test" {
|
||||||
panic("The data has not been encrypted!")
|
panic("The data has not been encrypted!")
|
||||||
@ -77,7 +82,7 @@ func Example() {
|
|||||||
println(string(encrypted))
|
println(string(encrypted))
|
||||||
|
|
||||||
// Decrypt
|
// Decrypt
|
||||||
decryptedState, err := enc.StateFile().DecryptState(encrypted)
|
decryptedState, err := sfe.DecryptState(encrypted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/opentofu/opentofu/internal/encryption/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PlanEncryption describes the methods that you can use for encrypting a plan file. Plan files are opaque values with
|
// PlanEncryption describes the methods that you can use for encrypting a plan file. Plan files are opaque values with
|
||||||
@ -27,7 +28,7 @@ type PlanEncryption interface {
|
|||||||
// Make sure that you pass a valid plan file as an input. Failing to provide a valid plan file may result in an
|
// Make sure that you pass a valid plan file as an input. Failing to provide a valid plan file may result in an
|
||||||
// error. However, output values may not be valid plan files and you should not pass the encrypted plan file to any
|
// error. However, output values may not be valid plan files and you should not pass the encrypted plan file to any
|
||||||
// additional functions that normally work with plan files.
|
// additional functions that normally work with plan files.
|
||||||
EncryptPlan([]byte) ([]byte, hcl.Diagnostics)
|
EncryptPlan([]byte) ([]byte, error)
|
||||||
|
|
||||||
// DecryptPlan decrypts an encrypted plan file.
|
// DecryptPlan decrypts an encrypted plan file.
|
||||||
//
|
//
|
||||||
@ -41,18 +42,23 @@ type PlanEncryption interface {
|
|||||||
//
|
//
|
||||||
// Pass a potentially encrypted plan file as an input, and you will receive the decrypted plan file or an error as
|
// Pass a potentially encrypted plan file as an input, and you will receive the decrypted plan file or an error as
|
||||||
// a result.
|
// a result.
|
||||||
DecryptPlan([]byte) ([]byte, hcl.Diagnostics)
|
DecryptPlan([]byte) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type planEncryption struct {
|
type planEncryption struct {
|
||||||
base *baseEncryption
|
base *baseEncryption
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p planEncryption) EncryptPlan(data []byte) ([]byte, hcl.Diagnostics) {
|
func newPlanEncryption(enc *encryption, target *config.TargetConfig, enforced bool, name string) (PlanEncryption, hcl.Diagnostics) {
|
||||||
|
base, diags := newBaseEncryption(enc, target, enforced, name)
|
||||||
|
return &planEncryption{base}, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p planEncryption) EncryptPlan(data []byte) ([]byte, error) {
|
||||||
return p.base.encrypt(data)
|
return p.base.encrypt(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p planEncryption) DecryptPlan(data []byte) ([]byte, hcl.Diagnostics) {
|
func (p planEncryption) DecryptPlan(data []byte) ([]byte, error) {
|
||||||
return p.base.decrypt(data, func(data []byte) error {
|
return p.base.decrypt(data, func(data []byte) error {
|
||||||
// Check magic bytes
|
// Check magic bytes
|
||||||
if len(data) < 4 || string(data[:4]) != "PK" {
|
if len(data) < 4 || string(data[:4]) != "PK" {
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/opentofu/opentofu/internal/encryption/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const StateEncryptionMarkerField = "encryption"
|
const StateEncryptionMarkerField = "encryption"
|
||||||
@ -32,7 +33,7 @@ type ReadOnlyStateEncryption interface {
|
|||||||
// function. Do not attempt to determine if the state file is encrypted as this function will take care of any
|
// function. Do not attempt to determine if the state file is encrypted as this function will take care of any
|
||||||
// and all encryption-related matters. After the function returns, use the returned byte array as a normal state
|
// and all encryption-related matters. After the function returns, use the returned byte array as a normal state
|
||||||
// file.
|
// file.
|
||||||
DecryptState([]byte) ([]byte, hcl.Diagnostics)
|
DecryptState([]byte) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StateEncryption describes the interface for encrypting state files.
|
// StateEncryption describes the interface for encrypting state files.
|
||||||
@ -56,18 +57,23 @@ type StateEncryption interface {
|
|||||||
// Pass in a valid JSON-serialized state file as an input and store the output. Note that you should not pass the
|
// Pass in a valid JSON-serialized state file as an input and store the output. Note that you should not pass the
|
||||||
// output to any additional functions that require a valid state file as it may not contain the fields typically
|
// output to any additional functions that require a valid state file as it may not contain the fields typically
|
||||||
// present in a state file.
|
// present in a state file.
|
||||||
EncryptState([]byte) ([]byte, hcl.Diagnostics)
|
EncryptState([]byte) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type stateEncryption struct {
|
type stateEncryption struct {
|
||||||
base *baseEncryption
|
base *baseEncryption
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stateEncryption) EncryptState(plainState []byte) ([]byte, hcl.Diagnostics) {
|
func newStateEncryption(enc *encryption, target *config.TargetConfig, enforced bool, name string) (StateEncryption, hcl.Diagnostics) {
|
||||||
|
base, diags := newBaseEncryption(enc, target, enforced, name)
|
||||||
|
return &stateEncryption{base}, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stateEncryption) EncryptState(plainState []byte) ([]byte, error) {
|
||||||
return s.base.encrypt(plainState)
|
return s.base.encrypt(plainState)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stateEncryption) DecryptState(encryptedState []byte) ([]byte, hcl.Diagnostics) {
|
func (s *stateEncryption) DecryptState(encryptedState []byte) ([]byte, error) {
|
||||||
return s.base.decrypt(encryptedState, func(data []byte) error {
|
return s.base.decrypt(encryptedState, func(data []byte) error {
|
||||||
tmp := struct {
|
tmp := struct {
|
||||||
FormatVersion string `json:"format_version"`
|
FormatVersion string `json:"format_version"`
|
||||||
|
Loading…
Reference in New Issue
Block a user