// 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 }