Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
401
cloud/maplefile-backend/pkg/maplefile/e2ee/keychain.go
Normal file
401
cloud/maplefile-backend/pkg/maplefile/e2ee/keychain.go
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
// 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue