[#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 <carpawell@nspcc.ru>
This commit is contained in:
Pavel Karpy 2022-03-21 14:53:49 +03:00 committed by Alex Vanin
parent a884ad56d9
commit 01ed366e99
6 changed files with 111 additions and 11 deletions

View file

@ -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)
}

View file

@ -33,7 +33,7 @@ func (s *TokenStore) Create(ctx context.Context, body *session.CreateRequestBody
return nil, err return nil, err
} }
value, err := packToken(body.GetExpiration(), &sk.PrivateKey) value, err := s.packToken(body.GetExpiration(), &sk.PrivateKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -1,6 +1,7 @@
package persistent package persistent
import ( import (
"crypto/ecdsa"
"time" "time"
"go.uber.org/zap" "go.uber.org/zap"
@ -9,6 +10,7 @@ import (
type cfg struct { type cfg struct {
l *zap.Logger l *zap.Logger
timeout time.Duration timeout time.Duration
privateKey *ecdsa.PrivateKey
} }
// Option allows setting optional parameters of the TokenStore. // Option allows setting optional parameters of the TokenStore.
@ -36,3 +38,11 @@ func WithTimeout(v time.Duration) Option {
c.timeout = v 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
}
}

View file

@ -1,6 +1,9 @@
package persistent package persistent
import ( import (
"crypto/aes"
"crypto/cipher"
"crypto/x509"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
@ -17,6 +20,11 @@ type TokenStore struct {
db *bbolt.DB db *bbolt.DB
l *zap.Logger l *zap.Logger
// optional AES-256 algorithm
// encryption in Galois/Counter
// Mode
gcm cipher.AEAD
} }
var sessionsBucket = []byte("sessions") 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 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. // 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 return nil
} }
t, err = unpackToken(rawToken) t, err = s.unpackToken(rawToken)
if err != nil { if err != nil {
return err return err
} }

View file

@ -12,12 +12,19 @@ import (
const expOffset = 8 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) rawKey, err := x509.MarshalECPrivateKey(key)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not marshal private key: %w", err) 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)) res := make([]byte, expOffset, expOffset+len(rawKey))
binary.LittleEndian.PutUint64(res, exp) binary.LittleEndian.PutUint64(res, exp)
@ -26,10 +33,20 @@ func packToken(exp uint64, key *ecdsa.PrivateKey) ([]byte, error) {
return res, nil return res, nil
} }
func unpackToken(raw []byte) (*storage.PrivateToken, error) { func (s *TokenStore) unpackToken(raw []byte) (*storage.PrivateToken, error) {
epoch := binary.LittleEndian.Uint64(raw[:expOffset]) 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 { if err != nil {
return nil, fmt.Errorf("could not unmarshal private key: %w", err) return nil, fmt.Errorf("could not unmarshal private key: %w", err)
} }

View file

@ -11,14 +11,16 @@ func TestPack(t *testing.T) {
key, err := keys.NewPrivateKey() key, err := keys.NewPrivateKey()
require.NoError(t, err) require.NoError(t, err)
ts := new(TokenStore)
const exp = 12345 const exp = 12345
raw, err := packToken(exp, &key.PrivateKey) raw, err := ts.packToken(exp, &key.PrivateKey)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, uint64(exp), epochFromToken(raw)) require.Equal(t, uint64(exp), epochFromToken(raw))
unpacked, err := unpackToken(raw) unpacked, err := ts.unpackToken(raw)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, uint64(exp), unpacked.ExpiredAt()) require.Equal(t, uint64(exp), unpacked.ExpiredAt())