forked from TrueCloudLab/frostfs-node
[#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:
parent
a884ad56d9
commit
01ed366e99
6 changed files with 111 additions and 11 deletions
32
pkg/services/session/storage/persistent/encryption.go
Normal file
32
pkg/services/session/storage/persistent/encryption.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Reference in a new issue