Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.0] - 2022-05-15

### Changed

- Existing AES256 state file encryption is no longer recommended.

### Added

- New state file encryption provider using `sops`. Currently integrated with PGP, AWS KMS and Hashicorp Vault.

## [0.0.19] - 2022-05-14

### Added
Expand Down
212 changes: 123 additions & 89 deletions README.md

Large diffs are not rendered by default.

54 changes: 31 additions & 23 deletions backend/crypt.go
Original file line number Diff line number Diff line change
@@ -1,41 +1,49 @@
package backend

import (
"fmt"
"os"

"golang.org/x/exp/maps"
"golang.org/x/exp/slices"

"github.com/plumber-cd/terraform-backend-git/crypt"
)

// getEncryptionPassphrase should check all possible config sources and return a state backend encryption key.
func getEncryptionPassphrase() string {
passphrase, _ := os.LookupEnv("TF_BACKEND_HTTP_ENCRYPTION_PASSPHRASE")
return passphrase
func getEncryptionProvider() (crypt.EncryptionProvider, error) {
provider, enabled := os.LookupEnv("TF_BACKEND_HTTP_ENCRYPTION_PROVIDER")
if enabled {
if !slices.Contains(maps.Keys(crypt.EncryptionProviders), provider) {
return nil, fmt.Errorf("Unknown encryption provider %q", provider)
}
return crypt.EncryptionProviders[provider], nil
}

// For backward compatibility
_, aesEnabled := os.LookupEnv("TF_BACKEND_HTTP_ENCRYPTION_PASSPHRASE")
if aesEnabled {
return crypt.EncryptionProviders["aes"], nil
}

return nil, nil
}

// encryptIfEnabled if encryption was enabled - return encrypted data, otherwise return the data as-is.
func encryptIfEnabled(state []byte) ([]byte, error) {
passphrase := getEncryptionPassphrase()

if passphrase == "" {
return state, nil
if ep, err := getEncryptionProvider(); err != nil {
return nil, err
} else if ep != nil {
return ep.Encrypt(state)
}

return crypt.EncryptAES(state, getEncryptionPassphrase())
return state, nil
}

// decryptIfEnabled if encryption was enabled - attempt to decrypt the data. Otherwise return it as-is.
// If decryption fails, it will assume encryption was not enabled previously for this state and return it as-is too.
// decryptIfEnabled if encryption was enabled - return decrypted data, otherwise return the data as-is.
func decryptIfEnabled(state []byte) ([]byte, error) {
passphrase := getEncryptionPassphrase()

if passphrase == "" {
return state, nil
}

buf, err := crypt.DecryptAES(state, getEncryptionPassphrase())
if err != nil && err.Error() == "cipher: message authentication failed" {
// Assumei t wasn't previously encrypted, return as-is
return state, nil
if ep, err := getEncryptionProvider(); err != nil {
return nil, err
} else if ep != nil {
return ep.Decrypt(state)
}
return buf, err
return state, nil
}
30 changes: 30 additions & 0 deletions cmd/docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cmd

import (
"log"
"os"

"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)

func init() {
rootCmd.AddCommand(docsCmd)
}

var docsCmd = &cobra.Command{
Use: "docs",
Short: "Generate docs",
Long: `Uses Cobra to generate CLI docs`,
Run: func(cmd *cobra.Command, args []string) {
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}

err = doc.GenMarkdownTree(rootCmd, cwd)
if err != nil {
log.Fatal(err)
}
},
}
115 changes: 115 additions & 0 deletions crypt/aes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package crypt

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"io"
"os"
)

func init() {
EncryptionProviders["aes"] = &AESEncryptionProvider{}
}

var (
ErrEncryptionPassphraseNotSet = errors.New("TF_BACKEND_HTTP_ENCRYPTION_PASSPHRASE was not set")
)

type AESEncryptionProvider struct{}

// getEncryptionPassphrase should check all possible config sources and return a state backend encryption key.
func getEncryptionPassphrase() (string, error) {
passphrase, ok := os.LookupEnv("TF_BACKEND_HTTP_ENCRYPTION_PASSPHRASE")
if !ok {
return "", ErrEncryptionPassphraseNotSet
}
return passphrase, nil
}

// createAesCipher uses this passphrase and creates a cipher from it's md5 hash
func createAesCipher(passphrase string) (cipher.Block, error) {
key, err := MD5(passphrase)
if err != nil {
return nil, err
}

block, err := aes.NewCipher([]byte(key))
if err != nil {
return nil, err
}

return block, nil
}

// createGCM will create new GCM for a given passphrase with the key calculated by createAesCipher.
func createGCM(passphrase string) (cipher.AEAD, error) {
block, err := createAesCipher(passphrase)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

return gcm, nil
}

// Encrypt will encrypt the data in buffer and return encrypted result.
// For a key it will use md5 hash from the passphrase.
func (p *AESEncryptionProvider) Encrypt(data []byte) ([]byte, error) {
passphrase, err := getEncryptionPassphrase()
if err != nil {
return nil, err
}

var ciphertext []byte

gcm, err := createGCM(passphrase)
if err != nil {
return ciphertext, err
}

nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return ciphertext, err
}

ciphertext = gcm.Seal(nonce, nonce, data, nil)
return ciphertext, nil
}

// Decrypt will decrypt the data in buffer.
// For a key it will use md5 hash from the passphrase.
func (p *AESEncryptionProvider) Decrypt(data []byte) ([]byte, error) {
passphrase, err := getEncryptionPassphrase()
if err != nil {
if err == ErrEncryptionPassphraseNotSet {
return data, nil
}
return nil, err
}

var plaintext []byte

gcm, err := createGCM(passphrase)
if err != nil {
return plaintext, err
}

nonceSize := gcm.NonceSize()
nonce, ciphertext := data[:nonceSize], data[nonceSize:]

result, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
if err.Error() == "cipher: message authentication failed" {
// Assume it wasn't previously encrypted, return as-is
return data, nil
}
return nil, err
}
return result, nil
}
69 changes: 0 additions & 69 deletions crypt/crypt.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package crypt

import (
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"encoding/hex"
"io"
)

// MD5 returns an md5 hash for a given string
Expand All @@ -17,68 +13,3 @@ func MD5(key string) (string, error) {
}
return hex.EncodeToString(hasher.Sum(nil)), nil
}

// createAesCipher uses this passphrase and creates a cipher from it's md5 hash
func createAesCipher(passphrase string) (cipher.Block, error) {
key, err := MD5(passphrase)
if err != nil {
return nil, err
}

block, err := aes.NewCipher([]byte(key))
if err != nil {
return nil, err
}

return block, nil
}

// createGCM will create new GCM for a given passphrase with the key calculated by createAesCipher.
func createGCM(passphrase string) (cipher.AEAD, error) {
block, err := createAesCipher(passphrase)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

return gcm, nil
}

// EncryptAES will encrypt the data in buffer and return encrypted result.
// For a key it will use md5 hash from the passphrase provided.
func EncryptAES(data []byte, passphrase string) ([]byte, error) {
var ciphertext []byte

gcm, err := createGCM(passphrase)
if err != nil {
return ciphertext, err
}

nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return ciphertext, err
}

ciphertext = gcm.Seal(nonce, nonce, data, nil)
return ciphertext, nil
}

// DecryptAES will decrypt the data in buffer.
// For a key it will use md5 hash from the passphrase provided.
func DecryptAES(data []byte, passphrase string) ([]byte, error) {
var plaintext []byte

gcm, err := createGCM(passphrase)
if err != nil {
return plaintext, err
}

nonceSize := gcm.NonceSize()
nonce, ciphertext := data[:nonceSize], data[nonceSize:]

return gcm.Open(nil, nonce, ciphertext, nil)
}
8 changes: 8 additions & 0 deletions crypt/encryption_providers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package crypt

type EncryptionProvider interface {
Encrypt([]byte) ([]byte, error)
Decrypt([]byte) ([]byte, error)
}

var EncryptionProviders = make(map[string]EncryptionProvider)
Loading