mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-09-24 08:55:37 +00:00
fix: implement an encryption layer around the syncapi transport
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package syncapi
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/garethgeorge/backrest/gen/go/v1sync"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// encryptedStream wraps a syncCommandStreamTrait with AES-256-GCM encryption.
|
||||
// Outgoing SyncStreamItems are serialized, encrypted, and sent as SyncActionEncrypted.
|
||||
// Incoming SyncActionEncrypted messages are decrypted and deserialized back to SyncStreamItems.
|
||||
//
|
||||
// To avoid nonce reuse (since both sides share the same key), each direction
|
||||
// uses a different nonce prefix byte: the side with the lexicographically smaller
|
||||
// ECDH public key uses prefix 0x00 for sending and expects 0x01 for receiving,
|
||||
// and vice versa.
|
||||
type encryptedStream struct {
|
||||
inner syncCommandStreamTrait
|
||||
gcm cipher.AEAD
|
||||
|
||||
sendPrefix byte
|
||||
recvPrefix byte
|
||||
|
||||
sendMu sync.Mutex
|
||||
sendCounter uint64
|
||||
|
||||
recvMu sync.Mutex
|
||||
recvCounter uint64
|
||||
}
|
||||
|
||||
func newEncryptedStream(inner syncCommandStreamTrait, gcm cipher.AEAD, localIsSmaller bool) *encryptedStream {
|
||||
var sendPrefix, recvPrefix byte
|
||||
if localIsSmaller {
|
||||
sendPrefix, recvPrefix = 0x00, 0x01
|
||||
} else {
|
||||
sendPrefix, recvPrefix = 0x01, 0x00
|
||||
}
|
||||
return &encryptedStream{
|
||||
inner: inner,
|
||||
gcm: gcm,
|
||||
sendPrefix: sendPrefix,
|
||||
recvPrefix: recvPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *encryptedStream) Send(item *v1sync.SyncStreamItem) error {
|
||||
plaintext, err := proto.Marshal(item)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal for encryption: %w", err)
|
||||
}
|
||||
|
||||
s.sendMu.Lock()
|
||||
nonce := s.makeNonce(s.sendPrefix, s.sendCounter)
|
||||
s.sendCounter++
|
||||
s.sendMu.Unlock()
|
||||
|
||||
ciphertext := s.gcm.Seal(nil, nonce, plaintext, nil)
|
||||
|
||||
return s.inner.Send(&v1sync.SyncStreamItem{
|
||||
Action: &v1sync.SyncStreamItem_Encrypted{
|
||||
Encrypted: &v1sync.SyncStreamItem_SyncActionEncrypted{
|
||||
Nonce: nonce,
|
||||
Ciphertext: ciphertext,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *encryptedStream) Receive() (*v1sync.SyncStreamItem, error) {
|
||||
envelope, err := s.inner.Receive()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encrypted := envelope.GetEncrypted()
|
||||
if encrypted == nil {
|
||||
return nil, fmt.Errorf("expected encrypted message, got %T", envelope.GetAction())
|
||||
}
|
||||
|
||||
s.recvMu.Lock()
|
||||
expectedNonce := s.makeNonce(s.recvPrefix, s.recvCounter)
|
||||
s.recvCounter++
|
||||
s.recvMu.Unlock()
|
||||
|
||||
if len(encrypted.Nonce) != s.gcm.NonceSize() {
|
||||
return nil, fmt.Errorf("invalid nonce size: got %d, want %d", len(encrypted.Nonce), s.gcm.NonceSize())
|
||||
}
|
||||
|
||||
// Verify nonce matches expected counter to prevent replay/reorder attacks
|
||||
for i := range expectedNonce {
|
||||
if expectedNonce[i] != encrypted.Nonce[i] {
|
||||
return nil, fmt.Errorf("nonce mismatch: possible replay or reorder attack")
|
||||
}
|
||||
}
|
||||
|
||||
plaintext, err := s.gcm.Open(nil, encrypted.Nonce, encrypted.Ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt message: %w", err)
|
||||
}
|
||||
|
||||
var inner v1sync.SyncStreamItem
|
||||
if err := proto.Unmarshal(plaintext, &inner); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal decrypted message: %w", err)
|
||||
}
|
||||
|
||||
return &inner, nil
|
||||
}
|
||||
|
||||
// makeNonce creates a 12-byte GCM nonce. The first byte is the direction prefix
|
||||
// (0x00 or 0x01), bytes 1-3 are zero, and bytes 4-11 are the counter in big-endian.
|
||||
func (s *encryptedStream) makeNonce(prefix byte, counter uint64) []byte {
|
||||
nonce := make([]byte, s.gcm.NonceSize()) // 12 bytes for GCM
|
||||
nonce[0] = prefix
|
||||
binary.BigEndian.PutUint64(nonce[4:], counter)
|
||||
return nonce
|
||||
}
|
||||
Reference in New Issue
Block a user