// monorepo/native/desktop/maplefile-cli/pkg/crypto/crypto.go package crypto import ( "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "fmt" "io" "log" "github.com/tyler-smith/go-bip39" "golang.org/x/crypto/argon2" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/nacl/box" ) const ( // Key sizes MasterKeySize = 32 // 256-bit KeyEncryptionKeySize = 32 RecoveryKeySize = 32 CollectionKeySize = 32 FileKeySize = 32 // ChaCha20-Poly1305 (symmetric encryption) constants ChaCha20Poly1305KeySize = 32 // ChaCha20 key size ChaCha20Poly1305NonceSize = 12 // ChaCha20-Poly1305 nonce size ChaCha20Poly1305Overhead = 16 // Poly1305 authentication tag size // Legacy naming for backward compatibility SecretBoxKeySize = ChaCha20Poly1305KeySize SecretBoxNonceSize = ChaCha20Poly1305NonceSize SecretBoxOverhead = ChaCha20Poly1305Overhead // Box (asymmetric encryption) constants BoxPublicKeySize = 32 BoxSecretKeySize = 32 BoxNonceSize = 24 BoxOverhead = box.Overhead BoxSealOverhead = BoxPublicKeySize + BoxOverhead // Argon2 parameters - must match between platforms Argon2IDAlgorithm = "argon2id" Argon2MemLimit = 4 * 1024 * 1024 // 4 MB (matching your internal/common/crypto settings) Argon2OpsLimit = 1 // 1 iteration (matching your settings) Argon2Parallelism = 1 Argon2KeySize = 32 Argon2SaltSize = 16 // Encryption algorithm identifiers ChaCha20Poly1305Algorithm = "chacha20poly1305" BoxSealAlgorithm = "box_seal" ) // EncryptedData represents encrypted data with its nonce type EncryptedData struct { Ciphertext []byte Nonce []byte } // 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 } // GenerateVerificationID creates a human-readable representation of a public key // JavaScript equivalent: The same BIP39 mnemonic implementation // Generate VerificationID from public key (deterministic) func GenerateVerificationID(publicKey []byte) (string, error) { if publicKey == nil { err := fmt.Errorf("no public key entered") log.Printf("pkg.crypto.VerifyVerificationID - Failed to generate verification ID with error: %v\n", err) return "", fmt.Errorf("failed to generate verification ID: %w", err) } // 1. Hash the public key with SHA256 hash := sha256.Sum256(publicKey[:]) // 2. Use the hash as entropy for BIP39 mnemonic, err := bip39.NewMnemonic(hash[:]) if err != nil { log.Printf("pkg.crypto.VerifyVerificationID - Failed to generate verification ID with error: %v\n", err) return "", fmt.Errorf("failed to generate verification ID: %w", err) } return mnemonic, nil } // Verify VerificationID matches public key func VerifyVerificationID(publicKey []byte, verificationID string) bool { expectedID, err := GenerateVerificationID(publicKey) if err != nil { log.Printf("pkg.crypto.VerifyVerificationID - Failed to generate verification ID with error: %v\n", err) return false } return expectedID == verificationID } // GenerateKeyPair generates a NaCl box keypair for asymmetric encryption func GenerateKeyPair() (publicKey []byte, privateKey []byte, verificationID string, err error) { pubKey, privKey, err := box.GenerateKey(rand.Reader) if err != nil { return nil, nil, "", fmt.Errorf("failed to generate key pair: %w", err) } if pubKey == nil { return nil, nil, "", fmt.Errorf("public key is empty") } // Generate deterministic verification ID verificationID, err = GenerateVerificationID(pubKey[:]) if err != nil { return nil, nil, "", err } return pubKey[:], privKey[:], verificationID, nil } // DeriveKeyFromPassword derives a key from a password using Argon2id // This matches the parameters used in your registration and login flows func DeriveKeyFromPassword(password string, salt []byte) ([]byte, error) { if len(salt) != Argon2SaltSize { return nil, fmt.Errorf("invalid salt size: expected %d, got %d", Argon2SaltSize, len(salt)) } key := argon2.IDKey( []byte(password), salt, Argon2OpsLimit, Argon2MemLimit, Argon2Parallelism, Argon2KeySize, ) return key, nil } // EncryptWithSecretBox encrypts data with a symmetric key using ChaCha20-Poly1305 func EncryptWithSecretBox(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 nonce 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 } // EncryptDataWithKey is a helper that encrypts data and returns ciphertext and nonce separately // This is for backward compatibility with existing code func EncryptDataWithKey(data, key []byte) (ciphertext []byte, nonce []byte, err error) { encData, err := EncryptWithSecretBox(data, key) if err != nil { return nil, nil, err } return encData.Ciphertext, encData.Nonce, nil } // DecryptWithSecretBox decrypts data with a symmetric key using ChaCha20-Poly1305 func DecryptWithSecretBox(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 } // EncryptWithBox encrypts data using NaCl box with the recipient's public key and sender's private key func EncryptWithBox(message []byte, recipientPublicKey, senderPrivateKey []byte) (*EncryptedData, error) { if len(recipientPublicKey) != BoxPublicKeySize { return nil, fmt.Errorf("recipient public key must be %d bytes", BoxPublicKeySize) } if len(senderPrivateKey) != BoxSecretKeySize { return nil, fmt.Errorf("sender private key must be %d bytes", BoxSecretKeySize) } // Generate nonce nonce, err := GenerateRandomBytes(BoxNonceSize) if err != nil { return nil, fmt.Errorf("failed to generate nonce: %w", err) } // Create fixed-size arrays var recipientPubKey [32]byte var senderPrivKey [32]byte var nonceArray [24]byte copy(recipientPubKey[:], recipientPublicKey) copy(senderPrivKey[:], senderPrivateKey) copy(nonceArray[:], nonce) // Encrypt ciphertext := box.Seal(nil, message, &nonceArray, &recipientPubKey, &senderPrivKey) return &EncryptedData{ Ciphertext: ciphertext, Nonce: nonce, }, nil } // DecryptWithBox decrypts data using NaCl box with the sender's public key and recipient's private key func DecryptWithBox(ciphertext, nonce []byte, senderPublicKey, recipientPrivateKey []byte) ([]byte, error) { if len(senderPublicKey) != BoxPublicKeySize { return nil, fmt.Errorf("sender 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(nonce) != BoxNonceSize { return nil, fmt.Errorf("nonce must be %d bytes", BoxNonceSize) } // Create fixed-size arrays var senderPubKey [32]byte var recipientPrivKey [32]byte var nonceArray [24]byte copy(senderPubKey[:], senderPublicKey) copy(recipientPrivKey[:], recipientPrivateKey) copy(nonceArray[:], nonce) // Decrypt plaintext, ok := box.Open(nil, ciphertext, &nonceArray, &senderPubKey, &recipientPrivKey) if !ok { return nil, errors.New("failed to decrypt: invalid keys, nonce, or corrupted ciphertext") } return plaintext, nil } // EncryptWithBoxSeal encrypts data with a recipient's public key using anonymous sender (sealed box) // This is used for encrypting data where the sender doesn't need to be authenticated func EncryptWithBoxSeal(message []byte, recipientPublicKey []byte) ([]byte, error) { if len(recipientPublicKey) != BoxPublicKeySize { return nil, fmt.Errorf("recipient public key must be %d bytes", BoxPublicKeySize) } // Create a fixed-size array for the recipient's public key var recipientPubKey [32]byte copy(recipientPubKey[:], recipientPublicKey) // Generate an ephemeral keypair ephemeralPubKey, ephemeralPrivKey, err := box.GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf("failed to generate ephemeral keypair: %w", err) } // Generate a 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 the message 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 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) // 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 } // DecryptWithBoxAnonymous decrypts data that was encrypted anonymously (without nonce in the data) // This is used in the login flow for decrypting challenges func DecryptWithBoxAnonymous(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) } // Create fixed-size arrays var pubKeyArray, privKeyArray [32]byte copy(pubKeyArray[:], recipientPublicKey) copy(privKeyArray[:], recipientPrivateKey) // Decrypt the sealed box challenge 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 } // EncodeToBase64 encodes bytes to base64 standard encoding func EncodeToBase64(data []byte) string { return base64.StdEncoding.EncodeToString(data) } // EncodeToBase64URL encodes bytes to base64 URL-safe encoding without padding func EncodeToBase64URL(data []byte) string { return base64.RawURLEncoding.EncodeToString(data) } // DecodeFromBase64 decodes a base64 standard encoded string to bytes func DecodeFromBase64(s string) ([]byte, error) { return base64.StdEncoding.DecodeString(s) } // DecodeFromBase64URL decodes a base64 URL-safe encoded string without padding to bytes func DecodeFromBase64URL(s string) ([]byte, error) { return base64.RawURLEncoding.DecodeString(s) } // CombineNonceAndCiphertext combines nonce and ciphertext into a single byte slice // This is useful for storing encrypted data as a single blob 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 // For ChaCha20-Poly1305, the nonce size is 12 bytes func SplitNonceAndCiphertext(combined []byte, nonceSize int) (nonce []byte, ciphertext []byte, err error) { if len(combined) < nonceSize { return nil, nil, fmt.Errorf("combined data too short: expected at least %d bytes, got %d", nonceSize, len(combined)) } nonce = combined[:nonceSize] ciphertext = combined[nonceSize:] return nonce, ciphertext, nil } // Helper function to convert EncryptedData to separate slices (for backward compatibility) func (ed *EncryptedData) Separate() (ciphertext []byte, nonce []byte) { return ed.Ciphertext, ed.Nonce } // ClearBytes overwrites a byte slice with zeros // This should be called on sensitive data like keys when they're no longer needed func ClearBytes(b []byte) { for i := range b { b[i] = 0 } } func HashSHA256(proofData []byte) []byte { hash := sha256.Sum256(proofData) return hash[:] }