diff --git a/keysources.go b/keysources.go deleted file mode 100644 index 99da1c6f8..000000000 --- a/keysources.go +++ /dev/null @@ -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 -} diff --git a/keysources_test.go b/keysources_test.go index 4da806c73..19a2a5abf 100644 --- a/keysources_test.go +++ b/keysources_test.go @@ -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 { diff --git a/kms/keysource.go b/kms/keysource.go new file mode 100644 index 000000000..81625435f --- /dev/null +++ b/kms/keysource.go @@ -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 +} diff --git a/pgp/keysource.go b/pgp/keysource.go new file mode 100644 index 000000000..7a8f6b7dd --- /dev/null +++ b/pgp/keysource.go @@ -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 +} diff --git a/sops.go b/sops.go new file mode 100644 index 000000000..a87d5034f --- /dev/null +++ b/sops.go @@ -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()) + } + } + } +}