From 533bc804c0787fb133fbfa8300d7d02c3b10f10d Mon Sep 17 00:00:00 2001 From: Adrian Utrilla Date: Thu, 25 May 2017 15:31:12 +0200 Subject: [PATCH] Added shamir secret sharing to the sops library --- shamir_test.go | 125 +++++++++++++++++++++++++++++++++++++++++++++++++ sops.go | 81 ++++++++++++++++++++++++++++++-- 2 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 shamir_test.go diff --git a/shamir_test.go b/shamir_test.go new file mode 100644 index 000000000..dcfd51ba6 --- /dev/null +++ b/shamir_test.go @@ -0,0 +1,125 @@ +package sops + +import ( + "testing" + "time" + + "crypto/rand" + + "github.com/stretchr/testify/assert" +) + +type PlaintextMasterKey struct { + Key []byte +} + +func (k *PlaintextMasterKey) Encrypt(dataKey []byte) error { + k.Key = dataKey + return nil +} + +func (k *PlaintextMasterKey) EncryptIfNeeded(dataKey []byte) error { + k.Key = dataKey + return nil +} + +func (k *PlaintextMasterKey) Decrypt() ([]byte, error) { + return k.Key, nil +} + +func (k *PlaintextMasterKey) NeedsRotation() bool { + return false +} + +func (k *PlaintextMasterKey) ToString() string { + return string(k.Key) +} + +func (k *PlaintextMasterKey) ToMap() map[string]interface{} { + return map[string]interface{}{ + "key": k.Key, + } +} + +func TestShamirRoundtripAllKeysAvailable(t *testing.T) { + key := make([]byte, 32) + _, err := rand.Read(key) + assert.NoError(t, err) + m := Metadata{ + Shamir: true, + KeySources: []KeySource{ + { + Name: "mock", + Keys: []MasterKey{ + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + }, + }, + }, + LastModified: time.Now(), + } + errs := m.UpdateMasterKeys(key) + assert.Empty(t, errs) + dataKey, err := m.GetDataKey() + assert.NoError(t, err) + assert.Equal(t, key, dataKey) +} + +func TestShamirRoundtripQuorumAvailable(t *testing.T) { + key := make([]byte, 32) + _, err := rand.Read(key) + assert.NoError(t, err) + m := Metadata{ + Shamir: true, + KeySources: []KeySource{ + { + Name: "mock", + Keys: []MasterKey{ + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + }, + }, + }, + LastModified: time.Now(), + } + errs := m.UpdateMasterKeys(key) + assert.Empty(t, errs) + m.KeySources[0].Keys = m.KeySources[0].Keys[:3] + dataKey, err := m.GetDataKey() + assert.NoError(t, err) + assert.Equal(t, key, dataKey) +} + +func TestShamirRoundtripNotEnoughKeys(t *testing.T) { + key := make([]byte, 32) + _, err := rand.Read(key) + assert.NoError(t, err) + m := Metadata{ + Shamir: true, + KeySources: []KeySource{ + { + Name: "mock", + Keys: []MasterKey{ + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + &PlaintextMasterKey{}, + }, + }, + }, + LastModified: time.Now(), + } + errs := m.UpdateMasterKeys(key) + assert.Empty(t, errs) + m.KeySources[0].Keys = m.KeySources[0].Keys[:2] + dataKey, err := m.GetDataKey() + assert.Error(t, err) + assert.NotEqual(t, key, dataKey) +} diff --git a/sops.go b/sops.go index d3530c0e2..b5ff3c7b3 100644 --- a/sops.go +++ b/sops.go @@ -43,6 +43,7 @@ import ( "strings" "time" + "github.com/hashicorp/vault/shamir" "go.mozilla.org/sops/kms" "go.mozilla.org/sops/pgp" ) @@ -184,7 +185,7 @@ func (tree TreeBranch) walkBranch(in TreeBranch, path []string, onLeaves func(in } key, ok := item.Key.(string) if !ok { - return nil, fmt.Errorf("Tree contains a non-string key (type %T): %s. Only string keys are" + + return nil, fmt.Errorf("Tree contains a non-string key (type %T): %s. Only string keys are"+ "supported", item.Key, item.Key) } newV, err := tree.walkValue(item.Value, append(path, key), onLeaves) @@ -288,6 +289,12 @@ type Metadata struct { MessageAuthenticationCode string Version string KeySources []KeySource + // Shamir specifies if the data key this file is encrypted with was + // split between all key sources using Shamir's Secret Sharing. + Shamir bool + // ShamirQuorum is the number of parts required to recover the original + // data key + ShamirQuorum int } // KeySource is a collection of MasterKeys with a Name. @@ -346,6 +353,11 @@ func (m *Metadata) RemoveMasterKeys(keys []MasterKey) { // UpdateMasterKeysIfNeeded encrypts the data key with all master keys if it's needed func (m *Metadata) UpdateMasterKeysIfNeeded(dataKey []byte) (errs []error) { + // If we're using Shamir and we've added or removed keys, we must + // generate Shamir parts again and reencrypt with all keys + if m.Shamir { + return m.updateMasterKeysShamir(dataKey) + } for _, ks := range m.KeySources { for _, k := range ks.Keys { err := k.EncryptIfNeeded(dataKey) @@ -357,8 +369,44 @@ func (m *Metadata) UpdateMasterKeysIfNeeded(dataKey []byte) (errs []error) { return } +// updateMasterKeysShamir splits the data key into parts using Shamir's Secret +// Sharing algorithm and encrypts each part with a master key +func (m *Metadata) updateMasterKeysShamir(dataKey []byte) (errs []error) { + keyCount := 0 + for _, ks := range m.KeySources { + for range ks.Keys { + keyCount++ + } + } + quorum := (keyCount / 2) + 1 + m.ShamirQuorum = quorum + parts, err := shamir.Split(dataKey, keyCount, quorum) + if err != nil { + errs = append(errs, fmt.Errorf("Could not split data key into parts for Shamir: %s", err)) + return + } + if len(parts) != keyCount { + errs = append(errs, fmt.Errorf("Not enough parts obtained from Shamir. Need %d, got %d", keyCount, len(parts))) + return + } + counter := 0 + for _, ks := range m.KeySources { + for _, k := range ks.Keys { + err := k.Encrypt(parts[counter]) + if err != nil { + errs = append(errs, fmt.Errorf("Failed to encrypt Shamir part with master key %q: %v\n", k.ToString(), err)) + } + counter++ + } + } + return +} + // UpdateMasterKeys encrypts the data key with all master keys func (m *Metadata) UpdateMasterKeys(dataKey []byte) (errs []error) { + if m.Shamir { + return m.updateMasterKeysShamir(dataKey) + } for _, ks := range m.KeySources { for _, k := range ks.Keys { err := k.Encrypt(dataKey) @@ -435,8 +483,26 @@ func (m *Metadata) ToMap() map[string]interface{} { return out } -// GetDataKey retrieves the data key from the first MasterKey in the Metadata's KeySources that's able to return it. -func (m Metadata) GetDataKey() ([]byte, error) { +func (m Metadata) getDataKeyShamir() ([]byte, error) { + var parts [][]byte + for _, ks := range m.KeySources { + for _, k := range ks.Keys { + key, err := k.Decrypt() + if err != nil { + // + } + parts = append(parts, key) + } + } + if len(parts) < m.ShamirQuorum { + return nil, fmt.Errorf("Not enough parts to recover data key with Shamir. Need %d, have %d.", m.ShamirQuorum, len(parts)) + } + return shamir.Combine(parts) +} + +// getFirstDataKey retrieves the data key from the first MasterKey in the +// Metadata's KeySources that's able to return it. +func (m Metadata) getFirstDataKey() ([]byte, error) { errMsg := "Could not decrypt the data key with any of the master keys:\n" for _, ks := range m.KeySources { for _, k := range ks.Keys { @@ -456,6 +522,15 @@ func (m Metadata) GetDataKey() ([]byte, error) { return nil, fmt.Errorf(errMsg) } +// GetDataKey retrieves the data key. +func (m Metadata) GetDataKey() ([]byte, error) { + if m.Shamir { + return m.getDataKeyShamir() + } else { + return m.getFirstDataKey() + } +} + // ToBytes converts a string, int, float or bool to a byte representation. func ToBytes(in interface{}) ([]byte, error) { switch in := in.(type) {