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
462
cloud/maplefile-backend/pkg/maplefile/e2ee/crypto.go
Normal file
462
cloud/maplefile-backend/pkg/maplefile/e2ee/crypto.go
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
// Package e2ee provides end-to-end encryption operations for the MapleFile SDK.
|
||||
package e2ee
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/awnumar/memguard"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
// KDF Algorithm identifiers
|
||||
const (
|
||||
Argon2IDAlgorithm = "argon2id"
|
||||
PBKDF2Algorithm = "PBKDF2-SHA256"
|
||||
)
|
||||
|
||||
// Argon2id key derivation parameters
|
||||
const (
|
||||
Argon2MemLimit = 4 * 1024 * 1024 // 4 MB
|
||||
Argon2OpsLimit = 1 // 1 iteration (time cost)
|
||||
Argon2Parallelism = 1 // 1 thread
|
||||
Argon2KeySize = 32 // 256-bit output
|
||||
Argon2SaltSize = 16 // 128-bit salt
|
||||
)
|
||||
|
||||
// PBKDF2 key derivation parameters (matching web frontend)
|
||||
const (
|
||||
PBKDF2Iterations = 100000 // 100,000 iterations (matching web frontend)
|
||||
PBKDF2KeySize = 32 // 256-bit output
|
||||
PBKDF2SaltSize = 16 // 128-bit salt
|
||||
)
|
||||
|
||||
// ChaCha20-Poly1305 constants (IETF variant - 12 byte nonce)
|
||||
const (
|
||||
ChaCha20Poly1305KeySize = 32 // ChaCha20 key size
|
||||
ChaCha20Poly1305NonceSize = 12 // ChaCha20-Poly1305 nonce size
|
||||
ChaCha20Poly1305Overhead = 16 // Poly1305 authentication tag size
|
||||
)
|
||||
|
||||
// XSalsa20-Poly1305 (NaCl secretbox) constants - 24 byte nonce
|
||||
// Used by web frontend (libsodium crypto_secretbox_easy)
|
||||
const (
|
||||
SecretBoxKeySize = 32 // Same as ChaCha20
|
||||
SecretBoxNonceSize = 24 // XSalsa20 uses 24-byte nonce
|
||||
SecretBoxOverhead = secretbox.Overhead // 16 bytes (Poly1305 tag)
|
||||
)
|
||||
|
||||
// Key sizes
|
||||
const (
|
||||
MasterKeySize = 32
|
||||
CollectionKeySize = 32
|
||||
FileKeySize = 32
|
||||
RecoveryKeySize = 32
|
||||
)
|
||||
|
||||
// NaCl Box constants
|
||||
const (
|
||||
BoxPublicKeySize = 32
|
||||
BoxSecretKeySize = 32
|
||||
BoxNonceSize = 24
|
||||
)
|
||||
|
||||
// EncryptedData represents encrypted data with its nonce.
|
||||
type EncryptedData struct {
|
||||
Ciphertext []byte
|
||||
Nonce []byte
|
||||
}
|
||||
|
||||
// DeriveKeyFromPassword derives a key encryption key (KEK) from a password using Argon2id.
|
||||
// This is the legacy function - prefer DeriveKeyFromPasswordWithAlgorithm for new code.
|
||||
func DeriveKeyFromPassword(password string, salt []byte) ([]byte, error) {
|
||||
return DeriveKeyFromPasswordArgon2id(password, salt)
|
||||
}
|
||||
|
||||
// DeriveKeyFromPasswordArgon2id derives a KEK using Argon2id algorithm.
|
||||
// SECURITY: Password bytes are wiped from memory after key derivation.
|
||||
func DeriveKeyFromPasswordArgon2id(password string, salt []byte) ([]byte, error) {
|
||||
if len(salt) != Argon2SaltSize {
|
||||
return nil, fmt.Errorf("invalid salt size: expected %d, got %d", Argon2SaltSize, len(salt))
|
||||
}
|
||||
|
||||
passwordBytes := []byte(password)
|
||||
defer memguard.WipeBytes(passwordBytes) // SECURITY: Wipe password bytes after use
|
||||
|
||||
key := argon2.IDKey(
|
||||
passwordBytes,
|
||||
salt,
|
||||
Argon2OpsLimit, // time cost = 1
|
||||
Argon2MemLimit, // memory = 4 MB
|
||||
Argon2Parallelism, // parallelism = 1
|
||||
Argon2KeySize, // output size = 32 bytes
|
||||
)
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// DeriveKeyFromPasswordPBKDF2 derives a KEK using PBKDF2-SHA256 algorithm.
|
||||
// This matches the web frontend's implementation.
|
||||
// SECURITY: Password bytes are wiped from memory after key derivation.
|
||||
func DeriveKeyFromPasswordPBKDF2(password string, salt []byte) ([]byte, error) {
|
||||
if len(salt) != PBKDF2SaltSize {
|
||||
return nil, fmt.Errorf("invalid salt size: expected %d, got %d", PBKDF2SaltSize, len(salt))
|
||||
}
|
||||
|
||||
passwordBytes := []byte(password)
|
||||
defer memguard.WipeBytes(passwordBytes) // SECURITY: Wipe password bytes after use
|
||||
|
||||
key := pbkdf2.Key(
|
||||
passwordBytes,
|
||||
salt,
|
||||
PBKDF2Iterations, // 100,000 iterations
|
||||
PBKDF2KeySize, // 32 bytes output
|
||||
sha256.New, // SHA-256 hash
|
||||
)
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// DeriveKeyFromPasswordWithAlgorithm derives a KEK using the specified algorithm.
|
||||
// algorithm should be one of: Argon2IDAlgorithm, PBKDF2Algorithm
|
||||
func DeriveKeyFromPasswordWithAlgorithm(password string, salt []byte, algorithm string) ([]byte, error) {
|
||||
switch algorithm {
|
||||
case Argon2IDAlgorithm: // "argon2id"
|
||||
return DeriveKeyFromPasswordArgon2id(password, salt)
|
||||
case PBKDF2Algorithm, "pbkdf2", "pbkdf2-sha256":
|
||||
return DeriveKeyFromPasswordPBKDF2(password, salt)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported KDF algorithm: %s", algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt encrypts data with a symmetric key using ChaCha20-Poly1305.
|
||||
func Encrypt(data, key []byte) (*EncryptedData, error) {
|
||||
if len(key) != ChaCha20Poly1305KeySize {
|
||||
return nil, fmt.Errorf("invalid key size: expected %d, got %d", ChaCha20Poly1305KeySize, len(key))
|
||||
}
|
||||
|
||||
// Create ChaCha20-Poly1305 cipher
|
||||
cipher, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
// Generate random nonce (12 bytes for ChaCha20-Poly1305)
|
||||
nonce, err := GenerateRandomBytes(ChaCha20Poly1305NonceSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate nonce: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt
|
||||
ciphertext := cipher.Seal(nil, nonce, data, nil)
|
||||
|
||||
return &EncryptedData{
|
||||
Ciphertext: ciphertext,
|
||||
Nonce: nonce,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts data with a symmetric key using ChaCha20-Poly1305.
|
||||
func Decrypt(ciphertext, nonce, key []byte) ([]byte, error) {
|
||||
if len(key) != ChaCha20Poly1305KeySize {
|
||||
return nil, fmt.Errorf("invalid key size: expected %d, got %d", ChaCha20Poly1305KeySize, len(key))
|
||||
}
|
||||
|
||||
if len(nonce) != ChaCha20Poly1305NonceSize {
|
||||
return nil, fmt.Errorf("invalid nonce size: expected %d, got %d", ChaCha20Poly1305NonceSize, len(nonce))
|
||||
}
|
||||
|
||||
// Create ChaCha20-Poly1305 cipher
|
||||
cipher, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt
|
||||
plaintext, err := cipher.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt: %w", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// EncryptWithSecretBox encrypts data with a symmetric key using XSalsa20-Poly1305 (NaCl secretbox).
|
||||
// This is compatible with libsodium's crypto_secretbox_easy used by the web frontend.
|
||||
// SECURITY: Key arrays are wiped from memory after encryption.
|
||||
func EncryptWithSecretBox(data, key []byte) (*EncryptedData, error) {
|
||||
if len(key) != SecretBoxKeySize {
|
||||
return nil, fmt.Errorf("invalid key size: expected %d, got %d", SecretBoxKeySize, len(key))
|
||||
}
|
||||
|
||||
// Generate random nonce (24 bytes for XSalsa20)
|
||||
nonce, err := GenerateRandomBytes(SecretBoxNonceSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate nonce: %w", err)
|
||||
}
|
||||
|
||||
// Convert to fixed-size arrays for NaCl
|
||||
var keyArray [32]byte
|
||||
var nonceArray [24]byte
|
||||
copy(keyArray[:], key)
|
||||
copy(nonceArray[:], nonce)
|
||||
defer memguard.WipeBytes(keyArray[:]) // SECURITY: Wipe key array
|
||||
|
||||
// Encrypt using secretbox
|
||||
ciphertext := secretbox.Seal(nil, data, &nonceArray, &keyArray)
|
||||
|
||||
return &EncryptedData{
|
||||
Ciphertext: ciphertext,
|
||||
Nonce: nonce,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecryptWithSecretBox decrypts data with a symmetric key using XSalsa20-Poly1305 (NaCl secretbox).
|
||||
// This is compatible with libsodium's crypto_secretbox_open_easy used by the web frontend.
|
||||
// SECURITY: Key arrays are wiped from memory after decryption.
|
||||
func DecryptWithSecretBox(ciphertext, nonce, key []byte) ([]byte, error) {
|
||||
if len(key) != SecretBoxKeySize {
|
||||
return nil, fmt.Errorf("invalid key size: expected %d, got %d", SecretBoxKeySize, len(key))
|
||||
}
|
||||
|
||||
if len(nonce) != SecretBoxNonceSize {
|
||||
return nil, fmt.Errorf("invalid nonce size: expected %d, got %d", SecretBoxNonceSize, len(nonce))
|
||||
}
|
||||
|
||||
// Convert to fixed-size arrays for NaCl
|
||||
var keyArray [32]byte
|
||||
var nonceArray [24]byte
|
||||
copy(keyArray[:], key)
|
||||
copy(nonceArray[:], nonce)
|
||||
defer memguard.WipeBytes(keyArray[:]) // SECURITY: Wipe key array
|
||||
|
||||
// Decrypt using secretbox
|
||||
plaintext, ok := secretbox.Open(nil, ciphertext, &nonceArray, &keyArray)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to decrypt: invalid key, nonce, or corrupted ciphertext")
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// DecryptWithAlgorithm decrypts data using the appropriate cipher based on nonce size.
|
||||
// - 12-byte nonce: ChaCha20-Poly1305 (IETF variant)
|
||||
// - 24-byte nonce: XSalsa20-Poly1305 (NaCl secretbox)
|
||||
func DecryptWithAlgorithm(ciphertext, nonce, key []byte) ([]byte, error) {
|
||||
switch len(nonce) {
|
||||
case ChaCha20Poly1305NonceSize: // 12 bytes
|
||||
return Decrypt(ciphertext, nonce, key)
|
||||
case SecretBoxNonceSize: // 24 bytes
|
||||
return DecryptWithSecretBox(ciphertext, nonce, key)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid nonce size: %d (expected %d for ChaCha20 or %d for XSalsa20)",
|
||||
len(nonce), ChaCha20Poly1305NonceSize, SecretBoxNonceSize)
|
||||
}
|
||||
}
|
||||
|
||||
// EncryptWithBoxSeal encrypts data anonymously using NaCl sealed box.
|
||||
// The result format is: ephemeral_public_key (32) || nonce (24) || ciphertext + auth_tag.
|
||||
func EncryptWithBoxSeal(message []byte, recipientPublicKey []byte) ([]byte, error) {
|
||||
if len(recipientPublicKey) != BoxPublicKeySize {
|
||||
return nil, fmt.Errorf("recipient public key must be %d bytes", BoxPublicKeySize)
|
||||
}
|
||||
|
||||
var recipientPubKey [32]byte
|
||||
copy(recipientPubKey[:], recipientPublicKey)
|
||||
|
||||
// Generate ephemeral keypair
|
||||
ephemeralPubKey, ephemeralPrivKey, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate ephemeral keypair: %w", err)
|
||||
}
|
||||
|
||||
// Generate random nonce
|
||||
nonce, err := GenerateRandomBytes(BoxNonceSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate nonce: %w", err)
|
||||
}
|
||||
var nonceArray [24]byte
|
||||
copy(nonceArray[:], nonce)
|
||||
|
||||
// Encrypt with ephemeral private key
|
||||
ciphertext := box.Seal(nil, message, &nonceArray, &recipientPubKey, ephemeralPrivKey)
|
||||
|
||||
// Result format: ephemeral_public_key || nonce || ciphertext
|
||||
result := make([]byte, BoxPublicKeySize+BoxNonceSize+len(ciphertext))
|
||||
copy(result[:BoxPublicKeySize], ephemeralPubKey[:])
|
||||
copy(result[BoxPublicKeySize:BoxPublicKeySize+BoxNonceSize], nonce)
|
||||
copy(result[BoxPublicKeySize+BoxNonceSize:], ciphertext)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DecryptWithBoxSeal decrypts data that was encrypted with EncryptWithBoxSeal.
|
||||
// SECURITY: Key arrays are wiped from memory after decryption.
|
||||
func DecryptWithBoxSeal(sealedData []byte, recipientPublicKey, recipientPrivateKey []byte) ([]byte, error) {
|
||||
if len(recipientPublicKey) != BoxPublicKeySize {
|
||||
return nil, fmt.Errorf("recipient public key must be %d bytes", BoxPublicKeySize)
|
||||
}
|
||||
if len(recipientPrivateKey) != BoxSecretKeySize {
|
||||
return nil, fmt.Errorf("recipient private key must be %d bytes", BoxSecretKeySize)
|
||||
}
|
||||
if len(sealedData) < BoxPublicKeySize+BoxNonceSize+box.Overhead {
|
||||
return nil, errors.New("sealed data too short")
|
||||
}
|
||||
|
||||
// Extract components
|
||||
ephemeralPublicKey := sealedData[:BoxPublicKeySize]
|
||||
nonce := sealedData[BoxPublicKeySize : BoxPublicKeySize+BoxNonceSize]
|
||||
ciphertext := sealedData[BoxPublicKeySize+BoxNonceSize:]
|
||||
|
||||
// Create fixed-size arrays
|
||||
var ephemeralPubKey [32]byte
|
||||
var recipientPrivKey [32]byte
|
||||
var nonceArray [24]byte
|
||||
copy(ephemeralPubKey[:], ephemeralPublicKey)
|
||||
copy(recipientPrivKey[:], recipientPrivateKey)
|
||||
copy(nonceArray[:], nonce)
|
||||
defer memguard.WipeBytes(recipientPrivKey[:]) // SECURITY: Wipe private key array
|
||||
|
||||
// Decrypt
|
||||
plaintext, ok := box.Open(nil, ciphertext, &nonceArray, &ephemeralPubKey, &recipientPrivKey)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to decrypt sealed box: invalid keys or corrupted ciphertext")
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// DecryptAnonymousBox decrypts sealed box data (used in login challenges).
|
||||
// SECURITY: Key arrays are wiped from memory after decryption.
|
||||
func DecryptAnonymousBox(encryptedData []byte, recipientPublicKey, recipientPrivateKey []byte) ([]byte, error) {
|
||||
if len(recipientPublicKey) != BoxPublicKeySize {
|
||||
return nil, fmt.Errorf("recipient public key must be %d bytes", BoxPublicKeySize)
|
||||
}
|
||||
if len(recipientPrivateKey) != BoxSecretKeySize {
|
||||
return nil, fmt.Errorf("recipient private key must be %d bytes", BoxSecretKeySize)
|
||||
}
|
||||
|
||||
var pubKeyArray, privKeyArray [32]byte
|
||||
copy(pubKeyArray[:], recipientPublicKey)
|
||||
copy(privKeyArray[:], recipientPrivateKey)
|
||||
defer memguard.WipeBytes(privKeyArray[:]) // SECURITY: Wipe private key array
|
||||
|
||||
decryptedData, ok := box.OpenAnonymous(nil, encryptedData, &pubKeyArray, &privKeyArray)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to decrypt anonymous box: invalid keys or corrupted data")
|
||||
}
|
||||
|
||||
return decryptedData, nil
|
||||
}
|
||||
|
||||
// GenerateRandomBytes generates cryptographically secure random bytes.
|
||||
func GenerateRandomBytes(size int) ([]byte, error) {
|
||||
if size <= 0 {
|
||||
return nil, errors.New("size must be positive")
|
||||
}
|
||||
|
||||
buf := make([]byte, size)
|
||||
_, err := io.ReadFull(rand.Reader, buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate random bytes: %w", err)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// GenerateKeyPair generates a NaCl box keypair for asymmetric encryption.
|
||||
func GenerateKeyPair() (publicKey []byte, privateKey []byte, err error) {
|
||||
pubKey, privKey, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate key pair: %w", err)
|
||||
}
|
||||
|
||||
return pubKey[:], privKey[:], nil
|
||||
}
|
||||
|
||||
// ClearBytes overwrites a byte slice with zeros using memguard for secure wiping.
|
||||
// This should be called on sensitive data like keys when they're no longer needed.
|
||||
// SECURITY: Uses memguard.WipeBytes for secure memory wiping that prevents compiler optimizations.
|
||||
func ClearBytes(b []byte) {
|
||||
memguard.WipeBytes(b)
|
||||
}
|
||||
|
||||
// CombineNonceAndCiphertext combines nonce and ciphertext into a single byte slice.
|
||||
func CombineNonceAndCiphertext(nonce, ciphertext []byte) []byte {
|
||||
combined := make([]byte, len(nonce)+len(ciphertext))
|
||||
copy(combined[:len(nonce)], nonce)
|
||||
copy(combined[len(nonce):], ciphertext)
|
||||
return combined
|
||||
}
|
||||
|
||||
// SplitNonceAndCiphertext splits a combined byte slice into nonce and ciphertext.
|
||||
// This function defaults to ChaCha20-Poly1305 nonce size (12 bytes) for backward compatibility.
|
||||
// For XSalsa20-Poly1305 (24-byte nonce), use SplitNonceAndCiphertextSecretBox.
|
||||
func SplitNonceAndCiphertext(combined []byte) (nonce []byte, ciphertext []byte, err error) {
|
||||
if len(combined) < ChaCha20Poly1305NonceSize {
|
||||
return nil, nil, fmt.Errorf("combined data too short: expected at least %d bytes, got %d", ChaCha20Poly1305NonceSize, len(combined))
|
||||
}
|
||||
|
||||
nonce = combined[:ChaCha20Poly1305NonceSize]
|
||||
ciphertext = combined[ChaCha20Poly1305NonceSize:]
|
||||
return nonce, ciphertext, nil
|
||||
}
|
||||
|
||||
// SplitNonceAndCiphertextSecretBox splits a combined byte slice for XSalsa20-Poly1305 (24-byte nonce).
|
||||
// This is compatible with libsodium's secretbox format: nonce (24) || ciphertext || mac (16).
|
||||
func SplitNonceAndCiphertextSecretBox(combined []byte) (nonce []byte, ciphertext []byte, err error) {
|
||||
if len(combined) < SecretBoxNonceSize {
|
||||
return nil, nil, fmt.Errorf("combined data too short: expected at least %d bytes, got %d", SecretBoxNonceSize, len(combined))
|
||||
}
|
||||
|
||||
nonce = combined[:SecretBoxNonceSize]
|
||||
ciphertext = combined[SecretBoxNonceSize:]
|
||||
return nonce, ciphertext, nil
|
||||
}
|
||||
|
||||
// SplitNonceAndCiphertextAuto automatically detects the nonce size based on data length.
|
||||
// It uses heuristics to determine if data is ChaCha20-Poly1305 (12-byte nonce) or XSalsa20 (24-byte nonce).
|
||||
// This function should be used when the cipher type is unknown.
|
||||
func SplitNonceAndCiphertextAuto(combined []byte) (nonce []byte, ciphertext []byte, err error) {
|
||||
// Web frontend uses XSalsa20-Poly1305 with 24-byte nonce
|
||||
// Native app used to use ChaCha20-Poly1305 with 12-byte nonce
|
||||
//
|
||||
// For encrypted master key data:
|
||||
// - Web frontend: nonce (24) + ciphertext (32 + 16 MAC) = 72 bytes
|
||||
// - Native/old: nonce (12) + ciphertext (32 + 16 MAC) = 60 bytes
|
||||
//
|
||||
// We can distinguish by checking if the data length suggests 24-byte nonce
|
||||
// Data encrypted with 24-byte nonce will be 12 bytes longer than 12-byte nonce version
|
||||
|
||||
if len(combined) < ChaCha20Poly1305NonceSize+ChaCha20Poly1305Overhead {
|
||||
return nil, nil, fmt.Errorf("combined data too short: expected at least %d bytes, got %d",
|
||||
ChaCha20Poly1305NonceSize+ChaCha20Poly1305Overhead, len(combined))
|
||||
}
|
||||
|
||||
// If data length is at least 72 bytes (24 nonce + 32 key + 16 MAC for master key),
|
||||
// try XSalsa20 format first. This is the web frontend format.
|
||||
if len(combined) >= SecretBoxNonceSize+SecretBoxOverhead+1 {
|
||||
return SplitNonceAndCiphertextSecretBox(combined)
|
||||
}
|
||||
|
||||
// Default to ChaCha20-Poly1305 (legacy)
|
||||
return SplitNonceAndCiphertext(combined)
|
||||
}
|
||||
|
||||
// EncodeToBase64 encodes bytes to base64 standard encoding.
|
||||
func EncodeToBase64(data []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
// DecodeFromBase64 decodes a base64 standard encoded string to bytes.
|
||||
func DecodeFromBase64(s string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(s)
|
||||
}
|
||||
235
cloud/maplefile-backend/pkg/maplefile/e2ee/file.go
Normal file
235
cloud/maplefile-backend/pkg/maplefile/e2ee/file.go
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
// Package e2ee provides end-to-end encryption operations for the MapleFile SDK.
|
||||
package e2ee
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// FileMetadata represents decrypted file metadata.
|
||||
type FileMetadata struct {
|
||||
Name string `json:"name"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
// EncryptFile encrypts file content using the file key.
|
||||
// Returns the combined nonce + ciphertext.
|
||||
// NOTE: This uses ChaCha20-Poly1305 (12-byte nonce). For web frontend compatibility,
|
||||
// use EncryptFileSecretBox instead.
|
||||
func EncryptFile(plaintext, fileKey []byte) ([]byte, error) {
|
||||
encryptedData, err := Encrypt(plaintext, fileKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt file: %w", err)
|
||||
}
|
||||
|
||||
// Combine nonce and ciphertext for storage
|
||||
combined := CombineNonceAndCiphertext(encryptedData.Nonce, encryptedData.Ciphertext)
|
||||
return combined, nil
|
||||
}
|
||||
|
||||
// EncryptFileSecretBox encrypts file content using XSalsa20-Poly1305 (NaCl secretbox).
|
||||
// Returns the combined nonce (24 bytes) + ciphertext.
|
||||
// This is compatible with the web frontend's libsodium implementation.
|
||||
func EncryptFileSecretBox(plaintext, fileKey []byte) ([]byte, error) {
|
||||
encryptedData, err := EncryptWithSecretBox(plaintext, fileKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt file: %w", err)
|
||||
}
|
||||
|
||||
// Combine nonce and ciphertext for storage (matching web frontend format)
|
||||
combined := CombineNonceAndCiphertext(encryptedData.Nonce, encryptedData.Ciphertext)
|
||||
return combined, nil
|
||||
}
|
||||
|
||||
// DecryptFile decrypts file content using the file key.
|
||||
// The input should be combined nonce + ciphertext.
|
||||
// Auto-detects the cipher based on nonce size:
|
||||
// - 24-byte nonce: XSalsa20-Poly1305 (web frontend / SecretBox)
|
||||
// - 12-byte nonce: ChaCha20-Poly1305 (legacy native app)
|
||||
func DecryptFile(encryptedData, fileKey []byte) ([]byte, error) {
|
||||
// Split nonce and ciphertext (auto-detect nonce size)
|
||||
nonce, ciphertext, err := SplitNonceAndCiphertextAuto(encryptedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to split encrypted data: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt using appropriate algorithm based on nonce size
|
||||
plaintext, err := DecryptWithAlgorithm(ciphertext, nonce, fileKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt file: %w", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// EncryptFileWithNonce encrypts file content and returns the ciphertext and nonce separately.
|
||||
func EncryptFileWithNonce(plaintext, fileKey []byte) (ciphertext []byte, nonce []byte, err error) {
|
||||
encryptedData, err := Encrypt(plaintext, fileKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to encrypt file: %w", err)
|
||||
}
|
||||
|
||||
return encryptedData.Ciphertext, encryptedData.Nonce, nil
|
||||
}
|
||||
|
||||
// DecryptFileWithNonce decrypts file content using separate ciphertext and nonce.
|
||||
func DecryptFileWithNonce(ciphertext, nonce, fileKey []byte) ([]byte, error) {
|
||||
plaintext, err := Decrypt(ciphertext, nonce, fileKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt file: %w", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// EncryptMetadata encrypts file metadata using the file key.
|
||||
// Returns base64-encoded combined nonce + ciphertext.
|
||||
// NOTE: This uses ChaCha20-Poly1305 (12-byte nonce). For web frontend compatibility,
|
||||
// use EncryptMetadataSecretBox instead.
|
||||
func EncryptMetadata(metadata *FileMetadata, fileKey []byte) (string, error) {
|
||||
// Convert metadata to JSON
|
||||
metadataBytes, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt metadata
|
||||
encryptedData, err := Encrypt(metadataBytes, fileKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encrypt metadata: %w", err)
|
||||
}
|
||||
|
||||
// Combine nonce and ciphertext
|
||||
combined := CombineNonceAndCiphertext(encryptedData.Nonce, encryptedData.Ciphertext)
|
||||
|
||||
// Encode to base64
|
||||
return EncodeToBase64(combined), nil
|
||||
}
|
||||
|
||||
// EncryptMetadataSecretBox encrypts file metadata using XSalsa20-Poly1305 (NaCl secretbox).
|
||||
// Returns base64-encoded combined nonce + ciphertext.
|
||||
// This is compatible with the web frontend's libsodium implementation.
|
||||
func EncryptMetadataSecretBox(metadata *FileMetadata, fileKey []byte) (string, error) {
|
||||
// Convert metadata to JSON
|
||||
metadataBytes, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt metadata using SecretBox
|
||||
encryptedData, err := EncryptWithSecretBox(metadataBytes, fileKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encrypt metadata: %w", err)
|
||||
}
|
||||
|
||||
// Combine nonce and ciphertext
|
||||
combined := CombineNonceAndCiphertext(encryptedData.Nonce, encryptedData.Ciphertext)
|
||||
|
||||
// Encode to base64
|
||||
return EncodeToBase64(combined), nil
|
||||
}
|
||||
|
||||
// DecryptMetadata decrypts file metadata using the file key.
|
||||
// The input should be base64-encoded combined nonce + ciphertext.
|
||||
func DecryptMetadata(encryptedMetadata string, fileKey []byte) (*FileMetadata, error) {
|
||||
// Decode from base64
|
||||
combined, err := DecodeFromBase64(encryptedMetadata)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode encrypted metadata: %w", err)
|
||||
}
|
||||
|
||||
// Split nonce and ciphertext
|
||||
nonce, ciphertext, err := SplitNonceAndCiphertext(combined)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to split encrypted metadata: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt
|
||||
decryptedBytes, err := Decrypt(ciphertext, nonce, fileKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt metadata: %w", err)
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
var metadata FileMetadata
|
||||
if err := json.Unmarshal(decryptedBytes, &metadata); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse decrypted metadata: %w", err)
|
||||
}
|
||||
|
||||
return &metadata, nil
|
||||
}
|
||||
|
||||
// EncryptMetadataWithNonce encrypts file metadata and returns nonce separately.
|
||||
func EncryptMetadataWithNonce(metadata *FileMetadata, fileKey []byte) (ciphertext []byte, nonce []byte, err error) {
|
||||
// Convert metadata to JSON
|
||||
metadataBytes, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt metadata
|
||||
encryptedData, err := Encrypt(metadataBytes, fileKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to encrypt metadata: %w", err)
|
||||
}
|
||||
|
||||
return encryptedData.Ciphertext, encryptedData.Nonce, nil
|
||||
}
|
||||
|
||||
// DecryptMetadataWithNonce decrypts file metadata using separate ciphertext and nonce.
|
||||
func DecryptMetadataWithNonce(ciphertext, nonce, fileKey []byte) (*FileMetadata, error) {
|
||||
// Decrypt
|
||||
decryptedBytes, err := Decrypt(ciphertext, nonce, fileKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt metadata: %w", err)
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
var metadata FileMetadata
|
||||
if err := json.Unmarshal(decryptedBytes, &metadata); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse decrypted metadata: %w", err)
|
||||
}
|
||||
|
||||
return &metadata, nil
|
||||
}
|
||||
|
||||
// EncryptData encrypts arbitrary data using the provided key.
|
||||
// Returns base64-encoded combined nonce + ciphertext.
|
||||
func EncryptData(data, key []byte) (string, error) {
|
||||
encryptedData, err := Encrypt(data, key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encrypt data: %w", err)
|
||||
}
|
||||
|
||||
// Combine nonce and ciphertext
|
||||
combined := CombineNonceAndCiphertext(encryptedData.Nonce, encryptedData.Ciphertext)
|
||||
|
||||
// Encode to base64
|
||||
return EncodeToBase64(combined), nil
|
||||
}
|
||||
|
||||
// DecryptData decrypts arbitrary data using the provided key.
|
||||
// The input should be base64-encoded combined nonce + ciphertext.
|
||||
func DecryptData(encryptedData string, key []byte) ([]byte, error) {
|
||||
// Decode from base64
|
||||
combined, err := DecodeFromBase64(encryptedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode encrypted data: %w", err)
|
||||
}
|
||||
|
||||
// Split nonce and ciphertext
|
||||
nonce, ciphertext, err := SplitNonceAndCiphertext(combined)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to split encrypted data: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt
|
||||
plaintext, err := Decrypt(ciphertext, nonce, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt data: %w", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
246
cloud/maplefile-backend/pkg/maplefile/e2ee/secure.go
Normal file
246
cloud/maplefile-backend/pkg/maplefile/e2ee/secure.go
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
// 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue