monorepo/cloud/maplefile-backend/pkg/maplefile/e2ee/secure.go

246 lines
7.7 KiB
Go

// Package e2ee provides end-to-end encryption operations for the MapleFile SDK.
// This file contains memguard-protected secure memory operations.
package e2ee
import (
"fmt"
"github.com/awnumar/memguard"
)
// SecureBuffer wraps memguard.LockedBuffer for type safety
type SecureBuffer struct {
buffer *memguard.LockedBuffer
}
// NewSecureBuffer creates a new secure buffer from bytes
func NewSecureBuffer(data []byte) (*SecureBuffer, error) {
if len(data) == 0 {
return nil, fmt.Errorf("cannot create secure buffer from empty data")
}
buffer := memguard.NewBufferFromBytes(data)
return &SecureBuffer{buffer: buffer}, nil
}
// NewSecureBufferRandom creates a new secure buffer with random data
func NewSecureBufferRandom(size int) (*SecureBuffer, error) {
if size <= 0 {
return nil, fmt.Errorf("size must be positive")
}
buffer := memguard.NewBuffer(size)
return &SecureBuffer{buffer: buffer}, nil
}
// Bytes returns the underlying bytes (caller must handle carefully)
func (s *SecureBuffer) Bytes() []byte {
if s.buffer == nil {
return nil
}
return s.buffer.Bytes()
}
// Size returns the size of the buffer
func (s *SecureBuffer) Size() int {
if s.buffer == nil {
return 0
}
return s.buffer.Size()
}
// Destroy securely destroys the buffer
func (s *SecureBuffer) Destroy() {
if s.buffer != nil {
s.buffer.Destroy()
s.buffer = nil
}
}
// Copy creates a new SecureBuffer with a copy of the data
func (s *SecureBuffer) Copy() (*SecureBuffer, error) {
if s.buffer == nil {
return nil, fmt.Errorf("cannot copy destroyed buffer")
}
return NewSecureBuffer(s.buffer.Bytes())
}
// SecureKeyChain is a KeyChain that stores the KEK in protected memory
type SecureKeyChain struct {
kek *SecureBuffer // Key Encryption Key in protected memory
salt []byte // Salt (not sensitive, kept in regular memory)
kdfAlgorithm string // KDF algorithm used
}
// NewSecureKeyChain creates a new SecureKeyChain with KEK in protected memory.
// This function defaults to Argon2id for backward compatibility.
// For cross-platform compatibility, use NewSecureKeyChainWithAlgorithm instead.
func NewSecureKeyChain(password string, salt []byte) (*SecureKeyChain, error) {
return NewSecureKeyChainWithAlgorithm(password, salt, Argon2IDAlgorithm)
}
// NewSecureKeyChainWithAlgorithm creates a new SecureKeyChain using the specified KDF algorithm.
// algorithm should be one of: Argon2IDAlgorithm ("argon2id") or PBKDF2Algorithm ("PBKDF2-SHA256").
// The web frontend uses PBKDF2-SHA256, while the native app historically used Argon2id.
func NewSecureKeyChainWithAlgorithm(password string, salt []byte, algorithm string) (*SecureKeyChain, error) {
// Both algorithms use 16-byte salt
if len(salt) != 16 {
return nil, fmt.Errorf("invalid salt size: expected 16, got %d", len(salt))
}
// Derive KEK from password using specified algorithm
kekBytes, err := DeriveKeyFromPasswordWithAlgorithm(password, salt, algorithm)
if err != nil {
return nil, fmt.Errorf("failed to derive key from password: %w", err)
}
// Store KEK in secure memory immediately
kek, err := NewSecureBuffer(kekBytes)
if err != nil {
ClearBytes(kekBytes)
return nil, fmt.Errorf("failed to create secure buffer for KEK: %w", err)
}
// Clear the temporary KEK bytes
ClearBytes(kekBytes)
return &SecureKeyChain{
kek: kek,
salt: salt,
kdfAlgorithm: algorithm,
}, nil
}
// Clear securely clears the SecureKeyChain's sensitive data
func (k *SecureKeyChain) Clear() {
if k.kek != nil {
k.kek.Destroy()
k.kek = nil
}
}
// DecryptMasterKeySecure decrypts the master key and returns it in a SecureBuffer.
// Auto-detects cipher based on nonce size (12 for ChaCha20, 24 for XSalsa20).
func (k *SecureKeyChain) DecryptMasterKeySecure(encryptedMasterKey *EncryptedKey) (*SecureBuffer, error) {
if k.kek == nil || k.kek.buffer == nil {
return nil, fmt.Errorf("keychain has been cleared")
}
// Decrypt using KEK from secure memory (auto-detect cipher based on nonce size)
masterKeyBytes, err := DecryptWithAlgorithm(encryptedMasterKey.Ciphertext, encryptedMasterKey.Nonce, k.kek.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to decrypt master key: %w", err)
}
// Store decrypted master key in secure memory
masterKey, err := NewSecureBuffer(masterKeyBytes)
if err != nil {
ClearBytes(masterKeyBytes)
return nil, fmt.Errorf("failed to create secure buffer for master key: %w", err)
}
// Clear temporary bytes
ClearBytes(masterKeyBytes)
return masterKey, nil
}
// DecryptMasterKey provides backward compatibility by returning []byte.
// For new code, prefer DecryptMasterKeySecure.
// Auto-detects cipher based on nonce size (12 for ChaCha20, 24 for XSalsa20).
func (k *SecureKeyChain) DecryptMasterKey(encryptedMasterKey *EncryptedKey) ([]byte, error) {
if k.kek == nil || k.kek.buffer == nil {
return nil, fmt.Errorf("keychain has been cleared")
}
// Decrypt using KEK from secure memory (auto-detect cipher)
return DecryptWithAlgorithm(encryptedMasterKey.Ciphertext, encryptedMasterKey.Nonce, k.kek.Bytes())
}
// EncryptMasterKey encrypts a master key with the KEK using ChaCha20-Poly1305.
// For web frontend compatibility, use EncryptMasterKeySecretBox instead.
func (k *SecureKeyChain) EncryptMasterKey(masterKey []byte) (*EncryptedKey, error) {
if k.kek == nil || k.kek.buffer == nil {
return nil, fmt.Errorf("keychain has been cleared")
}
encrypted, err := Encrypt(masterKey, k.kek.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to encrypt master key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// EncryptMasterKeySecretBox encrypts a master key with the KEK using XSalsa20-Poly1305 (SecretBox).
// This is compatible with the web frontend's libsodium implementation.
func (k *SecureKeyChain) EncryptMasterKeySecretBox(masterKey []byte) (*EncryptedKey, error) {
if k.kek == nil || k.kek.buffer == nil {
return nil, fmt.Errorf("keychain has been cleared")
}
encrypted, err := EncryptWithSecretBox(masterKey, k.kek.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to encrypt master key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// DecryptPrivateKeySecure decrypts a private key and returns it in a SecureBuffer.
// Auto-detects cipher based on nonce size (12 for ChaCha20, 24 for XSalsa20).
func DecryptPrivateKeySecure(encryptedPrivateKey *EncryptedKey, masterKey *SecureBuffer) (*SecureBuffer, error) {
if masterKey == nil || masterKey.buffer == nil {
return nil, fmt.Errorf("master key is nil or destroyed")
}
// Decrypt private key (auto-detect cipher based on nonce size)
privateKeyBytes, err := DecryptWithAlgorithm(encryptedPrivateKey.Ciphertext, encryptedPrivateKey.Nonce, masterKey.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to decrypt private key: %w", err)
}
// Store in secure memory
privateKey, err := NewSecureBuffer(privateKeyBytes)
if err != nil {
ClearBytes(privateKeyBytes)
return nil, fmt.Errorf("failed to create secure buffer for private key: %w", err)
}
// Clear temporary bytes
ClearBytes(privateKeyBytes)
return privateKey, nil
}
// WithSecureBuffer provides a callback pattern for temporary use of secure data
// The buffer is automatically destroyed after the callback returns
func WithSecureBuffer(data []byte, fn func(*SecureBuffer) error) error {
buf, err := NewSecureBuffer(data)
if err != nil {
return err
}
defer buf.Destroy()
return fn(buf)
}
// CopyToSecure copies regular bytes into a new SecureBuffer and clears the source
func CopyToSecure(data []byte) (*SecureBuffer, error) {
buf, err := NewSecureBuffer(data)
if err != nil {
return nil, err
}
// Clear the source data
ClearBytes(data)
return buf, nil
}