1
0
mirror of https://github.com/getsops/sops.git synced 2026-02-05 12:45:21 +01:00

Refactored PGP and KMS into their own packages

This commit is contained in:
Adrian Utrilla
2016-08-11 11:44:00 -07:00
parent f5defbc629
commit 791cd693c2
5 changed files with 372 additions and 291 deletions

View File

@@ -1,275 +0,0 @@
package sops
import (
"bytes"
"encoding/base64"
"encoding/hex"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/howeyc/gopass"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
"io/ioutil"
"os"
"os/user"
"path"
"regexp"
"strings"
)
// KeySource provides a way to obtain the symmetric encryption key used by sops
type KeySource interface {
DecryptKeys() (string, error)
EncryptKeys(plaintext string) error
}
type KMS struct {
Arn string
Role string
EncryptedKey string
}
type KMSKeySource struct {
KMS []KMS
}
type GPG struct {
Fingerprint string
EncryptedKey string
}
type GPGKeySource struct {
GPG []GPG
}
// NewKMSKeySourceFromString takes a comma-separated string with KMS ARNs and returns a KMSKeySource
func NewKMSKeySourceFromString(arns string) KMSKeySource {
var kmss []KMS
arns = strings.Replace(arns, " ", "", -1)
for _, s := range strings.Split(arns, ",") {
roleIndex := strings.Index(s, "+arn:aws:iam::")
k := KMS{}
if roleIndex > 0 {
k.Arn = s[:roleIndex]
k.Role = s[roleIndex+1:]
} else {
k.Arn = s
}
kmss = append(kmss, k)
}
return KMSKeySource{KMS: kmss}
}
func (k KMS) createStsSession(config aws.Config, sess *session.Session) (*session.Session, error) {
hostname, err := os.Hostname()
if err != nil {
return nil, err
}
stsService := sts.New(sess)
name := "sops@" + hostname
out, err := stsService.AssumeRole(&sts.AssumeRoleInput{
RoleArn: &k.Role, RoleSessionName: &name})
if err != nil {
return nil, err
}
config.Credentials = credentials.NewStaticCredentials(*out.Credentials.AccessKeyId,
*out.Credentials.SecretAccessKey, *out.Credentials.SessionToken)
sess, err = session.NewSession(&config)
if err != nil {
return nil, err
}
return sess, nil
}
func (k KMS) createSession() (*session.Session, error) {
re := regexp.MustCompile(`^arn:aws:kms:(.+):([0-9]+):key/(.+)$`)
matches := re.FindStringSubmatch(k.Arn)
if matches == nil {
return nil, fmt.Errorf("No valid ARN found in %s", k.Arn)
}
config := aws.Config{Region: aws.String(matches[1])}
sess, err := session.NewSession(&config)
if err != nil {
return nil, err
}
if k.Role != "" {
return k.createStsSession(config, sess)
}
return sess, nil
}
func (k KMS) DecryptKey(encryptedKey string) (string, error) {
sess, err := k.createSession()
if err != nil {
return "", fmt.Errorf("Error creating AWS session: %v", err)
}
service := kms.New(sess)
decrypted, err := service.Decrypt(&kms.DecryptInput{CiphertextBlob: []byte(encryptedKey)})
if err != nil {
return "", fmt.Errorf("Error decrypting key: %v", err)
}
return string(decrypted.Plaintext), nil
}
func (ks KMSKeySource) DecryptKeys() (string, error) {
errors := make([]error, 1)
for _, kms := range ks.KMS {
encKey, err := base64.StdEncoding.DecodeString(kms.EncryptedKey)
if err != nil {
continue
}
key, err := kms.DecryptKey(string(encKey))
if err == nil {
return key, nil
}
errors = append(errors, err)
}
return "", fmt.Errorf("The key could not be decrypted with any KMS entries", errors)
}
func (ks KMSKeySource) EncryptKeys(plaintext string) error {
for i, _ := range ks.KMS {
sess, err := ks.KMS[i].createSession()
if err != nil {
return err
}
service := kms.New(sess)
out, err := service.Encrypt(&kms.EncryptInput{Plaintext: []byte(plaintext), KeyId: &ks.KMS[i].Arn})
if err != nil {
return err
}
ks.KMS[i].EncryptedKey = base64.StdEncoding.EncodeToString(out.CiphertextBlob)
}
return nil
}
// NewPGPKeySourceFromString created a new PGPKeySource from a comma-separated string of PGP fingerprints
func NewPGPKeySourceFromString(fps string) GPGKeySource {
var gpgs []GPG
for _, s := range strings.Split(fps, ",") {
gpgs = append(gpgs, GPG{Fingerprint: strings.Replace(s, " ", "", -1)})
}
return GPGKeySource{GPG: gpgs}
}
func (gpg GPGKeySource) gpgHome() string {
dir := os.Getenv("GNUPGHOME")
if dir == "" {
usr, err := user.Current()
if err != nil {
return "~/.gnupg"
}
return path.Join(usr.HomeDir, ".gnupg")
}
return dir
}
func (gpg GPGKeySource) loadRing(path string) (openpgp.EntityList, error) {
f, err := os.Open(path)
if err != nil {
return openpgp.EntityList{}, err
}
defer f.Close()
keyring, err := openpgp.ReadKeyRing(f)
if err != nil {
return keyring, err
}
return keyring, nil
}
func (gpg GPGKeySource) secRing() (openpgp.EntityList, error) {
return gpg.loadRing(gpg.gpgHome() + "/secring.gpg")
}
func (gpg GPGKeySource) pubRing() (openpgp.EntityList, error) {
return gpg.loadRing(gpg.gpgHome() + "/pubring.gpg")
}
func (gpg GPGKeySource) fingerprintMap(ring openpgp.EntityList) map[string]openpgp.Entity {
fps := make(map[string]openpgp.Entity)
for _, entity := range ring {
fp := strings.ToUpper(hex.EncodeToString(entity.PrimaryKey.Fingerprint[:]))
if entity != nil {
fps[fp] = *entity
}
}
return fps
}
func (gpg GPGKeySource) passphrasePrompt(keys []openpgp.Key, symmetric bool) ([]byte, error) {
fmt.Print("Enter PGP key passphrase: ")
psswd, err := gopass.GetPasswd()
if err != nil {
fmt.Println(err)
}
return psswd, err
}
func (gpg GPGKeySource) DecryptKeys() (string, error) {
ring, err := gpg.secRing()
if err != nil {
return "", fmt.Errorf("Could not load secring: %s", err)
}
for _, g := range gpg.GPG {
block, err := armor.Decode(strings.NewReader(g.EncryptedKey))
if err != nil {
fmt.Println("Decode failed", err)
continue
}
md, err := openpgp.ReadMessage(block.Body, ring, gpg.passphrasePrompt, nil)
if err != nil {
fmt.Println("ReadMessage failed", err)
continue
}
if b, err := ioutil.ReadAll(md.UnverifiedBody); err == nil {
return string(b), nil
}
}
return "", fmt.Errorf("The key could not be decrypted with any of the GPG entries")
}
func (gpg GPGKeySource) EncryptKeys(plaintext string) error {
ring, err := gpg.pubRing()
if err != nil {
return err
}
fingerprints := gpg.fingerprintMap(ring)
for i, _ := range gpg.GPG {
entity, ok := fingerprints[gpg.GPG[i].Fingerprint]
if !ok {
return fmt.Errorf("Key with fingerprint %s is not available in keyring.", gpg.GPG[i].Fingerprint)
}
encbuf := new(bytes.Buffer)
armorbuf, err := armor.Encode(encbuf, "PGP MESSAGE", nil)
if err != nil {
return err
}
plaintextbuf, err := openpgp.Encrypt(armorbuf, []*openpgp.Entity{&entity}, nil, nil, nil)
if err != nil {
return err
}
_, err = plaintextbuf.Write([]byte(plaintext))
if err != nil {
return err
}
err = plaintextbuf.Close()
if err != nil {
return err
}
err = armorbuf.Close()
if err != nil {
return err
}
bytes, err := ioutil.ReadAll(encbuf)
if err != nil {
return err
}
gpg.GPG[i].EncryptedKey = string(bytes)
}
return nil
}

View File

@@ -1,18 +1,25 @@
package sops
import (
"fmt"
"go.mozilla.org/sops/kms"
"go.mozilla.org/sops/pgp"
"testing"
"testing/quick"
)
func TestKMS(t *testing.T) {
// TODO: make this not terrible and mock KMS with a reverseable operation on the key, or something. Good luck running the tests on a machine that's not mine!
ks := KMSKeySource{KMS: []KMS{
KMS{Arn: "arn:aws:kms:us-east-1:927034868273:key/e9fc75db-05e9-44c1-9c35-633922bac347", Role: "", EncryptedKey: ""},
}}
k := kms.KMSMasterKey{Arn: "arn:aws:kms:us-east-1:927034868273:key/e9fc75db-05e9-44c1-9c35-633922bac347", Role: "", EncryptedKey: ""}
f := func(x string) bool {
ks.EncryptKeys(x)
v, _ := ks.DecryptKeys()
err := k.Encrypt(x)
if err != nil {
fmt.Println(err)
}
v, err := k.Decrypt()
if err != nil {
fmt.Println(err)
}
if x == "" {
return true // we can't encrypt an empty string
}
@@ -28,13 +35,10 @@ func TestKMS(t *testing.T) {
}
func TestGPG(t *testing.T) {
ks := GPGKeySource{GPG: []GPG{
GPG{Fingerprint: "64FEF099B0544CF975BCD408A014A073E0848B51"},
},
}
key := pgp.NewGPGMasterKeyFromFingerprint("64FEF099B0544CF975BCD408A014A073E0848B51")
f := func(x string) bool {
ks.EncryptKeys(x)
k, _ := ks.DecryptKeys()
key.Encrypt(x)
k, _ := key.Decrypt()
return x == k
}
if err := quick.Check(f, nil); err != nil {
@@ -44,18 +48,22 @@ func TestGPG(t *testing.T) {
func TestGPGKeySourceFromString(t *testing.T) {
s := "C8C5 2C0A B2A4 8174 01E8 12C8 F3CC 3233 3FAD 9F1E, C8C5 2C0A B2A4 8174 01E8 12C8 F3CC 3233 3FAD 9F1E"
ks := NewPGPKeySourceFromString(s)
ks := pgp.GPGMasterKeysFromFingerprintString(s)
expected := "C8C52C0AB2A4817401E812C8F3CC32333FAD9F1E"
if ks.GPG[0].Fingerprint != expected || ks.GPG[1].Fingerprint != expected {
if ks[0].Fingerprint != expected {
t.Errorf("Fingerprint does not match. Got %s, expected %s", ks[0].Fingerprint, expected)
}
if ks[1].Fingerprint != expected {
t.Error("Fingerprint does not match")
}
}
func TestKMSKeySourceFromString(t *testing.T) {
s := "arn:aws:kms:us-east-1:656532927350:key/920aff2e-c5f1-4040-943a-047fa387b27e+arn:aws:iam::927034868273:role/sops-dev, arn:aws:kms:ap-southeast-1:656532927350:key/9006a8aa-0fa6-4c14-930e-a2dfb916de1d"
ks := NewKMSKeySourceFromString(s)
k1 := ks.KMS[0]
k2 := ks.KMS[1]
ks := kms.KMSMasterKeysFromArnString(s)
k1 := ks[0]
k2 := ks[1]
expectedArn1 := "arn:aws:kms:us-east-1:656532927350:key/920aff2e-c5f1-4040-943a-047fa387b27e"
expectedRole1 := "arn:aws:iam::927034868273:role/sops-dev"
if k1.Arn != expectedArn1 {

128
kms/keysource.go Normal file
View File

@@ -0,0 +1,128 @@
package kms
import (
"encoding/base64"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/aws/aws-sdk-go/service/sts"
"os"
"regexp"
"strings"
"time"
)
type KMSMasterKey struct {
Arn string
Role string
EncryptedKey string
CreationDate time.Time
}
func (key *KMSMasterKey) Encrypt(dataKey string) error {
sess, err := key.createSession()
if err != nil {
return err
}
service := kms.New(sess)
out, err := service.Encrypt(&kms.EncryptInput{Plaintext: []byte(dataKey), KeyId: &key.Arn})
if err != nil {
return err
}
key.EncryptedKey = base64.StdEncoding.EncodeToString(out.CiphertextBlob)
return nil
}
func (key *KMSMasterKey) EncryptIfNeeded(dataKey string) error {
if key.EncryptedKey == "" {
return key.Encrypt(dataKey)
}
return nil
}
func (key *KMSMasterKey) Decrypt() (string, error) {
k, err := base64.StdEncoding.DecodeString(key.EncryptedKey)
if err != nil {
return "", fmt.Errorf("Error base64-decoding encrypted data key: %s", err)
}
sess, err := key.createSession()
if err != nil {
return "", fmt.Errorf("Error creating AWS session: %v", err)
}
service := kms.New(sess)
decrypted, err := service.Decrypt(&kms.DecryptInput{CiphertextBlob: k})
if err != nil {
return "", fmt.Errorf("Error decrypting key: %v", err)
}
return string(decrypted.Plaintext), nil
}
func (key *KMSMasterKey) NeedsRotation() bool {
return time.Since(key.CreationDate).Hours() > float64(24*30*6)
}
func (key *KMSMasterKey) ToString() string {
return key.Arn
}
func NewKMSMasterKeyFromArn(arn string) KMSMasterKey {
k := KMSMasterKey{}
arn = strings.Replace(arn, " ", "", -1)
roleIndex := strings.Index(arn, "+arn:aws:iam::")
if roleIndex > 0 {
k.Arn = arn[:roleIndex]
k.Role = arn[roleIndex+1:]
} else {
k.Arn = arn
}
return k
}
func KMSMasterKeysFromArnString(arn string) []KMSMasterKey {
var keys []KMSMasterKey
for _, s := range strings.Split(arn, ",") {
keys = append(keys, NewKMSMasterKeyFromArn(s))
}
return keys
}
func (k KMSMasterKey) createStsSession(config aws.Config, sess *session.Session) (*session.Session, error) {
hostname, err := os.Hostname()
if err != nil {
return nil, err
}
stsService := sts.New(sess)
name := "sops@" + hostname
out, err := stsService.AssumeRole(&sts.AssumeRoleInput{
RoleArn: &k.Role, RoleSessionName: &name})
if err != nil {
return nil, err
}
config.Credentials = credentials.NewStaticCredentials(*out.Credentials.AccessKeyId,
*out.Credentials.SecretAccessKey, *out.Credentials.SessionToken)
sess, err = session.NewSession(&config)
if err != nil {
return nil, err
}
return sess, nil
}
func (k KMSMasterKey) createSession() (*session.Session, error) {
re := regexp.MustCompile(`^arn:aws:kms:(.+):([0-9]+):key/(.+)$`)
matches := re.FindStringSubmatch(k.Arn)
if matches == nil {
return nil, fmt.Errorf("No valid ARN found in %s", k.Arn)
}
config := aws.Config{Region: aws.String(matches[1])}
sess, err := session.NewSession(&config)
if err != nil {
return nil, err
}
if k.Role != "" {
return k.createStsSession(config, sess)
}
return sess, nil
}

162
pgp/keysource.go Normal file
View File

@@ -0,0 +1,162 @@
package pgp
import (
"bytes"
"encoding/hex"
"fmt"
"github.com/howeyc/gopass"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
"io/ioutil"
"os"
"os/user"
"path"
"strings"
"time"
)
type GPGMasterKey struct {
Fingerprint string
EncryptedKey string
CreationDate time.Time
}
func (key *GPGMasterKey) Encrypt(dataKey string) error {
ring, err := key.pubRing()
if err != nil {
return err
}
fingerprints := key.fingerprintMap(ring)
entity, ok := fingerprints[key.Fingerprint]
if !ok {
return fmt.Errorf("Key with fingerprint %s is not available in keyring.", key.Fingerprint)
}
encbuf := new(bytes.Buffer)
armorbuf, err := armor.Encode(encbuf, "PGP MESSAGE", nil)
if err != nil {
return err
}
plaintextbuf, err := openpgp.Encrypt(armorbuf, []*openpgp.Entity{&entity}, nil, nil, nil)
if err != nil {
return err
}
_, err = plaintextbuf.Write([]byte(dataKey))
if err != nil {
return err
}
err = plaintextbuf.Close()
if err != nil {
return err
}
err = armorbuf.Close()
if err != nil {
return err
}
bytes, err := ioutil.ReadAll(encbuf)
if err != nil {
return err
}
key.EncryptedKey = string(bytes)
return nil
}
func (key *GPGMasterKey) EncryptIfNeeded(dataKey string) error {
if key.EncryptedKey == "" {
return key.Encrypt(dataKey)
}
return nil
}
func (key *GPGMasterKey) Decrypt() (string, error) {
ring, err := key.secRing()
if err != nil {
return "", fmt.Errorf("Could not load secring: %s", err)
}
block, err := armor.Decode(strings.NewReader(key.EncryptedKey))
if err != nil {
return "", fmt.Errorf("Armor decoding failed: %s", err)
}
md, err := openpgp.ReadMessage(block.Body, ring, key.passphrasePrompt, nil)
if err != nil {
return "", fmt.Errorf("Reading PGP message failed: %s", err)
}
if b, err := ioutil.ReadAll(md.UnverifiedBody); err == nil {
return string(b), nil
}
return "", fmt.Errorf("The key could not be decrypted with any of the GPG entries")
}
func (key *GPGMasterKey) NeedsRotation() bool {
return time.Since(key.CreationDate).Hours() > 24*30*6
}
func (key *GPGMasterKey) ToString() string {
return key.Fingerprint
}
func (key *GPGMasterKey) gpgHome() string {
dir := os.Getenv("GNUPGHOME")
if dir == "" {
usr, err := user.Current()
if err != nil {
return "~/.gnupg"
}
return path.Join(usr.HomeDir, ".gnupg")
}
return dir
}
func NewGPGMasterKeyFromFingerprint(fingerprint string) GPGMasterKey {
return GPGMasterKey{
Fingerprint: strings.Replace(fingerprint, " ", "", -1),
}
}
func GPGMasterKeysFromFingerprintString(fingerprint string) []GPGMasterKey {
var keys []GPGMasterKey
for _, s := range strings.Split(fingerprint, ",") {
keys = append(keys, NewGPGMasterKeyFromFingerprint(s))
}
return keys
}
func (key *GPGMasterKey) loadRing(path string) (openpgp.EntityList, error) {
f, err := os.Open(path)
if err != nil {
return openpgp.EntityList{}, err
}
defer f.Close()
keyring, err := openpgp.ReadKeyRing(f)
if err != nil {
return keyring, err
}
return keyring, nil
}
func (key *GPGMasterKey) secRing() (openpgp.EntityList, error) {
return key.loadRing(key.gpgHome() + "/secring.gpg")
}
func (key *GPGMasterKey) pubRing() (openpgp.EntityList, error) {
return key.loadRing(key.gpgHome() + "/pubring.gpg")
}
func (key *GPGMasterKey) fingerprintMap(ring openpgp.EntityList) map[string]openpgp.Entity {
fps := make(map[string]openpgp.Entity)
for _, entity := range ring {
fp := strings.ToUpper(hex.EncodeToString(entity.PrimaryKey.Fingerprint[:]))
if entity != nil {
fps[fp] = *entity
}
}
return fps
}
func (key *GPGMasterKey) passphrasePrompt(keys []openpgp.Key, symmetric bool) ([]byte, error) {
fmt.Print("Enter PGP key passphrase: ")
psswd, err := gopass.GetPasswd()
if err != nil {
fmt.Println(err)
}
return psswd, err
}

58
sops.go Normal file
View File

@@ -0,0 +1,58 @@
package sops
import (
"fmt"
"time"
)
type Metadata struct {
LastModified time.Time
UnencryptedSuffix string
MessageAuthenticationCode string
Version string
KeySources []KeySource
}
type KeySource struct {
Name string
Keys []*MasterKey
}
type MasterKey interface {
Encrypt(dataKey string) error
EncryptIfNeeded(dataKey string) error
Decrypt() (string, error)
NeedsRotation() bool
ToString() string
}
func (m *Metadata) MasterKeyCount() int {
count := 0
for _, ks := range m.KeySources {
count += len(ks.Keys)
}
return count
}
func (m *Metadata) RemoveMasterKeys(keys []MasterKey) {
for _, ks := range m.KeySources {
for i, k := range ks.Keys {
for _, k2 := range keys {
if (*k).ToString() == k2.ToString() {
ks.Keys = append(ks.Keys[:i], ks.Keys[i+1:]...)
}
}
}
}
}
func (m *Metadata) UpdateMasterKeys(dataKey string) {
for _, ks := range m.KeySources {
for _, k := range ks.Keys {
err := (*k).EncryptIfNeeded(dataKey)
if err != nil {
fmt.Println("[WARNING]: could not encrypt data key with master key ", (*k).ToString())
}
}
}
}