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
32
cloud/maplefile-backend/pkg/security/crypto/constants.go
Normal file
32
cloud/maplefile-backend/pkg/security/crypto/constants.go
Normal 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)
|
||||
)
|
||||
174
cloud/maplefile-backend/pkg/security/crypto/encrypt.go
Normal file
174
cloud/maplefile-backend/pkg/security/crypto/encrypt.go
Normal 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)
|
||||
}
|
||||
117
cloud/maplefile-backend/pkg/security/crypto/keys.go
Normal file
117
cloud/maplefile-backend/pkg/security/crypto/keys.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue