Skip to content

Commit 55da9c7

Browse files
authored
Keyloader: split LoadKey (sub function NewKey) (#26)
* Keyloader: split LoadKey into a sub function NewKey() which directly accepts KeyConfig objects without going through configstore. * Keyloader NewKey: sort key configs * Update .travis.yaml
1 parent 262d351 commit 55da9c7

File tree

3 files changed

+125
-30
lines changed

3 files changed

+125
-30
lines changed

.travis.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
language: go
22
go:
3-
- 1.11.x
4-
- 1.12.x
3+
- 1.13.x
4+
- 1.14.x
55

66
# Only clone the most recent commit.
77
git:
@@ -15,8 +15,7 @@ notifications:
1515
# build and immediately stop. It's sorta like having set -e enabled in bash.
1616
# Make sure golangci-lint is vendored.
1717
before_script:
18-
- go get -v -u github.com/golangci/golangci-lint/cmd/golangci-lint
19-
- go install github.com/golangci/golangci-lint/cmd/golangci-lint
18+
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.25.0
2019

2120
# script always runs to completion (set +e). If we have linter issues AND a
2221
# failing test, we want to see both. Configure golangci-lint with a

keyloader/keyloader.go

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package keyloader
22

33
import (
44
"fmt"
5+
"sort"
56
"strconv"
67
"sync"
78
"sync/atomic"
@@ -202,6 +203,7 @@ func reorderTimestamp(s *configstore.Item) int64 {
202203
i, err := s.Unmarshaled()
203204
if err == nil {
204205
ret := i.(*KeyConfig).Timestamp
206+
// small hack to tiebreak in favor of sealed keys (mostly in case of zero-value timestamp)
205207
if i.(*KeyConfig).Sealed {
206208
ret++
207209
}
@@ -218,7 +220,62 @@ func configFactory() interface{} {
218220
** CONSTRUCTORS
219221
*/
220222

223+
// NewKey returns a symmecrypt.Key object configured from a number of KeyConfig objects.
224+
// If several KeyConfigs are supplied, the returned Key will be composite.
225+
// A composite key encrypts with the latest Key (based on timestamp) and decrypts with any of they keys.
226+
//
227+
// If the key configuration specifies it is sealed, the key returned will be wrapped by an unseal mechanism.
228+
// When the symmecrypt/seal global singleton gets unsealed, the key will become usable instantly. It will return errors in the meantime.
229+
//
230+
// The key cipher name is expected to match a KeyFactory that got registered through RegisterCipher().
231+
// Either use a built-in cipher, or make sure to register a proper factory for this cipher.
232+
// This KeyFactory will be called, either directly or when the symmecrypt/seal global singleton gets unsealed, if applicable.
233+
func NewKey(cfgs ...*KeyConfig) (symmecrypt.Key, error) {
234+
235+
if len(cfgs) == 0 {
236+
return nil, errors.New("missing key config")
237+
}
238+
239+
// sort by timestamp: latest (bigger timestamp) first
240+
sort.Slice(cfgs, func(i, j int) bool { return cfgs[i].Timestamp > cfgs[j].Timestamp })
241+
242+
firstNonSealed := !cfgs[0].Sealed
243+
comp := symmecrypt.CompositeKey{}
244+
245+
for _, cfg := range cfgs {
246+
247+
var ref symmecrypt.Key
248+
factory, err := symmecrypt.GetKeyFactory(cfg.Cipher)
249+
if err != nil {
250+
return nil, err
251+
}
252+
if cfg.Sealed {
253+
// if the first position (used for encryption in composite keys) was not sealed, but other keys used for fallback decryption are sealed
254+
// it may be an attack to trigger a reencrypt with a key known by the attacker
255+
if firstNonSealed {
256+
return nil, errors.New("DANGER! Detected downgrade to non-sealed encryption key. Non-sealed key has higher priority, this looks malicious. Aborting!")
257+
}
258+
ref = newSealedKey(cfg, factory)
259+
} else {
260+
ref, err = factory.NewKey(cfg.Key)
261+
if err != nil {
262+
return nil, err
263+
}
264+
}
265+
266+
comp = append(comp, ref)
267+
}
268+
269+
// if only a single key config was provided, decapsulate the composite key
270+
if len(comp) == 1 {
271+
return comp[0], nil
272+
}
273+
274+
return comp, nil
275+
}
276+
221277
// LoadKey instantiates a new encryption key for a given identifier from the default store in configstore.
278+
// It retrieves all the necessary data from configstore then calls NewKey().
222279
//
223280
// If several keys are found for the identifier, they are sorted by timestamp, and a composite key is returned.
224281
// The most recent key will be used for encryption, and decryption will be done by any of them.
@@ -235,6 +292,7 @@ func LoadKey(identifier string) (symmecrypt.Key, error) {
235292
}
236293

237294
// LoadKeyFromStore instantiates a new encryption key for a given identifier from a specific store instance.
295+
// It retrieves all the necessary data from configstore then calls NewKey().
238296
//
239297
// If several keys are found for the identifier, they are sorted by timestamp, and a composite key is returned.
240298
// The most recent key will be used for encryption, and decryption will be done by any of them.
@@ -261,52 +319,34 @@ func LoadKeyFromStore(identifier string, store *configstore.Store) (symmecrypt.K
261319
return nil, fmt.Errorf("ambiguous config: several encryption keys conflicting for '%s'", identifier)
262320
}
263321

264-
comp := symmecrypt.CompositeKey{}
265-
266-
hadNonSealed := false
322+
var cfgs []*KeyConfig
267323

268324
for _, item := range items.Items {
269-
270325
i, err := item.Unmarshaled()
271326
if err != nil {
272327
return nil, err
273328
}
274-
var ref symmecrypt.Key
275329
cfg := i.(*KeyConfig)
276-
factory, err := symmecrypt.GetKeyFactory(cfg.Cipher)
277-
if err != nil {
278-
return nil, err
279-
}
280-
if cfg.Sealed {
281-
if hadNonSealed {
282-
panic(fmt.Sprintf("encryption key '%s': DANGER! Detected downgrade to non-sealed encryption key. Non-sealed key has higher priority, this looks malicious. Aborting!", identifier))
283-
}
284-
ref = newSealedKey(cfg, factory)
285-
} else {
286-
hadNonSealed = true
287-
ref, err = factory.NewKey(cfg.Key)
288-
if err != nil {
289-
return nil, err
290-
}
291-
}
292-
293-
comp = append(comp, ref)
330+
cfgs = append(cfgs, cfg)
294331
}
295332

296-
if len(comp) == 1 {
297-
return comp[0], nil
333+
key, err := NewKey(cfgs...)
334+
if err != nil {
335+
return nil, fmt.Errorf("encryption key '%s': %s", identifier, err)
298336
}
299337

300-
return comp, nil
338+
return key, nil
301339
}
302340

303341
// LoadSingleKey instantiates a new encryption key using LoadKey from the default store in configstore without specifying its identifier.
342+
// It retrieves all the necessary data from configstore then calls NewKey().
304343
// It will error if several different identifiers are found.
305344
func LoadSingleKey() (symmecrypt.Key, error) {
306345
return LoadSingleKeyFromStore(configstore.DefaultStore)
307346
}
308347

309348
// LoadSingleKey instantiates a new encryption key using LoadKey from a specific store instance without specifying its identifier.
349+
// It retrieves all the necessary data from configstore then calls NewKey().
310350
// It will error if several different identifiers are found.
311351
func LoadSingleKeyFromStore(store *configstore.Store) (symmecrypt.Key, error) {
312352
ident, err := singleKeyIdentifier(store)

symmecrypt_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,49 @@ func ProviderTest() (configstore.ItemList, error) {
4040
return ret, nil
4141
}
4242

43+
// Bad config: conflicting timestamps
44+
func ProviderTestKOTimestamp() (configstore.ItemList, error) {
45+
ret := configstore.ItemList{
46+
Items: []configstore.Item{
47+
configstore.NewItem(
48+
keyloader.EncryptionKeyConfigName,
49+
`{"key":"5fdb8af280b007a46553dfddb3f42bc10619dcabca8d4fdf5239b09445ab1a41","identifier":"test","sealed":false,"timestamp":1,"cipher":"aes-gcm"}`,
50+
1,
51+
),
52+
configstore.NewItem(
53+
keyloader.EncryptionKeyConfigName,
54+
`{"key":"QXdDW4N/jmJzpMu7i1zu4YF1opTn7H+eOk9CLFGBSFg=","identifier":"test","sealed":false,"timestamp":1,"cipher":"xchacha20-poly1305"}`,
55+
1,
56+
),
57+
},
58+
}
59+
return ret, nil
60+
}
61+
62+
// Bad config: latest key non sealed
63+
func ProviderTestKOSeal() (configstore.ItemList, error) {
64+
ret := configstore.ItemList{
65+
Items: []configstore.Item{
66+
configstore.NewItem(
67+
keyloader.EncryptionKeyConfigName,
68+
`{"key":"5fdb8af280b007a46553dfddb3f42bc10619dcabca8d4fdf5239b09445ab1a41","identifier":"test","sealed":false,"timestamp":10,"cipher":"aes-gcm"}`,
69+
1,
70+
),
71+
configstore.NewItem(
72+
keyloader.EncryptionKeyConfigName,
73+
`{"key":"QXdDW4N/jmJzpMu7i1zu4YF1opTn7H+eOk9CLFGBSFg=","identifier":"test","sealed":true,"timestamp":1,"cipher":"xchacha20-poly1305"}`,
74+
1,
75+
),
76+
},
77+
}
78+
return ret, nil
79+
}
80+
81+
var KOTests = map[string]func() (configstore.ItemList, error){
82+
"timestamp": ProviderTestKOTimestamp,
83+
"seal": ProviderTestKOSeal,
84+
}
85+
4386
func TestMain(m *testing.M) {
4487

4588
configstore.RegisterProvider("test", ProviderTest)
@@ -353,6 +396,19 @@ func TestWriterWithEncoders(t *testing.T) {
353396
}
354397
}
355398

399+
func TestKeyloaderKO(t *testing.T) {
400+
401+
for testName, provider := range KOTests {
402+
st := configstore.NewStore()
403+
st.RegisterProvider("test", provider)
404+
405+
_, err := keyloader.LoadKeyFromStore("test", st)
406+
if err == nil {
407+
t.Fatalf("nil error with KO config (%s)", testName)
408+
}
409+
}
410+
}
411+
356412
func ExampleNewWriter() {
357413
k, err := keyloader.LoadKey("test")
358414
if err != nil {

0 commit comments

Comments
 (0)