Skip to content

Commit

Permalink
Rotate Encryption Key (#13)
Browse files Browse the repository at this point in the history
* add key rotation
  • Loading branch information
hooksie1 authored Aug 10, 2024
1 parent 15b741b commit 6389ea5
Show file tree
Hide file tree
Showing 219 changed files with 162,057 additions and 67 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ piggybankctl.exe
piggybankctl-darwin
piggybank*.tar.gz
piggybank*.zip
fly.toml

dist/
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/CoverWhale/logr v0.0.0-20240513164108-a4fd5504b303
github.com/briandowns/spinner v1.23.0
github.com/nats-io/jsm.go v0.1.1
github.com/nats-io/nats-server/v2 v2.10.12
github.com/nats-io/nats.go v1.36.0
github.com/segmentio/ksuid v1.0.4
github.com/spf13/cobra v1.8.1
Expand All @@ -31,8 +32,10 @@ require (
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/highwayhash v1.0.2 // indirect
github.com/minio/selfupdate v0.6.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/nats-io/jwt/v2 v2.5.5 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
Expand All @@ -52,6 +55,7 @@ require (
golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,18 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/nats-io/jsm.go v0.1.1 h1:6vjllz276SdC+3Fb3XI71p9B6toxkCruuB1K6unQEr0=
github.com/nats-io/jsm.go v0.1.1/go.mod h1:cFz5wR1pW0zLFotntS4HA7V8Wm+sf8zpF+iQJHbsS6M=
github.com/nats-io/jwt/v2 v2.5.5 h1:ROfXb50elFq5c9+1ztaUbdlrArNFl2+fQWP6B8HGEq4=
github.com/nats-io/jwt/v2 v2.5.5/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A=
github.com/nats-io/nats-server/v2 v2.10.12 h1:G6u+RDrHkw4bkwn7I911O5jqys7jJVRY6MwgndyUsnE=
github.com/nats-io/nats-server/v2 v2.10.12/go.mod h1:H1n6zXtYLFCgXcf/SF8QNTSIFuS8tyZQMN9NguUHdEs=
github.com/nats-io/nats.go v1.36.0 h1:suEUPuWzTSse/XhESwqLxXGuj8vGRuPRoG7MoRN/qyU=
github.com/nats-io/nats.go v1.36.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
Expand Down Expand Up @@ -121,6 +127,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -139,6 +146,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
Expand Down
4 changes: 0 additions & 4 deletions service/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ type Request struct {
Data []byte
}

type SecretResponse struct {
Details string `json:"details"`
}

type ResponseError struct {
Error string `json:"error"`
}
Expand Down
34 changes: 25 additions & 9 deletions service/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ const (
databaseUnlockSubject = "unlock"
databaseLockSubject = "lock"
databaseStatusSubject = "status"
databaseRotateSubject = "rotate"
DBInit DBVerb = "init"
DBLock DBVerb = "lock"
DBUnlock DBVerb = "unlock"
DBStatus DBVerb = "status"
DBRotate DBVerb = "rotate"
GET Verb = "GET"
POST Verb = "POST"
DELETE Verb = "DELETE"
Expand All @@ -29,6 +31,7 @@ var SubjectVerbs = map[DBVerb]string{
DBLock: fmt.Sprintf("%s.%s", databaseSubject, databaseLockSubject),
DBUnlock: fmt.Sprintf("%s.%s", databaseSubject, databaseUnlockSubject),
DBStatus: fmt.Sprintf("%s.%s", databaseSubject, databaseStatusSubject),
DBRotate: fmt.Sprintf("%s.%s", databaseSubject, databaseRotateSubject),
}

type DBVerb string
Expand All @@ -55,15 +58,18 @@ type Backend interface {
}

func GetClientDBVerbs() []string {
return []string{DBInit.String(), DBLock.String(), DBUnlock.String(), DBStatus.String()}
return []string{DBInit.String(), DBLock.String(), DBUnlock.String(), DBStatus.String(), DBRotate.String()}
}

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

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

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

record := NewJSRecord().SetEncryptionKey(key).SetBucket(piggyBucket).SetKey("init").SetValue(random)
record := JetStreamRecord{
encryptionKey: key,
bucket: piggyBucket,
key: "init",
value: []byte(random),
}

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

if err := a.AddRecord(record); err != nil {
if err := a.AddRecord(&record); err != nil {
return nil, err
}

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

kv := NewJSRecord().SetBucket(piggyBucket).SetKey("init").SetValue(key.DBKey)
kv := JetStreamRecord{
bucket: piggyBucket,
key: "init",
value: []byte(key.DBKey),
}

if err := a.Unlock(kv); err != nil {
if err := a.Unlock(&kv); err != nil {
return fmt.Errorf("error unlocking database: %v", err)
}

Expand Down Expand Up @@ -157,7 +173,7 @@ func (a *AppContext) AddRecord(k KV) error {
}

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

decrypted, err := decrypt(data, databaseKey)
decrypted, err := decrypt(data, decryptionKey)
if err != nil {
return nil, err
}
Expand Down
55 changes: 11 additions & 44 deletions service/jetstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package service
import "regexp"

type JetStreamRecord struct {
bucket string
key string
value []byte
encryption []byte
bucket string
key string
value []byte
encryptionKey []byte
}

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

// SetBucket sets the bucket field on a JetStreamRecord
func (j *JetStreamRecord) SetBucket(b string) *JetStreamRecord {
j.bucket = b
return j
}

// SetKey sets the key field on a JetStreamRecord
func (j *JetStreamRecord) SetKey(k string) *JetStreamRecord {
j.key = k
return j
}

// SetSanitizedKey removes the prefix from the key name on a JetStreamRecord
// This is to keep from having the bucket name duplicated in the subject
func (j *JetStreamRecord) SetSanitizedKey(k string) *JetStreamRecord {
func SanitizeKey(k string) string {
reg := regexp.MustCompile(`piggybank.secrets.\w+.`)
subj := reg.ReplaceAllString(k, "${1}")
j.key = subj
return j
}

// SetValue sets the value field on a JetStreamRecord
func (j *JetStreamRecord) SetValue(v string) *JetStreamRecord {
j.value = []byte(v)
return j
return reg.ReplaceAllString(k, "${1}")
}

// Encrypt encrypts the value of the JetStreamRecord using the encryption key stored in the record
func (j *JetStreamRecord) Encrypt() error {
v, err := encrypt(j.value, j.encryption)
v, err := encrypt(j.value, j.encryptionKey)
if err != nil {
return err
}
Expand All @@ -68,22 +46,11 @@ func (j *JetStreamRecord) Encrypt() error {
}

// Decrypt decrypts the value of the JetStreamRecord using the encryption key stored in the record
func (j *JetStreamRecord) Decrypt() error {
v, err := decrypt(j.value, j.encryption)
if err != nil {
return err
}

j.value, err = fromBase64(string(v))
func (j *JetStreamRecord) Decrypt() ([]byte, error) {
v, err := decrypt(j.value, j.encryptionKey)
if err != nil {
return err
return nil, err
}

return nil
}

// SetEncryptionKey sets the encryption key in the JetStreamRecord
func (j *JetStreamRecord) SetEncryptionKey(k []byte) *JetStreamRecord {
j.encryption = k
return j
return fromBase64(string(v))
}
60 changes: 50 additions & 10 deletions service/nats.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package service

import (
"encoding/json"
"fmt"
"time"

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

type RotateRequest struct {
CurrentKey string `json:"current_key"`
}

// SecretHandler wraps any secret handlers to check if database is currently locked
func SecretHandler(a AppHandlerFunc) AppHandlerFunc {
return func(r micro.Request, app AppContext) error {
Expand All @@ -41,6 +46,7 @@ func Lock(r micro.Request, app AppContext) error {
}

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

func RotateKey(r micro.Request, app AppContext) error {
var rotateReq RotateRequest

if err := json.Unmarshal(r.Data(), &rotateReq); err != nil {
return NewClientError(fmt.Errorf("bad request"), 400)
}

if rotateReq.CurrentKey == "" {
return NewClientError(fmt.Errorf("current db key required"), 400)
}

app.logger.Info("rotating encryption key")
data, err := app.Rotate(rotateReq.CurrentKey)
if err != nil {
return err
}

return r.RespondJSON(ResponseMessage{Details: toBase64(data)})
}

func Unlock(r micro.Request, app AppContext) error {
var unlocked bool
kv := NewJSRecord().SetBucket(piggyBucket).SetKey("init")
kv := JetStreamRecord{
bucket: piggyBucket,
key: "init",
}
if databaseKey != nil {
unlocked = true
}
Expand All @@ -60,7 +89,7 @@ func Unlock(r micro.Request, app AppContext) error {
return NewClientError(fmt.Errorf("database already unlocked"), 400)
}

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

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

func GetRecord(r micro.Request, app AppContext) error {
record := NewJSRecord().SetBucket(piggyBucket).SetSanitizedKey(r.Subject())
decrypted, err := app.getRecord(record)
record := JetStreamRecord{
bucket: piggyBucket,
key: SanitizeKey(r.Subject()),
}
decrypted, err := app.getRecord(&record, databaseKey)
if err != nil {
return err
}
Expand All @@ -92,19 +125,26 @@ func GetRecord(r micro.Request, app AppContext) error {
}

func AddRecord(r micro.Request, app AppContext) error {
record := NewJSRecord().SetBucket(piggyBucket).SetSanitizedKey(r.Subject())
record.SetValue(string(r.Data()))
record.SetEncryptionKey(databaseKey)
if err := app.addRecord(record); err != nil {
record := JetStreamRecord{
bucket: piggyBucket,
key: SanitizeKey(r.Subject()),
value: r.Data(),
encryptionKey: databaseKey,
}

if err := app.addRecord(&record); err != nil {
return err
}

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

func DeleteRecord(r micro.Request, app AppContext) error {
record := NewJSRecord().SetBucket(piggyBucket).SetSanitizedKey(r.Subject())
if err := app.deleteRecord(record); err != nil {
record := JetStreamRecord{
bucket: piggyBucket,
key: SanitizeKey(r.Subject()),
}
if err := app.deleteRecord(&record); err != nil {
return err
}

Expand Down
Loading

0 comments on commit 6389ea5

Please sign in to comment.