Skip to content

Commit 114016e

Browse files
authored
Add sops (#26)
* Initial SOPS implementation using PGP * Add Hashicorp Vault to sops * Add AWS STS * Docs * Changelog * Close #11
1 parent 007ea82 commit 114016e

25 files changed

+1457
-181
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [0.1.0] - 2022-05-15
10+
11+
### Changed
12+
13+
- Existing AES256 state file encryption is no longer recommended.
14+
15+
### Added
16+
17+
- New state file encryption provider using `sops`. Currently integrated with PGP, AWS KMS and Hashicorp Vault.
18+
919
## [0.0.19] - 2022-05-14
1020

1121
### Added

README.md

Lines changed: 123 additions & 89 deletions
Large diffs are not rendered by default.

backend/crypt.go

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,49 @@
11
package backend
22

33
import (
4+
"fmt"
45
"os"
56

7+
"golang.org/x/exp/maps"
8+
"golang.org/x/exp/slices"
9+
610
"github.com/plumber-cd/terraform-backend-git/crypt"
711
)
812

9-
// getEncryptionPassphrase should check all possible config sources and return a state backend encryption key.
10-
func getEncryptionPassphrase() string {
11-
passphrase, _ := os.LookupEnv("TF_BACKEND_HTTP_ENCRYPTION_PASSPHRASE")
12-
return passphrase
13+
func getEncryptionProvider() (crypt.EncryptionProvider, error) {
14+
provider, enabled := os.LookupEnv("TF_BACKEND_HTTP_ENCRYPTION_PROVIDER")
15+
if enabled {
16+
if !slices.Contains(maps.Keys(crypt.EncryptionProviders), provider) {
17+
return nil, fmt.Errorf("Unknown encryption provider %q", provider)
18+
}
19+
return crypt.EncryptionProviders[provider], nil
20+
}
21+
22+
// For backward compatibility
23+
_, aesEnabled := os.LookupEnv("TF_BACKEND_HTTP_ENCRYPTION_PASSPHRASE")
24+
if aesEnabled {
25+
return crypt.EncryptionProviders["aes"], nil
26+
}
27+
28+
return nil, nil
1329
}
1430

1531
// encryptIfEnabled if encryption was enabled - return encrypted data, otherwise return the data as-is.
1632
func encryptIfEnabled(state []byte) ([]byte, error) {
17-
passphrase := getEncryptionPassphrase()
18-
19-
if passphrase == "" {
20-
return state, nil
33+
if ep, err := getEncryptionProvider(); err != nil {
34+
return nil, err
35+
} else if ep != nil {
36+
return ep.Encrypt(state)
2137
}
22-
23-
return crypt.EncryptAES(state, getEncryptionPassphrase())
38+
return state, nil
2439
}
2540

26-
// decryptIfEnabled if encryption was enabled - attempt to decrypt the data. Otherwise return it as-is.
27-
// If decryption fails, it will assume encryption was not enabled previously for this state and return it as-is too.
41+
// decryptIfEnabled if encryption was enabled - return decrypted data, otherwise return the data as-is.
2842
func decryptIfEnabled(state []byte) ([]byte, error) {
29-
passphrase := getEncryptionPassphrase()
30-
31-
if passphrase == "" {
32-
return state, nil
33-
}
34-
35-
buf, err := crypt.DecryptAES(state, getEncryptionPassphrase())
36-
if err != nil && err.Error() == "cipher: message authentication failed" {
37-
// Assumei t wasn't previously encrypted, return as-is
38-
return state, nil
43+
if ep, err := getEncryptionProvider(); err != nil {
44+
return nil, err
45+
} else if ep != nil {
46+
return ep.Decrypt(state)
3947
}
40-
return buf, err
48+
return state, nil
4149
}

cmd/docs.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package cmd
2+
3+
import (
4+
"log"
5+
"os"
6+
7+
"github.com/spf13/cobra"
8+
"github.com/spf13/cobra/doc"
9+
)
10+
11+
func init() {
12+
rootCmd.AddCommand(docsCmd)
13+
}
14+
15+
var docsCmd = &cobra.Command{
16+
Use: "docs",
17+
Short: "Generate docs",
18+
Long: `Uses Cobra to generate CLI docs`,
19+
Run: func(cmd *cobra.Command, args []string) {
20+
cwd, err := os.Getwd()
21+
if err != nil {
22+
log.Fatal(err)
23+
}
24+
25+
err = doc.GenMarkdownTree(rootCmd, cwd)
26+
if err != nil {
27+
log.Fatal(err)
28+
}
29+
},
30+
}

crypt/aes.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package crypt
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"crypto/rand"
7+
"errors"
8+
"io"
9+
"os"
10+
)
11+
12+
func init() {
13+
EncryptionProviders["aes"] = &AESEncryptionProvider{}
14+
}
15+
16+
var (
17+
ErrEncryptionPassphraseNotSet = errors.New("TF_BACKEND_HTTP_ENCRYPTION_PASSPHRASE was not set")
18+
)
19+
20+
type AESEncryptionProvider struct{}
21+
22+
// getEncryptionPassphrase should check all possible config sources and return a state backend encryption key.
23+
func getEncryptionPassphrase() (string, error) {
24+
passphrase, ok := os.LookupEnv("TF_BACKEND_HTTP_ENCRYPTION_PASSPHRASE")
25+
if !ok {
26+
return "", ErrEncryptionPassphraseNotSet
27+
}
28+
return passphrase, nil
29+
}
30+
31+
// createAesCipher uses this passphrase and creates a cipher from it's md5 hash
32+
func createAesCipher(passphrase string) (cipher.Block, error) {
33+
key, err := MD5(passphrase)
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
block, err := aes.NewCipher([]byte(key))
39+
if err != nil {
40+
return nil, err
41+
}
42+
43+
return block, nil
44+
}
45+
46+
// createGCM will create new GCM for a given passphrase with the key calculated by createAesCipher.
47+
func createGCM(passphrase string) (cipher.AEAD, error) {
48+
block, err := createAesCipher(passphrase)
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
gcm, err := cipher.NewGCM(block)
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
return gcm, nil
59+
}
60+
61+
// Encrypt will encrypt the data in buffer and return encrypted result.
62+
// For a key it will use md5 hash from the passphrase.
63+
func (p *AESEncryptionProvider) Encrypt(data []byte) ([]byte, error) {
64+
passphrase, err := getEncryptionPassphrase()
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
var ciphertext []byte
70+
71+
gcm, err := createGCM(passphrase)
72+
if err != nil {
73+
return ciphertext, err
74+
}
75+
76+
nonce := make([]byte, gcm.NonceSize())
77+
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
78+
return ciphertext, err
79+
}
80+
81+
ciphertext = gcm.Seal(nonce, nonce, data, nil)
82+
return ciphertext, nil
83+
}
84+
85+
// Decrypt will decrypt the data in buffer.
86+
// For a key it will use md5 hash from the passphrase.
87+
func (p *AESEncryptionProvider) Decrypt(data []byte) ([]byte, error) {
88+
passphrase, err := getEncryptionPassphrase()
89+
if err != nil {
90+
if err == ErrEncryptionPassphraseNotSet {
91+
return data, nil
92+
}
93+
return nil, err
94+
}
95+
96+
var plaintext []byte
97+
98+
gcm, err := createGCM(passphrase)
99+
if err != nil {
100+
return plaintext, err
101+
}
102+
103+
nonceSize := gcm.NonceSize()
104+
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
105+
106+
result, err := gcm.Open(nil, nonce, ciphertext, nil)
107+
if err != nil {
108+
if err.Error() == "cipher: message authentication failed" {
109+
// Assume it wasn't previously encrypted, return as-is
110+
return data, nil
111+
}
112+
return nil, err
113+
}
114+
return result, nil
115+
}

crypt/crypt.go

Lines changed: 0 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package crypt
22

33
import (
4-
"crypto/aes"
5-
"crypto/cipher"
64
"crypto/md5"
7-
"crypto/rand"
85
"encoding/hex"
9-
"io"
106
)
117

128
// MD5 returns an md5 hash for a given string
@@ -17,68 +13,3 @@ func MD5(key string) (string, error) {
1713
}
1814
return hex.EncodeToString(hasher.Sum(nil)), nil
1915
}
20-
21-
// createAesCipher uses this passphrase and creates a cipher from it's md5 hash
22-
func createAesCipher(passphrase string) (cipher.Block, error) {
23-
key, err := MD5(passphrase)
24-
if err != nil {
25-
return nil, err
26-
}
27-
28-
block, err := aes.NewCipher([]byte(key))
29-
if err != nil {
30-
return nil, err
31-
}
32-
33-
return block, nil
34-
}
35-
36-
// createGCM will create new GCM for a given passphrase with the key calculated by createAesCipher.
37-
func createGCM(passphrase string) (cipher.AEAD, error) {
38-
block, err := createAesCipher(passphrase)
39-
if err != nil {
40-
return nil, err
41-
}
42-
43-
gcm, err := cipher.NewGCM(block)
44-
if err != nil {
45-
return nil, err
46-
}
47-
48-
return gcm, nil
49-
}
50-
51-
// EncryptAES will encrypt the data in buffer and return encrypted result.
52-
// For a key it will use md5 hash from the passphrase provided.
53-
func EncryptAES(data []byte, passphrase string) ([]byte, error) {
54-
var ciphertext []byte
55-
56-
gcm, err := createGCM(passphrase)
57-
if err != nil {
58-
return ciphertext, err
59-
}
60-
61-
nonce := make([]byte, gcm.NonceSize())
62-
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
63-
return ciphertext, err
64-
}
65-
66-
ciphertext = gcm.Seal(nonce, nonce, data, nil)
67-
return ciphertext, nil
68-
}
69-
70-
// DecryptAES will decrypt the data in buffer.
71-
// For a key it will use md5 hash from the passphrase provided.
72-
func DecryptAES(data []byte, passphrase string) ([]byte, error) {
73-
var plaintext []byte
74-
75-
gcm, err := createGCM(passphrase)
76-
if err != nil {
77-
return plaintext, err
78-
}
79-
80-
nonceSize := gcm.NonceSize()
81-
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
82-
83-
return gcm.Open(nil, nonce, ciphertext, nil)
84-
}

crypt/encryption_providers.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package crypt
2+
3+
type EncryptionProvider interface {
4+
Encrypt([]byte) ([]byte, error)
5+
Decrypt([]byte) ([]byte, error)
6+
}
7+
8+
var EncryptionProviders = make(map[string]EncryptionProvider)

0 commit comments

Comments
 (0)