Skip to content

Commit 6389ea5

Browse files
authored
Rotate Encryption Key (#13)
* add key rotation
1 parent 15b741b commit 6389ea5

File tree

219 files changed

+162057
-67
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

219 files changed

+162057
-67
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ piggybankctl.exe
55
piggybankctl-darwin
66
piggybank*.tar.gz
77
piggybank*.zip
8+
fly.toml
89

910
dist/

go.mod

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/CoverWhale/logr v0.0.0-20240513164108-a4fd5504b303
1111
github.com/briandowns/spinner v1.23.0
1212
github.com/nats-io/jsm.go v0.1.1
13+
github.com/nats-io/nats-server/v2 v2.10.12
1314
github.com/nats-io/nats.go v1.36.0
1415
github.com/segmentio/ksuid v1.0.4
1516
github.com/spf13/cobra v1.8.1
@@ -31,8 +32,10 @@ require (
3132
github.com/magiconair/properties v1.8.7 // indirect
3233
github.com/mattn/go-colorable v0.1.13 // indirect
3334
github.com/mattn/go-isatty v0.0.20 // indirect
35+
github.com/minio/highwayhash v1.0.2 // indirect
3436
github.com/minio/selfupdate v0.6.0 // indirect
3537
github.com/mitchellh/mapstructure v1.5.0 // indirect
38+
github.com/nats-io/jwt/v2 v2.5.5 // indirect
3639
github.com/nats-io/nkeys v0.4.7 // indirect
3740
github.com/nats-io/nuid v1.0.1 // indirect
3841
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
@@ -52,6 +55,7 @@ require (
5255
golang.org/x/sys v0.21.0 // indirect
5356
golang.org/x/term v0.21.0 // indirect
5457
golang.org/x/text v0.16.0 // indirect
58+
golang.org/x/time v0.5.0 // indirect
5559
gopkg.in/ini.v1 v1.67.0 // indirect
5660
gopkg.in/yaml.v3 v3.0.1 // indirect
5761
)

go.sum

+9
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,18 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
5252
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
5353
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
5454
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
55+
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
56+
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
5557
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
5658
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
5759
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
5860
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
5961
github.com/nats-io/jsm.go v0.1.1 h1:6vjllz276SdC+3Fb3XI71p9B6toxkCruuB1K6unQEr0=
6062
github.com/nats-io/jsm.go v0.1.1/go.mod h1:cFz5wR1pW0zLFotntS4HA7V8Wm+sf8zpF+iQJHbsS6M=
63+
github.com/nats-io/jwt/v2 v2.5.5 h1:ROfXb50elFq5c9+1ztaUbdlrArNFl2+fQWP6B8HGEq4=
64+
github.com/nats-io/jwt/v2 v2.5.5/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A=
65+
github.com/nats-io/nats-server/v2 v2.10.12 h1:G6u+RDrHkw4bkwn7I911O5jqys7jJVRY6MwgndyUsnE=
66+
github.com/nats-io/nats-server/v2 v2.10.12/go.mod h1:H1n6zXtYLFCgXcf/SF8QNTSIFuS8tyZQMN9NguUHdEs=
6167
github.com/nats-io/nats.go v1.36.0 h1:suEUPuWzTSse/XhESwqLxXGuj8vGRuPRoG7MoRN/qyU=
6268
github.com/nats-io/nats.go v1.36.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
6369
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
@@ -121,6 +127,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
121127
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
122128
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
123129
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
130+
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
124131
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
125132
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
126133
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -139,6 +146,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
139146
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
140147
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
141148
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
149+
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
150+
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
142151
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
143152
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
144153
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

service/client.go

-4
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ type Request struct {
2222
Data []byte
2323
}
2424

25-
type SecretResponse struct {
26-
Details string `json:"details"`
27-
}
28-
2925
type ResponseError struct {
3026
Error string `json:"error"`
3127
}

service/database.go

+25-9
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ const (
1414
databaseUnlockSubject = "unlock"
1515
databaseLockSubject = "lock"
1616
databaseStatusSubject = "status"
17+
databaseRotateSubject = "rotate"
1718
DBInit DBVerb = "init"
1819
DBLock DBVerb = "lock"
1920
DBUnlock DBVerb = "unlock"
2021
DBStatus DBVerb = "status"
22+
DBRotate DBVerb = "rotate"
2123
GET Verb = "GET"
2224
POST Verb = "POST"
2325
DELETE Verb = "DELETE"
@@ -29,6 +31,7 @@ var SubjectVerbs = map[DBVerb]string{
2931
DBLock: fmt.Sprintf("%s.%s", databaseSubject, databaseLockSubject),
3032
DBUnlock: fmt.Sprintf("%s.%s", databaseSubject, databaseUnlockSubject),
3133
DBStatus: fmt.Sprintf("%s.%s", databaseSubject, databaseStatusSubject),
34+
DBRotate: fmt.Sprintf("%s.%s", databaseSubject, databaseRotateSubject),
3235
}
3336

3437
type DBVerb string
@@ -55,15 +58,18 @@ type Backend interface {
5558
}
5659

5760
func GetClientDBVerbs() []string {
58-
return []string{DBInit.String(), DBLock.String(), DBUnlock.String(), DBStatus.String()}
61+
return []string{DBInit.String(), DBLock.String(), DBUnlock.String(), DBStatus.String(), DBRotate.String()}
5962
}
6063

6164
// initialize sets the initialization key. Once this is set it does not need to be run again, unless you lose the encryption key.
6265
// If you lose the encryption key, everything is lost.
6366
func (a *AppContext) initialize() ([]byte, error) {
64-
kv := NewJSRecord().SetBucket(piggyBucket).SetKey("init")
67+
kv := JetStreamRecord{
68+
bucket: piggyBucket,
69+
key: "init",
70+
}
6571

66-
_, err := a.GetRecord(kv)
72+
_, err := a.GetRecord(&kv)
6773
if err != nil && err != nats.ErrKeyNotFound {
6874
return nil, err
6975
}
@@ -72,15 +78,21 @@ func (a *AppContext) initialize() ([]byte, error) {
7278
return nil, NewClientError(fmt.Errorf("database already initialized"), 400)
7379
}
7480

81+
a.logger.Info("generating intial key")
7582
key, random := generateKey(), generatePass()
7683

77-
record := NewJSRecord().SetEncryptionKey(key).SetBucket(piggyBucket).SetKey("init").SetValue(random)
84+
record := JetStreamRecord{
85+
encryptionKey: key,
86+
bucket: piggyBucket,
87+
key: "init",
88+
value: []byte(random),
89+
}
7890

7991
if err := record.Encrypt(); err != nil {
8092
return nil, err
8193
}
8294

83-
if err := a.AddRecord(record); err != nil {
95+
if err := a.AddRecord(&record); err != nil {
8496
return nil, err
8597
}
8698

@@ -124,9 +136,13 @@ func (a *AppContext) unlock(data []byte) error {
124136
return NewClientError(fmt.Errorf("key is too short"), 400)
125137
}
126138

127-
kv := NewJSRecord().SetBucket(piggyBucket).SetKey("init").SetValue(key.DBKey)
139+
kv := JetStreamRecord{
140+
bucket: piggyBucket,
141+
key: "init",
142+
value: []byte(key.DBKey),
143+
}
128144

129-
if err := a.Unlock(kv); err != nil {
145+
if err := a.Unlock(&kv); err != nil {
130146
return fmt.Errorf("error unlocking database: %v", err)
131147
}
132148

@@ -157,7 +173,7 @@ func (a *AppContext) AddRecord(k KV) error {
157173
}
158174

159175
// getRecord wraps GetRecord by decrypting the returned value and handling resposnes.
160-
func (a *AppContext) getRecord(k KV) ([]byte, error) {
176+
func (a *AppContext) getRecord(k KV, decryptionKey []byte) ([]byte, error) {
161177
data, err := a.GetRecord(k)
162178
if err != nil && err == nats.ErrKeyNotFound {
163179
return nil, NewClientError(fmt.Errorf("key not found"), 404)
@@ -167,7 +183,7 @@ func (a *AppContext) getRecord(k KV) ([]byte, error) {
167183
return nil, err
168184
}
169185

170-
decrypted, err := decrypt(data, databaseKey)
186+
decrypted, err := decrypt(data, decryptionKey)
171187
if err != nil {
172188
return nil, err
173189
}

service/jetstream.go

+11-44
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ package service
33
import "regexp"
44

55
type JetStreamRecord struct {
6-
bucket string
7-
key string
8-
value []byte
9-
encryption []byte
6+
bucket string
7+
key string
8+
value []byte
9+
encryptionKey []byte
1010
}
1111

1212
// NewJSRecord returns a new JetStreamRecord
@@ -29,36 +29,14 @@ func (j *JetStreamRecord) Value() []byte {
2929
return j.value
3030
}
3131

32-
// SetBucket sets the bucket field on a JetStreamRecord
33-
func (j *JetStreamRecord) SetBucket(b string) *JetStreamRecord {
34-
j.bucket = b
35-
return j
36-
}
37-
38-
// SetKey sets the key field on a JetStreamRecord
39-
func (j *JetStreamRecord) SetKey(k string) *JetStreamRecord {
40-
j.key = k
41-
return j
42-
}
43-
44-
// SetSanitizedKey removes the prefix from the key name on a JetStreamRecord
45-
// This is to keep from having the bucket name duplicated in the subject
46-
func (j *JetStreamRecord) SetSanitizedKey(k string) *JetStreamRecord {
32+
func SanitizeKey(k string) string {
4733
reg := regexp.MustCompile(`piggybank.secrets.\w+.`)
48-
subj := reg.ReplaceAllString(k, "${1}")
49-
j.key = subj
50-
return j
51-
}
52-
53-
// SetValue sets the value field on a JetStreamRecord
54-
func (j *JetStreamRecord) SetValue(v string) *JetStreamRecord {
55-
j.value = []byte(v)
56-
return j
34+
return reg.ReplaceAllString(k, "${1}")
5735
}
5836

5937
// Encrypt encrypts the value of the JetStreamRecord using the encryption key stored in the record
6038
func (j *JetStreamRecord) Encrypt() error {
61-
v, err := encrypt(j.value, j.encryption)
39+
v, err := encrypt(j.value, j.encryptionKey)
6240
if err != nil {
6341
return err
6442
}
@@ -68,22 +46,11 @@ func (j *JetStreamRecord) Encrypt() error {
6846
}
6947

7048
// Decrypt decrypts the value of the JetStreamRecord using the encryption key stored in the record
71-
func (j *JetStreamRecord) Decrypt() error {
72-
v, err := decrypt(j.value, j.encryption)
73-
if err != nil {
74-
return err
75-
}
76-
77-
j.value, err = fromBase64(string(v))
49+
func (j *JetStreamRecord) Decrypt() ([]byte, error) {
50+
v, err := decrypt(j.value, j.encryptionKey)
7851
if err != nil {
79-
return err
52+
return nil, err
8053
}
8154

82-
return nil
83-
}
84-
85-
// SetEncryptionKey sets the encryption key in the JetStreamRecord
86-
func (j *JetStreamRecord) SetEncryptionKey(k []byte) *JetStreamRecord {
87-
j.encryption = k
88-
return j
55+
return fromBase64(string(v))
8956
}

service/nats.go

+50-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package service
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"time"
67

@@ -24,6 +25,10 @@ type ResponseMessage struct {
2425
Details string `json:"details,omitempty"`
2526
}
2627

28+
type RotateRequest struct {
29+
CurrentKey string `json:"current_key"`
30+
}
31+
2732
// SecretHandler wraps any secret handlers to check if database is currently locked
2833
func SecretHandler(a AppHandlerFunc) AppHandlerFunc {
2934
return func(r micro.Request, app AppContext) error {
@@ -41,6 +46,7 @@ func Lock(r micro.Request, app AppContext) error {
4146
}
4247

4348
func Initialize(r micro.Request, app AppContext) error {
49+
app.logger.Info("initializing database")
4450
data, err := app.initialize()
4551
if err != nil {
4652
return err
@@ -49,9 +55,32 @@ func Initialize(r micro.Request, app AppContext) error {
4955
return r.RespondJSON(ResponseMessage{Details: toBase64(data)})
5056
}
5157

58+
func RotateKey(r micro.Request, app AppContext) error {
59+
var rotateReq RotateRequest
60+
61+
if err := json.Unmarshal(r.Data(), &rotateReq); err != nil {
62+
return NewClientError(fmt.Errorf("bad request"), 400)
63+
}
64+
65+
if rotateReq.CurrentKey == "" {
66+
return NewClientError(fmt.Errorf("current db key required"), 400)
67+
}
68+
69+
app.logger.Info("rotating encryption key")
70+
data, err := app.Rotate(rotateReq.CurrentKey)
71+
if err != nil {
72+
return err
73+
}
74+
75+
return r.RespondJSON(ResponseMessage{Details: toBase64(data)})
76+
}
77+
5278
func Unlock(r micro.Request, app AppContext) error {
5379
var unlocked bool
54-
kv := NewJSRecord().SetBucket(piggyBucket).SetKey("init")
80+
kv := JetStreamRecord{
81+
bucket: piggyBucket,
82+
key: "init",
83+
}
5584
if databaseKey != nil {
5685
unlocked = true
5786
}
@@ -60,7 +89,7 @@ func Unlock(r micro.Request, app AppContext) error {
6089
return NewClientError(fmt.Errorf("database already unlocked"), 400)
6190
}
6291

63-
_, err := app.GetRecord(kv)
92+
_, err := app.GetRecord(&kv)
6493
if err != nil && err != nats.ErrKeyNotFound {
6594
return err
6695
}
@@ -69,6 +98,7 @@ func Unlock(r micro.Request, app AppContext) error {
6998
return NewClientError(fmt.Errorf("database not initialized"), 400)
7099
}
71100

101+
app.logger.Info("unlocking database")
72102
if err := app.unlock(r.Data()); err != nil {
73103
return err
74104
}
@@ -82,8 +112,11 @@ func Status(r micro.Request, app AppContext) error {
82112
}
83113

84114
func GetRecord(r micro.Request, app AppContext) error {
85-
record := NewJSRecord().SetBucket(piggyBucket).SetSanitizedKey(r.Subject())
86-
decrypted, err := app.getRecord(record)
115+
record := JetStreamRecord{
116+
bucket: piggyBucket,
117+
key: SanitizeKey(r.Subject()),
118+
}
119+
decrypted, err := app.getRecord(&record, databaseKey)
87120
if err != nil {
88121
return err
89122
}
@@ -92,19 +125,26 @@ func GetRecord(r micro.Request, app AppContext) error {
92125
}
93126

94127
func AddRecord(r micro.Request, app AppContext) error {
95-
record := NewJSRecord().SetBucket(piggyBucket).SetSanitizedKey(r.Subject())
96-
record.SetValue(string(r.Data()))
97-
record.SetEncryptionKey(databaseKey)
98-
if err := app.addRecord(record); err != nil {
128+
record := JetStreamRecord{
129+
bucket: piggyBucket,
130+
key: SanitizeKey(r.Subject()),
131+
value: r.Data(),
132+
encryptionKey: databaseKey,
133+
}
134+
135+
if err := app.addRecord(&record); err != nil {
99136
return err
100137
}
101138

102139
return r.RespondJSON(ResponseMessage{Details: "successfully stored secret"})
103140
}
104141

105142
func DeleteRecord(r micro.Request, app AppContext) error {
106-
record := NewJSRecord().SetBucket(piggyBucket).SetSanitizedKey(r.Subject())
107-
if err := app.deleteRecord(record); err != nil {
143+
record := JetStreamRecord{
144+
bucket: piggyBucket,
145+
key: SanitizeKey(r.Subject()),
146+
}
147+
if err := app.deleteRecord(&record); err != nil {
108148
return err
109149
}
110150

0 commit comments

Comments
 (0)