From 01ed366e99091802275ff6428620b7e892fa0354 Mon Sep 17 00:00:00 2001 From: Pavel Karpy Date: Mon, 21 Mar 2022 14:53:49 +0300 Subject: [PATCH] [#1255] node/session: Add encryption Add `WithEncryption` option that passes ECDSA key to the persistent session storage. It uses 32 bytes from marshalled ECDSA key in ASN.1 DER from in AES-256 algorithm encryption in Galois/Counter Mode. Signed-off-by: Pavel Karpy --- .../session/storage/persistent/encryption.go | 32 ++++++++++++++ .../session/storage/persistent/executor.go | 2 +- .../session/storage/persistent/options.go | 14 +++++- .../session/storage/persistent/storage.go | 43 ++++++++++++++++++- .../session/storage/persistent/util.go | 25 +++++++++-- .../session/storage/persistent/util_test.go | 6 ++- 6 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 pkg/services/session/storage/persistent/encryption.go diff --git a/pkg/services/session/storage/persistent/encryption.go b/pkg/services/session/storage/persistent/encryption.go new file mode 100644 index 000000000..ff16b1852 --- /dev/null +++ b/pkg/services/session/storage/persistent/encryption.go @@ -0,0 +1,32 @@ +package persistent + +import ( + "crypto/rand" + "fmt" + "io" +) + +func (s *TokenStore) encrypt(value []byte) ([]byte, error) { + nonce := make([]byte, s.gcm.NonceSize()) + + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("could not init random nonce: %w", err) + } + + return s.gcm.Seal(nonce, nonce, value, nil), nil +} + +func (s *TokenStore) decrypt(value []byte) ([]byte, error) { + nonceSize := s.gcm.NonceSize() + if len(value) < nonceSize { + return nil, fmt.Errorf( + "unexpected encrypted length: nonce length is %d, encrypted data lenght is %d", + nonceSize, + len(value), + ) + } + + nonce, encryptedData := value[:nonceSize], value[nonceSize:] + + return s.gcm.Open(nil, nonce, encryptedData, nil) +} diff --git a/pkg/services/session/storage/persistent/executor.go b/pkg/services/session/storage/persistent/executor.go index c414fe08f..415a428ee 100644 --- a/pkg/services/session/storage/persistent/executor.go +++ b/pkg/services/session/storage/persistent/executor.go @@ -33,7 +33,7 @@ func (s *TokenStore) Create(ctx context.Context, body *session.CreateRequestBody return nil, err } - value, err := packToken(body.GetExpiration(), &sk.PrivateKey) + value, err := s.packToken(body.GetExpiration(), &sk.PrivateKey) if err != nil { return nil, err } diff --git a/pkg/services/session/storage/persistent/options.go b/pkg/services/session/storage/persistent/options.go index 9f47961d3..61a0969c9 100644 --- a/pkg/services/session/storage/persistent/options.go +++ b/pkg/services/session/storage/persistent/options.go @@ -1,14 +1,16 @@ package persistent import ( + "crypto/ecdsa" "time" "go.uber.org/zap" ) type cfg struct { - l *zap.Logger - timeout time.Duration + l *zap.Logger + timeout time.Duration + privateKey *ecdsa.PrivateKey } // Option allows setting optional parameters of the TokenStore. @@ -36,3 +38,11 @@ func WithTimeout(v time.Duration) Option { c.timeout = v } } + +// WithEncryptionKey return an option to encrypt private +// session keys using provided private key. +func WithEncryptionKey(k *ecdsa.PrivateKey) Option { + return func(c *cfg) { + c.privateKey = k + } +} diff --git a/pkg/services/session/storage/persistent/storage.go b/pkg/services/session/storage/persistent/storage.go index 6f0c86d0f..05ab607d5 100644 --- a/pkg/services/session/storage/persistent/storage.go +++ b/pkg/services/session/storage/persistent/storage.go @@ -1,6 +1,9 @@ package persistent import ( + "crypto/aes" + "crypto/cipher" + "crypto/x509" "encoding/hex" "fmt" @@ -17,6 +20,11 @@ type TokenStore struct { db *bbolt.DB l *zap.Logger + + // optional AES-256 algorithm + // encryption in Galois/Counter + // Mode + gcm cipher.AEAD } var sessionsBucket = []byte("sessions") @@ -49,7 +57,38 @@ func NewTokenStore(path string, opts ...Option) (*TokenStore, error) { return nil, fmt.Errorf("could not init session bucket: %w", err) } - return &TokenStore{db: db, l: cfg.l}, nil + ts := &TokenStore{db: db, l: cfg.l} + + // enable encryption if it + // was configured so + if cfg.privateKey != nil { + rawKey, err := x509.MarshalECPrivateKey(cfg.privateKey) + if err != nil { + return nil, fmt.Errorf("could not marshal provided private key: %w", err) + } + + // tagOffset is a constant offset for + // tags when marshalling ECDSA key in + // ASN.1 DER form + const tagOffset = 7 + + // using first 32 bytes from + // the marshalled private key + // as a secret + c, err := aes.NewCipher(rawKey[tagOffset : tagOffset+32]) + if err != nil { + return nil, fmt.Errorf("could not create cipher block: %w", err) + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, fmt.Errorf("could not wrapp cipher block in Galois Counter Mode: %w", err) + } + + ts.gcm = gcm + } + + return ts, nil } // Get returns private token corresponding to the given identifiers. @@ -74,7 +113,7 @@ func (s *TokenStore) Get(ownerID *ownerSDK.ID, tokenID []byte) (t *storage.Priva return nil } - t, err = unpackToken(rawToken) + t, err = s.unpackToken(rawToken) if err != nil { return err } diff --git a/pkg/services/session/storage/persistent/util.go b/pkg/services/session/storage/persistent/util.go index 53ab9101d..3180497f0 100644 --- a/pkg/services/session/storage/persistent/util.go +++ b/pkg/services/session/storage/persistent/util.go @@ -12,12 +12,19 @@ import ( const expOffset = 8 -func packToken(exp uint64, key *ecdsa.PrivateKey) ([]byte, error) { +func (s *TokenStore) packToken(exp uint64, key *ecdsa.PrivateKey) ([]byte, error) { rawKey, err := x509.MarshalECPrivateKey(key) if err != nil { return nil, fmt.Errorf("could not marshal private key: %w", err) } + if s.gcm != nil { + rawKey, err = s.encrypt(rawKey) + if err != nil { + return nil, fmt.Errorf("could not encrypt session key: %w", err) + } + } + res := make([]byte, expOffset, expOffset+len(rawKey)) binary.LittleEndian.PutUint64(res, exp) @@ -26,10 +33,20 @@ func packToken(exp uint64, key *ecdsa.PrivateKey) ([]byte, error) { return res, nil } -func unpackToken(raw []byte) (*storage.PrivateToken, error) { - epoch := binary.LittleEndian.Uint64(raw[:expOffset]) +func (s *TokenStore) unpackToken(raw []byte) (*storage.PrivateToken, error) { + var err error - key, err := x509.ParseECPrivateKey(raw[expOffset:]) + epoch := epochFromToken(raw) + rawKey := raw[expOffset:] + + if s.gcm != nil { + rawKey, err = s.decrypt(rawKey) + if err != nil { + return nil, fmt.Errorf("could not decrypt session key: %w", err) + } + } + + key, err := x509.ParseECPrivateKey(rawKey) if err != nil { return nil, fmt.Errorf("could not unmarshal private key: %w", err) } diff --git a/pkg/services/session/storage/persistent/util_test.go b/pkg/services/session/storage/persistent/util_test.go index 9842ddaf8..0cb81e7f9 100644 --- a/pkg/services/session/storage/persistent/util_test.go +++ b/pkg/services/session/storage/persistent/util_test.go @@ -11,14 +11,16 @@ func TestPack(t *testing.T) { key, err := keys.NewPrivateKey() require.NoError(t, err) + ts := new(TokenStore) + const exp = 12345 - raw, err := packToken(exp, &key.PrivateKey) + raw, err := ts.packToken(exp, &key.PrivateKey) require.NoError(t, err) require.Equal(t, uint64(exp), epochFromToken(raw)) - unpacked, err := unpackToken(raw) + unpacked, err := ts.unpackToken(raw) require.NoError(t, err) require.Equal(t, uint64(exp), unpacked.ExpiredAt())