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

401 lines
14 KiB
Go

// Package e2ee provides end-to-end encryption operations for the MapleFile SDK.
package e2ee
import (
"fmt"
)
// KeyChain holds the key encryption key derived from the user's password.
// It provides methods for decrypting keys in the E2EE chain.
type KeyChain struct {
kek []byte // Key Encryption Key derived from password
salt []byte // Password salt used for key derivation
kdfAlgorithm string // KDF algorithm used ("argon2id" or "PBKDF2-SHA256")
}
// EncryptedKey represents a key encrypted with another key.
type EncryptedKey struct {
Ciphertext []byte `json:"ciphertext"`
Nonce []byte `json:"nonce"`
}
// NewKeyChain creates a new KeyChain by deriving the KEK from the password and salt.
// This function defaults to Argon2id for backward compatibility.
// For cross-platform compatibility, use NewKeyChainWithAlgorithm instead.
func NewKeyChain(password string, salt []byte) (*KeyChain, error) {
return NewKeyChainWithAlgorithm(password, salt, Argon2IDAlgorithm)
}
// NewKeyChainWithAlgorithm creates a new KeyChain 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 NewKeyChainWithAlgorithm(password string, salt []byte, algorithm string) (*KeyChain, error) {
// Validate salt size (both algorithms use 16-byte salt)
if len(salt) != 16 {
return nil, fmt.Errorf("invalid salt size: expected 16, got %d", len(salt))
}
// Derive key encryption key from password using specified algorithm
kek, err := DeriveKeyFromPasswordWithAlgorithm(password, salt, algorithm)
if err != nil {
return nil, fmt.Errorf("failed to derive key from password: %w", err)
}
return &KeyChain{
kek: kek,
salt: salt,
kdfAlgorithm: algorithm,
}, nil
}
// Clear securely clears the KeyChain's sensitive data from memory.
// This should be called when the KeyChain is no longer needed.
func (k *KeyChain) Clear() {
if k.kek != nil {
ClearBytes(k.kek)
k.kek = nil
}
}
// DecryptMasterKey decrypts the user's master key using the KEK.
// This method auto-detects the cipher based on nonce size:
// - 12-byte nonce: ChaCha20-Poly1305 (native app)
// - 24-byte nonce: XSalsa20-Poly1305 (web frontend)
func (k *KeyChain) DecryptMasterKey(encryptedMasterKey *EncryptedKey) ([]byte, error) {
if k.kek == nil {
return nil, fmt.Errorf("keychain has been cleared")
}
// Auto-detect cipher based on nonce size
masterKey, err := DecryptWithAlgorithm(encryptedMasterKey.Ciphertext, encryptedMasterKey.Nonce, k.kek)
if err != nil {
return nil, fmt.Errorf("failed to decrypt master key: %w", err)
}
return masterKey, nil
}
// DecryptCollectionKey decrypts a collection key using the master key.
// Auto-detects cipher based on nonce size (12 for ChaCha20, 24 for XSalsa20).
func DecryptCollectionKey(encryptedCollectionKey *EncryptedKey, masterKey []byte) ([]byte, error) {
collectionKey, err := DecryptWithAlgorithm(encryptedCollectionKey.Ciphertext, encryptedCollectionKey.Nonce, masterKey)
if err != nil {
return nil, fmt.Errorf("failed to decrypt collection key: %w", err)
}
return collectionKey, nil
}
// DecryptFileKey decrypts a file key using the collection key.
// Auto-detects cipher based on nonce size (12 for ChaCha20, 24 for XSalsa20).
func DecryptFileKey(encryptedFileKey *EncryptedKey, collectionKey []byte) ([]byte, error) {
fileKey, err := DecryptWithAlgorithm(encryptedFileKey.Ciphertext, encryptedFileKey.Nonce, collectionKey)
if err != nil {
return nil, fmt.Errorf("failed to decrypt file key: %w", err)
}
return fileKey, nil
}
// DecryptPrivateKey decrypts the user's private key using the master key.
// Auto-detects cipher based on nonce size (12 for ChaCha20, 24 for XSalsa20).
func DecryptPrivateKey(encryptedPrivateKey *EncryptedKey, masterKey []byte) ([]byte, error) {
privateKey, err := DecryptWithAlgorithm(encryptedPrivateKey.Ciphertext, encryptedPrivateKey.Nonce, masterKey)
if err != nil {
return nil, fmt.Errorf("failed to decrypt private key: %w", err)
}
return privateKey, nil
}
// DecryptRecoveryKey decrypts the user's recovery key using the master key.
// Auto-detects cipher based on nonce size (12 for ChaCha20, 24 for XSalsa20).
func DecryptRecoveryKey(encryptedRecoveryKey *EncryptedKey, masterKey []byte) ([]byte, error) {
recoveryKey, err := DecryptWithAlgorithm(encryptedRecoveryKey.Ciphertext, encryptedRecoveryKey.Nonce, masterKey)
if err != nil {
return nil, fmt.Errorf("failed to decrypt recovery key: %w", err)
}
return recoveryKey, nil
}
// DecryptMasterKeyWithRecoveryKey decrypts the master key using the recovery key.
// This is used during account recovery.
// Auto-detects cipher based on nonce size (12 for ChaCha20, 24 for XSalsa20).
func DecryptMasterKeyWithRecoveryKey(encryptedMasterKey *EncryptedKey, recoveryKey []byte) ([]byte, error) {
masterKey, err := DecryptWithAlgorithm(encryptedMasterKey.Ciphertext, encryptedMasterKey.Nonce, recoveryKey)
if err != nil {
return nil, fmt.Errorf("failed to decrypt master key with recovery key: %w", err)
}
return masterKey, nil
}
// GenerateMasterKey generates a new random master key.
func GenerateMasterKey() ([]byte, error) {
return GenerateRandomBytes(MasterKeySize)
}
// GenerateCollectionKey generates a new random collection key.
func GenerateCollectionKey() ([]byte, error) {
return GenerateRandomBytes(CollectionKeySize)
}
// GenerateFileKey generates a new random file key.
func GenerateFileKey() ([]byte, error) {
return GenerateRandomBytes(FileKeySize)
}
// GenerateRecoveryKey generates a new random recovery key.
func GenerateRecoveryKey() ([]byte, error) {
return GenerateRandomBytes(RecoveryKeySize)
}
// GenerateSalt generates a new random salt for password derivation.
func GenerateSalt() ([]byte, error) {
return GenerateRandomBytes(Argon2SaltSize)
}
// EncryptMasterKey encrypts a master key with the KEK.
func (k *KeyChain) EncryptMasterKey(masterKey []byte) (*EncryptedKey, error) {
if k.kek == nil {
return nil, fmt.Errorf("keychain has been cleared")
}
encrypted, err := Encrypt(masterKey, k.kek)
if err != nil {
return nil, fmt.Errorf("failed to encrypt master key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// EncryptCollectionKey encrypts a collection key with the master key using ChaCha20-Poly1305.
// For web frontend compatibility, use EncryptCollectionKeySecretBox instead.
func EncryptCollectionKey(collectionKey, masterKey []byte) (*EncryptedKey, error) {
encrypted, err := Encrypt(collectionKey, masterKey)
if err != nil {
return nil, fmt.Errorf("failed to encrypt collection key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// EncryptCollectionKeySecretBox encrypts a collection key with the master key using XSalsa20-Poly1305.
// This is compatible with the web frontend's libsodium implementation.
func EncryptCollectionKeySecretBox(collectionKey, masterKey []byte) (*EncryptedKey, error) {
encrypted, err := EncryptWithSecretBox(collectionKey, masterKey)
if err != nil {
return nil, fmt.Errorf("failed to encrypt collection key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// EncryptFileKey encrypts a file key with the collection key.
// NOTE: This uses ChaCha20-Poly1305 (12-byte nonce). For web frontend compatibility,
// use EncryptFileKeySecretBox instead.
func EncryptFileKey(fileKey, collectionKey []byte) (*EncryptedKey, error) {
encrypted, err := Encrypt(fileKey, collectionKey)
if err != nil {
return nil, fmt.Errorf("failed to encrypt file key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// EncryptFileKeySecretBox encrypts a file key with the collection key using XSalsa20-Poly1305.
// This is compatible with the web frontend's libsodium implementation.
func EncryptFileKeySecretBox(fileKey, collectionKey []byte) (*EncryptedKey, error) {
encrypted, err := EncryptWithSecretBox(fileKey, collectionKey)
if err != nil {
return nil, fmt.Errorf("failed to encrypt file key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// EncryptPrivateKey encrypts a private key with the master key.
func EncryptPrivateKey(privateKey, masterKey []byte) (*EncryptedKey, error) {
encrypted, err := Encrypt(privateKey, masterKey)
if err != nil {
return nil, fmt.Errorf("failed to encrypt private key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// EncryptRecoveryKey encrypts a recovery key with the master key.
func EncryptRecoveryKey(recoveryKey, masterKey []byte) (*EncryptedKey, error) {
encrypted, err := Encrypt(recoveryKey, masterKey)
if err != nil {
return nil, fmt.Errorf("failed to encrypt recovery key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// EncryptMasterKeyWithRecoveryKey encrypts a master key with the recovery key.
// This is used to enable account recovery.
func EncryptMasterKeyWithRecoveryKey(masterKey, recoveryKey []byte) (*EncryptedKey, error) {
encrypted, err := Encrypt(masterKey, recoveryKey)
if err != nil {
return nil, fmt.Errorf("failed to encrypt master key with recovery key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// =============================================================================
// SecretBox (XSalsa20-Poly1305) Encryption Functions
// These match the web frontend's libsodium crypto_secretbox_easy implementation
// =============================================================================
// EncryptMasterKeySecretBox encrypts a master key with the KEK using XSalsa20-Poly1305.
// This is compatible with the web frontend's libsodium implementation.
func (k *KeyChain) EncryptMasterKeySecretBox(masterKey []byte) (*EncryptedKey, error) {
if k.kek == nil {
return nil, fmt.Errorf("keychain has been cleared")
}
encrypted, err := EncryptWithSecretBox(masterKey, k.kek)
if err != nil {
return nil, fmt.Errorf("failed to encrypt master key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// EncryptPrivateKeySecretBox encrypts a private key with the master key using XSalsa20-Poly1305.
func EncryptPrivateKeySecretBox(privateKey, masterKey []byte) (*EncryptedKey, error) {
encrypted, err := EncryptWithSecretBox(privateKey, masterKey)
if err != nil {
return nil, fmt.Errorf("failed to encrypt private key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// EncryptRecoveryKeySecretBox encrypts a recovery key with the master key using XSalsa20-Poly1305.
func EncryptRecoveryKeySecretBox(recoveryKey, masterKey []byte) (*EncryptedKey, error) {
encrypted, err := EncryptWithSecretBox(recoveryKey, masterKey)
if err != nil {
return nil, fmt.Errorf("failed to encrypt recovery key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// EncryptMasterKeyWithRecoveryKeySecretBox encrypts a master key with the recovery key using XSalsa20-Poly1305.
func EncryptMasterKeyWithRecoveryKeySecretBox(masterKey, recoveryKey []byte) (*EncryptedKey, error) {
encrypted, err := EncryptWithSecretBox(masterKey, recoveryKey)
if err != nil {
return nil, fmt.Errorf("failed to encrypt master key with recovery key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// EncryptCollectionKeyForSharing encrypts a collection key for a recipient using BoxSeal.
// This is used when sharing a collection with another user.
func EncryptCollectionKeyForSharing(collectionKey, recipientPublicKey []byte) ([]byte, error) {
if len(recipientPublicKey) != BoxPublicKeySize {
return nil, fmt.Errorf("invalid recipient public key size: expected %d, got %d", BoxPublicKeySize, len(recipientPublicKey))
}
return EncryptWithBoxSeal(collectionKey, recipientPublicKey)
}
// DecryptSharedCollectionKey decrypts a collection key that was shared using BoxSeal.
// This is used when accessing a shared collection.
func DecryptSharedCollectionKey(encryptedCollectionKey, publicKey, privateKey []byte) ([]byte, error) {
return DecryptWithBoxSeal(encryptedCollectionKey, publicKey, privateKey)
}
// ============================================================================
// Tag Key Operations
// ============================================================================
// GenerateTagKey generates a new 32-byte tag key for encrypting tag data.
func GenerateTagKey() ([]byte, error) {
return GenerateRandomBytes(SecretBoxKeySize)
}
// GenerateKey is an alias for GenerateTagKey (convenience function).
func GenerateKey() []byte {
key, _ := GenerateTagKey()
return key
}
// EncryptTagKey encrypts a tag key with the master key using ChaCha20-Poly1305.
func EncryptTagKey(tagKey, masterKey []byte) (*EncryptedKey, error) {
encrypted, err := Encrypt(tagKey, masterKey)
if err != nil {
return nil, fmt.Errorf("failed to encrypt tag key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// EncryptTagKeySecretBox encrypts a tag key with the master key using XSalsa20-Poly1305.
func EncryptTagKeySecretBox(tagKey, masterKey []byte) (*EncryptedKey, error) {
encrypted, err := EncryptWithSecretBox(tagKey, masterKey)
if err != nil {
return nil, fmt.Errorf("failed to encrypt tag key: %w", err)
}
return &EncryptedKey{
Ciphertext: encrypted.Ciphertext,
Nonce: encrypted.Nonce,
}, nil
}
// DecryptTagKey decrypts a tag key with the master key.
func DecryptTagKey(encryptedTagKey *EncryptedKey, masterKey []byte) ([]byte, error) {
// Try XSalsa20-Poly1305 first (based on nonce size)
if len(encryptedTagKey.Nonce) == SecretBoxNonceSize {
return DecryptWithSecretBox(encryptedTagKey.Ciphertext, encryptedTagKey.Nonce, masterKey)
}
// Fall back to ChaCha20-Poly1305
return Decrypt(encryptedTagKey.Ciphertext, encryptedTagKey.Nonce, masterKey)
}