Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,32 @@
package crypto
// Constants to ensure compatibility between Go and JavaScript
const (
// Key sizes
MasterKeySize = 32 // 256-bit
KeyEncryptionKeySize = 32
CollectionKeySize = 32
FileKeySize = 32
RecoveryKeySize = 32
// ChaCha20-Poly1305 constants (updated from XSalsa20-Poly1305)
NonceSize = 12 // ChaCha20-Poly1305 nonce size (changed from 24)
PublicKeySize = 32
PrivateKeySize = 32
SealedBoxOverhead = 16
// Legacy naming for backward compatibility
SecretBoxNonceSize = NonceSize
// Argon2 parameters - must match between platforms
Argon2IDAlgorithm = "argon2id"
Argon2MemLimit = 67108864 // 64 MB
Argon2OpsLimit = 4
Argon2Parallelism = 1
Argon2KeySize = 32
Argon2SaltSize = 16
// Encryption algorithm identifiers
ChaCha20Poly1305Algorithm = "chacha20poly1305" // Primary algorithm
XSalsa20Poly1305Algorithm = "xsalsa20poly1305" // Legacy algorithm (deprecated)
)

View file

@ -0,0 +1,174 @@
package crypto
import (
"crypto/rand"
"errors"
"fmt"
"io"
"github.com/awnumar/memguard"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/nacl/box"
)
// EncryptData represents encrypted data with its nonce
type EncryptData struct {
Ciphertext []byte
Nonce []byte
}
// EncryptWithSecretKey encrypts data with a symmetric key using ChaCha20-Poly1305
// JavaScript equivalent: sodium.crypto_secretbox_easy() but using ChaCha20-Poly1305
func EncryptWithSecretKey(data, key []byte) (*EncryptData, error) {
if len(key) != MasterKeySize {
return nil, fmt.Errorf("invalid key size: expected %d, got %d", MasterKeySize, 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 := GenerateRandomNonce()
if err != nil {
return nil, fmt.Errorf("failed to generate nonce: %w", err)
}
// Encrypt
ciphertext := cipher.Seal(nil, nonce, data, nil)
return &EncryptData{
Ciphertext: ciphertext,
Nonce: nonce,
}, nil
}
// DecryptWithSecretKey decrypts data with a symmetric key using ChaCha20-Poly1305
// JavaScript equivalent: sodium.crypto_secretbox_open_easy() but using ChaCha20-Poly1305
func DecryptWithSecretKey(encryptedData *EncryptData, key []byte) ([]byte, error) {
if len(key) != MasterKeySize {
return nil, fmt.Errorf("invalid key size: expected %d, got %d", MasterKeySize, len(key))
}
if len(encryptedData.Nonce) != NonceSize {
return nil, fmt.Errorf("invalid nonce size: expected %d, got %d", NonceSize, len(encryptedData.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, encryptedData.Nonce, encryptedData.Ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("decryption failed: %w", err)
}
return plaintext, nil
}
// EncryptWithPublicKey encrypts data with a public key using NaCl box (XSalsa20-Poly1305)
// Note: Asymmetric encryption still uses NaCl box for compatibility
// JavaScript equivalent: sodium.crypto_box_seal()
func EncryptWithPublicKey(data, recipientPublicKey []byte) ([]byte, error) {
if len(recipientPublicKey) != PublicKeySize {
return nil, fmt.Errorf("invalid public key size: expected %d, got %d", PublicKeySize, len(recipientPublicKey))
}
// Convert to fixed-size array
var pubKeyArray [32]byte
copy(pubKeyArray[:], recipientPublicKey)
// Generate nonce for box encryption (24 bytes for NaCl box)
var nonce [24]byte
if _, err := rand.Read(nonce[:]); err != nil {
return nil, fmt.Errorf("failed to generate nonce: %w", err)
}
// For sealed box, we need to use SealAnonymous
sealed, err := box.SealAnonymous(nil, data, &pubKeyArray, rand.Reader)
if err != nil {
return nil, fmt.Errorf("failed to seal data: %w", err)
}
return sealed, nil
}
// DecryptWithPrivateKey decrypts data with a private key using NaCl box
// Note: Asymmetric encryption still uses NaCl box for compatibility
// JavaScript equivalent: sodium.crypto_box_seal_open()
// SECURITY: Key arrays are wiped from memory after use to prevent key extraction via memory dumps.
func DecryptWithPrivateKey(encryptedData, publicKey, privateKey []byte) ([]byte, error) {
if len(privateKey) != PrivateKeySize {
return nil, fmt.Errorf("invalid private key size: expected %d, got %d", PrivateKeySize, len(privateKey))
}
if len(publicKey) != PublicKeySize {
return nil, fmt.Errorf("invalid public key size: expected %d, got %d", PublicKeySize, len(publicKey))
}
// Convert to fixed-size arrays
var pubKeyArray [32]byte
copy(pubKeyArray[:], publicKey)
defer memguard.WipeBytes(pubKeyArray[:]) // SECURITY: Wipe public key array
var privKeyArray [32]byte
copy(privKeyArray[:], privateKey)
defer memguard.WipeBytes(privKeyArray[:]) // SECURITY: Wipe private key array
// Decrypt using OpenAnonymous for sealed box
plaintext, ok := box.OpenAnonymous(nil, encryptedData, &pubKeyArray, &privKeyArray)
if !ok {
return nil, errors.New("decryption failed: invalid keys or corrupted data")
}
return plaintext, nil
}
// EncryptFileChunked encrypts a file in chunks using ChaCha20-Poly1305
// JavaScript equivalent: sodium.crypto_secretstream_* but using ChaCha20-Poly1305
// SECURITY: Plaintext data is wiped from memory after encryption.
func EncryptFileChunked(reader io.Reader, key []byte) ([]byte, error) {
// This would be a more complex implementation using
// chunked encryption. For brevity, we'll use a simpler approach
// that reads the entire file into memory first.
data, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("failed to read data: %w", err)
}
defer memguard.WipeBytes(data) // SECURITY: Wipe plaintext after encryption
encData, err := EncryptWithSecretKey(data, key)
if err != nil {
return nil, fmt.Errorf("failed to encrypt data: %w", err)
}
// Combine nonce and ciphertext
result := make([]byte, len(encData.Nonce)+len(encData.Ciphertext))
copy(result, encData.Nonce)
copy(result[len(encData.Nonce):], encData.Ciphertext)
return result, nil
}
// DecryptFileChunked decrypts a chunked encrypted file using ChaCha20-Poly1305
// JavaScript equivalent: sodium.crypto_secretstream_* but using ChaCha20-Poly1305
func DecryptFileChunked(encryptedData, key []byte) ([]byte, error) {
// Split nonce and ciphertext
if len(encryptedData) < NonceSize {
return nil, fmt.Errorf("encrypted data too short: expected at least %d bytes, got %d", NonceSize, len(encryptedData))
}
nonce := encryptedData[:NonceSize]
ciphertext := encryptedData[NonceSize:]
// Decrypt
return DecryptWithSecretKey(&EncryptData{
Ciphertext: ciphertext,
Nonce: nonce,
}, key)
}

View file

@ -0,0 +1,117 @@
package crypto
import (
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"io"
"log"
"github.com/awnumar/memguard"
"github.com/tyler-smith/go-bip39"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/nacl/box"
)
// GenerateRandomKey generates a new random key using crypto_secretbox_keygen
// JavaScript equivalent: sodium.randombytes_buf(crypto.MasterKeySize)
func GenerateRandomKey(size int) ([]byte, error) {
if size <= 0 {
return nil, errors.New("key size must be positive")
}
key := make([]byte, size)
_, err := io.ReadFull(rand.Reader, key)
if err != nil {
return nil, fmt.Errorf("failed to generate random key: %w", err)
}
return key, nil
}
// GenerateKeyPair generates a public/private key pair using NaCl box
// JavaScript equivalent: sodium.crypto_box_keypair()
func GenerateKeyPair() (publicKey, 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)
}
// Convert from fixed-size arrays to slices
publicKey = pubKey[:]
privateKey = privKey[:]
// Generate deterministic verification ID
verificationID, err = GenerateVerificationID(publicKey[:])
if err != nil {
return nil, nil, "", fmt.Errorf("failed to generate verification ID: %w", err)
}
return publicKey, privateKey, verificationID, nil
}
// DeriveKeyFromPassword derives a key encryption key from a password using Argon2id
// JavaScript equivalent: sodium.crypto_pwhash()
// SECURITY: Password bytes are wiped from memory after key derivation.
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))
}
// Convert password to bytes for wiping
passwordBytes := []byte(password)
defer memguard.WipeBytes(passwordBytes) // SECURITY: Wipe password bytes after use
// These parameters must match between Go and JavaScript
key := argon2.IDKey(
passwordBytes,
salt,
Argon2OpsLimit,
Argon2MemLimit,
Argon2Parallelism,
Argon2KeySize,
)
return key, nil
}
// GenerateRandomNonce generates a random nonce for ChaCha20-Poly1305 encryption operations
// JavaScript equivalent: sodium.randombytes_buf(crypto.NonceSize)
func GenerateRandomNonce() ([]byte, error) {
nonce := make([]byte, NonceSize) // NonceSize is now 12 for ChaCha20-Poly1305
_, err := io.ReadFull(rand.Reader, nonce)
if err != nil {
return nil, fmt.Errorf("failed to generate random nonce: %w", err)
}
return nonce, 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 len(publicKey) == 0 {
return "", errors.New("public key cannot be empty")
}
// 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 {
return "", fmt.Errorf("failed to generate verification ID: %w", err)
}
return mnemonic, nil
}
// VerifyVerificationID checks if a verification ID matches a 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
}