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
424
native/desktop/maplefile/pkg/crypto/crypto.go
Normal file
424
native/desktop/maplefile/pkg/crypto/crypto.go
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
// 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[:]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue