Feat/Add Crypto Libs #3
@@ -1,219 +0,0 @@
|
||||
// Package enclave provides encrypted database operations with WebAuthn PRF key derivation.
|
||||
package enclave
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnclaveSalt is the salt used for HKDF key derivation
|
||||
EnclaveSalt = "nebula-enclave-v1"
|
||||
|
||||
// KeySize is the size of the derived encryption key (256 bits)
|
||||
KeySize = 32
|
||||
|
||||
// NonceSize is the size of the GCM nonce (96 bits)
|
||||
NonceSize = 12
|
||||
|
||||
// AuthTagSize is the size of the GCM authentication tag (128 bits)
|
||||
AuthTagSize = 16
|
||||
)
|
||||
|
||||
// DeriveEncryptionKey derives a 256-bit encryption key from WebAuthn PRF output using HKDF.
|
||||
//
|
||||
// Parameters:
|
||||
// - prfOutput: The raw PRF output from WebAuthn (typically 32 bytes)
|
||||
//
|
||||
// Returns:
|
||||
// - A 32-byte key suitable for AES-256-GCM encryption
|
||||
// - An error if key derivation fails
|
||||
func DeriveEncryptionKey(prfOutput []byte) ([]byte, error) {
|
||||
if len(prfOutput) == 0 {
|
||||
return nil, fmt.Errorf("enclave: PRF output cannot be empty")
|
||||
}
|
||||
|
||||
salt := []byte(EnclaveSalt)
|
||||
info := []byte("database-encryption")
|
||||
|
||||
hkdfReader := hkdf.New(sha256.New, prfOutput, salt, info)
|
||||
|
||||
key := make([]byte, KeySize)
|
||||
if _, err := io.ReadFull(hkdfReader, key); err != nil {
|
||||
return nil, fmt.Errorf("enclave: failed to derive key: %w", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// DeriveKeyWithContext derives an encryption key with additional context binding.
|
||||
// This allows deriving different keys for different purposes from the same PRF output.
|
||||
//
|
||||
// Parameters:
|
||||
// - prfOutput: The raw PRF output from WebAuthn
|
||||
// - context: Additional context to bind the key to (e.g., "database", "mpc-share")
|
||||
func DeriveKeyWithContext(prfOutput []byte, context string) ([]byte, error) {
|
||||
if len(prfOutput) == 0 {
|
||||
return nil, fmt.Errorf("enclave: PRF output cannot be empty")
|
||||
}
|
||||
if context == "" {
|
||||
return nil, fmt.Errorf("enclave: context cannot be empty")
|
||||
}
|
||||
|
||||
salt := []byte(EnclaveSalt)
|
||||
info := []byte(context)
|
||||
|
||||
hkdfReader := hkdf.New(sha256.New, prfOutput, salt, info)
|
||||
|
||||
key := make([]byte, KeySize)
|
||||
if _, err := io.ReadFull(hkdfReader, key); err != nil {
|
||||
return nil, fmt.Errorf("enclave: failed to derive key: %w", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// EncryptedData represents encrypted data with its metadata.
|
||||
type EncryptedData struct {
|
||||
// Nonce is the unique nonce used for this encryption (12 bytes)
|
||||
Nonce []byte `json:"nonce"`
|
||||
// Ciphertext is the encrypted data including the GCM authentication tag
|
||||
Ciphertext []byte `json:"ciphertext"`
|
||||
// Version indicates the encryption scheme version
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
// Encrypt encrypts plaintext using AES-256-GCM with the provided key.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: 32-byte encryption key (from DeriveEncryptionKey)
|
||||
// - plaintext: The data to encrypt
|
||||
//
|
||||
// Returns:
|
||||
// - EncryptedData containing nonce and ciphertext
|
||||
// - An error if encryption fails
|
||||
func Encrypt(key, plaintext []byte) (*EncryptedData, error) {
|
||||
if len(key) != KeySize {
|
||||
return nil, fmt.Errorf("enclave: invalid key size: got %d, want %d", len(key), KeySize)
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("enclave: failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("enclave: failed to create GCM: %w", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, NonceSize)
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, fmt.Errorf("enclave: failed to generate nonce: %w", err)
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
|
||||
|
||||
return &EncryptedData{
|
||||
Nonce: nonce,
|
||||
Ciphertext: ciphertext,
|
||||
Version: 1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts ciphertext using AES-256-GCM with the provided key.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: 32-byte encryption key (from DeriveEncryptionKey)
|
||||
// - data: The EncryptedData to decrypt
|
||||
//
|
||||
// Returns:
|
||||
// - The decrypted plaintext
|
||||
// - An error if decryption fails (including authentication failure)
|
||||
func Decrypt(key []byte, data *EncryptedData) ([]byte, error) {
|
||||
if len(key) != KeySize {
|
||||
return nil, fmt.Errorf("enclave: invalid key size: got %d, want %d", len(key), KeySize)
|
||||
}
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("enclave: encrypted data cannot be nil")
|
||||
}
|
||||
if len(data.Nonce) != NonceSize {
|
||||
return nil, fmt.Errorf("enclave: invalid nonce size: got %d, want %d", len(data.Nonce), NonceSize)
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("enclave: failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("enclave: failed to create GCM: %w", err)
|
||||
}
|
||||
|
||||
plaintext, err := gcm.Open(nil, data.Nonce, data.Ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("enclave: decryption failed (authentication error): %w", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// EncryptBytes is a convenience function that encrypts and returns serialized bytes.
|
||||
// The format is: version (1 byte) + nonce (12 bytes) + ciphertext (variable)
|
||||
func EncryptBytes(key, plaintext []byte) ([]byte, error) {
|
||||
data, err := Encrypt(key, plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]byte, 1+NonceSize+len(data.Ciphertext))
|
||||
result[0] = byte(data.Version)
|
||||
copy(result[1:1+NonceSize], data.Nonce)
|
||||
copy(result[1+NonceSize:], data.Ciphertext)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DecryptBytes is a convenience function that decrypts serialized encrypted bytes.
|
||||
// Expected format: version (1 byte) + nonce (12 bytes) + ciphertext (variable)
|
||||
func DecryptBytes(key, encryptedBytes []byte) ([]byte, error) {
|
||||
if len(encryptedBytes) < 1+NonceSize+AuthTagSize {
|
||||
return nil, fmt.Errorf("enclave: encrypted data too short")
|
||||
}
|
||||
|
||||
version := int(encryptedBytes[0])
|
||||
if version != 1 {
|
||||
return nil, fmt.Errorf("enclave: unsupported encryption version: %d", version)
|
||||
}
|
||||
|
||||
data := &EncryptedData{
|
||||
Version: version,
|
||||
Nonce: encryptedBytes[1 : 1+NonceSize],
|
||||
Ciphertext: encryptedBytes[1+NonceSize:],
|
||||
}
|
||||
|
||||
return Decrypt(key, data)
|
||||
}
|
||||
|
||||
// GenerateNonce generates a cryptographically secure random nonce.
|
||||
func GenerateNonce() ([]byte, error) {
|
||||
nonce := make([]byte, NonceSize)
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, fmt.Errorf("enclave: failed to generate nonce: %w", err)
|
||||
}
|
||||
return nonce, nil
|
||||
}
|
||||
|
||||
// SecureZero zeros out a byte slice to prevent sensitive data from remaining in memory.
|
||||
func SecureZero(b []byte) {
|
||||
for i := range b {
|
||||
b[i] = 0
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package enclave
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"enclave/internal/keybase"
|
||||
)
|
||||
|
||||
// Enclave wraps a Keybase with encryption capabilities using WebAuthn PRF-derived keys.
|
||||
type Enclave struct {
|
||||
keybase *keybase.Keybase
|
||||
encryptionKey []byte
|
||||
}
|
||||
|
||||
// Config holds enclave configuration options.
|
||||
type Config struct {
|
||||
PRFOutput []byte
|
||||
}
|
||||
|
||||
// New creates a new Enclave with the given PRF output for key derivation.
|
||||
func New(prfOutput []byte) (*Enclave, error) {
|
||||
if len(prfOutput) == 0 {
|
||||
return nil, fmt.Errorf("enclave: PRF output required")
|
||||
}
|
||||
|
||||
key, err := DeriveEncryptionKey(prfOutput)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("enclave: key derivation failed: %w", err)
|
||||
}
|
||||
|
||||
kb, err := keybase.Open()
|
||||
if err != nil {
|
||||
SecureZero(key)
|
||||
return nil, fmt.Errorf("enclave: failed to open keybase: %w", err)
|
||||
}
|
||||
|
||||
return &Enclave{
|
||||
keybase: kb,
|
||||
encryptionKey: key,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Keybase returns the underlying keybase instance.
|
||||
func (e *Enclave) Keybase() *keybase.Keybase {
|
||||
return e.keybase
|
||||
}
|
||||
|
||||
// DID returns the current DID.
|
||||
func (e *Enclave) DID() string {
|
||||
return e.keybase.DID()
|
||||
}
|
||||
|
||||
// IsInitialized returns true if the enclave has been initialized with a DID.
|
||||
func (e *Enclave) IsInitialized() bool {
|
||||
return e.keybase.IsInitialized()
|
||||
}
|
||||
|
||||
// SerializeEncrypted exports the database state as encrypted bytes.
|
||||
func (e *Enclave) SerializeEncrypted() ([]byte, error) {
|
||||
plaintext, err := e.keybase.Serialize()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("enclave: serialization failed: %w", err)
|
||||
}
|
||||
|
||||
encrypted, err := EncryptBytes(e.encryptionKey, plaintext)
|
||||
if err != nil {
|
||||
SecureZero(plaintext)
|
||||
return nil, fmt.Errorf("enclave: encryption failed: %w", err)
|
||||
}
|
||||
|
||||
SecureZero(plaintext)
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
// LoadEncrypted loads the database state from encrypted bytes.
|
||||
func (e *Enclave) LoadEncrypted(encryptedData []byte) error {
|
||||
plaintext, err := DecryptBytes(e.encryptionKey, encryptedData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("enclave: decryption failed: %w", err)
|
||||
}
|
||||
defer SecureZero(plaintext)
|
||||
|
||||
return e.loadFromPlaintext(plaintext)
|
||||
}
|
||||
|
||||
// loadFromPlaintext parses and executes the SQL statements to restore database state.
|
||||
func (e *Enclave) loadFromPlaintext(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return fmt.Errorf("enclave: empty data")
|
||||
}
|
||||
|
||||
return e.keybase.RestoreFromDump(data)
|
||||
}
|
||||
|
||||
// Close securely closes the enclave and zeros out sensitive data.
|
||||
func (e *Enclave) Close() error {
|
||||
SecureZero(e.encryptionKey)
|
||||
return keybase.Close()
|
||||
}
|
||||
|
||||
// EncryptedBundle represents a complete encrypted database export.
|
||||
type EncryptedBundle struct {
|
||||
Version int `json:"version"`
|
||||
DID string `json:"did"`
|
||||
Ciphertext []byte `json:"ciphertext"`
|
||||
Nonce []byte `json:"nonce"`
|
||||
}
|
||||
|
||||
// Export creates a complete encrypted bundle for storage.
|
||||
func (e *Enclave) Export() (*EncryptedBundle, error) {
|
||||
plaintext, err := e.keybase.Serialize()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("enclave: serialization failed: %w", err)
|
||||
}
|
||||
defer SecureZero(plaintext)
|
||||
|
||||
encData, err := Encrypt(e.encryptionKey, plaintext)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("enclave: encryption failed: %w", err)
|
||||
}
|
||||
|
||||
return &EncryptedBundle{
|
||||
Version: encData.Version,
|
||||
DID: e.keybase.DID(),
|
||||
Ciphertext: encData.Ciphertext,
|
||||
Nonce: encData.Nonce,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Import loads an encrypted bundle.
|
||||
func (e *Enclave) Import(bundle *EncryptedBundle) error {
|
||||
if bundle == nil {
|
||||
return fmt.Errorf("enclave: bundle cannot be nil")
|
||||
}
|
||||
|
||||
encData := &EncryptedData{
|
||||
Version: bundle.Version,
|
||||
Ciphertext: bundle.Ciphertext,
|
||||
Nonce: bundle.Nonce,
|
||||
}
|
||||
|
||||
plaintext, err := Decrypt(e.encryptionKey, encData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("enclave: decryption failed: %w", err)
|
||||
}
|
||||
defer SecureZero(plaintext)
|
||||
|
||||
return e.loadFromPlaintext(plaintext)
|
||||
}
|
||||
|
||||
// MarshalBundle serializes an encrypted bundle to JSON.
|
||||
func (b *EncryptedBundle) Marshal() ([]byte, error) {
|
||||
return json.Marshal(b)
|
||||
}
|
||||
|
||||
// UnmarshalBundle deserializes an encrypted bundle from JSON.
|
||||
func UnmarshalBundle(data []byte) (*EncryptedBundle, error) {
|
||||
var bundle EncryptedBundle
|
||||
if err := json.Unmarshal(data, &bundle); err != nil {
|
||||
return nil, fmt.Errorf("enclave: failed to unmarshal bundle: %w", err)
|
||||
}
|
||||
return &bundle, nil
|
||||
}
|
||||
|
||||
// FromExisting wraps an existing keybase with encryption capabilities.
|
||||
func FromExisting(kb *keybase.Keybase, prfOutput []byte) (*Enclave, error) {
|
||||
if kb == nil {
|
||||
return nil, fmt.Errorf("enclave: keybase cannot be nil")
|
||||
}
|
||||
if len(prfOutput) == 0 {
|
||||
return nil, fmt.Errorf("enclave: PRF output required")
|
||||
}
|
||||
|
||||
key, err := DeriveEncryptionKey(prfOutput)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("enclave: key derivation failed: %w", err)
|
||||
}
|
||||
|
||||
return &Enclave{
|
||||
keybase: kb,
|
||||
encryptionKey: key,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user