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
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
package persistent
|
package persistent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
Loading…
Reference in a new issue