117 lines
3.5 KiB
Go
117 lines
3.5 KiB
Go
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
|
|
}
|