401 lines
14 KiB
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)
|
|
}
|