2021-05-18 02:02:00 +02:00
|
|
|
package keychain
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
2021-05-22 01:16:56 +02:00
|
|
|
"time"
|
2021-05-18 02:02:00 +02:00
|
|
|
|
|
|
|
"go.mozilla.org/sops/v3"
|
2021-05-22 01:16:56 +02:00
|
|
|
sopsaes "go.mozilla.org/sops/v3/aes"
|
2021-05-18 02:02:00 +02:00
|
|
|
sopsage "go.mozilla.org/sops/v3/age"
|
|
|
|
"go.mozilla.org/sops/v3/cmd/sops/common"
|
|
|
|
sopskeys "go.mozilla.org/sops/v3/keys"
|
|
|
|
sopsyaml "go.mozilla.org/sops/v3/stores/yaml"
|
|
|
|
"go.mozilla.org/sops/v3/version"
|
|
|
|
)
|
|
|
|
|
2021-05-22 01:16:56 +02:00
|
|
|
var (
|
|
|
|
cipher = sopsaes.NewCipher()
|
|
|
|
)
|
|
|
|
|
2021-05-18 02:02:00 +02:00
|
|
|
// setupEnv: hack to inject a SOPS env var for age
|
|
|
|
func setupEnv() error {
|
|
|
|
p, err := Path()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return os.Setenv("SOPS_AGE_KEY_FILE", p)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Encrypt data using SOPS with the AGE backend, using the provided public key
|
|
|
|
func Encrypt(ctx context.Context, path string, plaintext []byte, key string) ([]byte, error) {
|
|
|
|
if err := setupEnv(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
store := &sopsyaml.Store{}
|
|
|
|
branches, err := store.LoadPlainFile(plaintext)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
ageKeys, err := sopsage.MasterKeysFromRecipients(key)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
ageMasterKeys := make([]sopskeys.MasterKey, 0, len(ageKeys))
|
|
|
|
for _, k := range ageKeys {
|
|
|
|
ageMasterKeys = append(ageMasterKeys, k)
|
|
|
|
}
|
|
|
|
var group sops.KeyGroup
|
|
|
|
group = append(group, ageMasterKeys...)
|
|
|
|
|
|
|
|
tree := sops.Tree{
|
|
|
|
Branches: branches,
|
|
|
|
Metadata: sops.Metadata{
|
|
|
|
KeyGroups: []sops.KeyGroup{group},
|
|
|
|
EncryptedSuffix: "secret",
|
|
|
|
Version: version.Version,
|
|
|
|
},
|
|
|
|
FilePath: path,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a data key
|
|
|
|
dataKey, errs := tree.GenerateDataKey()
|
|
|
|
if len(errs) > 0 {
|
|
|
|
return nil, fmt.Errorf("error encrypting the data key with one or more master keys: %v", errs)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = common.EncryptTree(common.EncryptTreeOpts{
|
2021-05-22 01:16:56 +02:00
|
|
|
DataKey: dataKey, Tree: &tree, Cipher: cipher,
|
2021-05-18 02:02:00 +02:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return store.EmitEncryptedFile(tree)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reencrypt a file with new content using the same keys
|
|
|
|
func Reencrypt(_ context.Context, path string, plaintext []byte) ([]byte, error) {
|
|
|
|
if err := setupEnv(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
current, err := os.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load the encrypted file
|
|
|
|
store := &sopsyaml.Store{}
|
|
|
|
tree, err := store.LoadEncryptedFile(current)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the file with the new data
|
|
|
|
newBranches, err := store.LoadPlainFile(plaintext)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
tree.Branches = newBranches
|
|
|
|
|
|
|
|
// Re-encrypt the file
|
|
|
|
key, err := tree.Metadata.GetDataKey()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
err = common.EncryptTree(common.EncryptTreeOpts{
|
2021-05-22 01:16:56 +02:00
|
|
|
DataKey: key, Tree: &tree, Cipher: cipher,
|
2021-05-18 02:02:00 +02:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return store.EmitEncryptedFile(tree)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Decrypt data using sops
|
|
|
|
func Decrypt(_ context.Context, encrypted []byte) ([]byte, error) {
|
|
|
|
if err := setupEnv(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:16:56 +02:00
|
|
|
store := &sopsyaml.Store{}
|
|
|
|
|
|
|
|
// Load SOPS file and access the data key
|
|
|
|
tree, err := store.LoadEncryptedFile(encrypted)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
key, err := tree.Metadata.GetDataKey()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Decrypt the tree
|
|
|
|
mac, err := tree.Decrypt(key, cipher)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Compute the hash of the cleartext tree and compare it with
|
|
|
|
// the one that was stored in the document. If they match,
|
|
|
|
// integrity was preserved
|
|
|
|
originalMac, err := cipher.Decrypt(
|
|
|
|
tree.Metadata.MessageAuthenticationCode,
|
|
|
|
key,
|
|
|
|
tree.Metadata.LastModified.Format(time.RFC3339),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if originalMac != mac {
|
|
|
|
return nil, fmt.Errorf("failed to verify data integrity. expected mac %q, got %q", originalMac, mac)
|
|
|
|
}
|
|
|
|
|
|
|
|
return store.EmitPlainFile(tree.Branches)
|
2021-05-18 02:02:00 +02:00
|
|
|
}
|