mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 12:26:58 +00:00
- Invalidate plaintext refresh tokens (migration 019) - Add `--sign` flag to `orama build` for rootwallet manifest signing - Add `--ca-fingerprint` TOFU verification for production joins/invites - Save cluster secrets from join (RQLite auth, Olric key, IPFS peers) - Add RQLite auth config fields
99 lines
3.0 KiB
Go
99 lines
3.0 KiB
Go
// Package secrets provides application-level encryption for sensitive data stored in RQLite.
|
|
// Uses AES-256-GCM with HKDF key derivation from the cluster secret.
|
|
package secrets
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/hkdf"
|
|
)
|
|
|
|
// Prefix for encrypted values to distinguish from plaintext during migration.
|
|
const encryptedPrefix = "enc:"
|
|
|
|
// DeriveKey derives a 32-byte AES-256 key from the cluster secret using HKDF-SHA256.
|
|
// The purpose string provides domain separation (e.g., "turn-encryption").
|
|
func DeriveKey(clusterSecret, purpose string) ([]byte, error) {
|
|
if clusterSecret == "" {
|
|
return nil, fmt.Errorf("cluster secret is empty")
|
|
}
|
|
reader := hkdf.New(sha256.New, []byte(clusterSecret), nil, []byte(purpose))
|
|
key := make([]byte, 32)
|
|
if _, err := io.ReadFull(reader, key); err != nil {
|
|
return nil, fmt.Errorf("HKDF key derivation failed: %w", err)
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
// Encrypt encrypts plaintext with AES-256-GCM using the given key.
|
|
// Returns a base64-encoded string prefixed with "enc:" for identification.
|
|
func Encrypt(plaintext string, key []byte) (string, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create cipher: %w", err)
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create GCM: %w", err)
|
|
}
|
|
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
return "", fmt.Errorf("failed to generate nonce: %w", err)
|
|
}
|
|
|
|
// nonce is prepended to ciphertext
|
|
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
|
return encryptedPrefix + base64.StdEncoding.EncodeToString(ciphertext), nil
|
|
}
|
|
|
|
// Decrypt decrypts an "enc:"-prefixed ciphertext string with AES-256-GCM.
|
|
// If the input is not prefixed with "enc:", it is returned as-is (plaintext passthrough
|
|
// for backward compatibility during migration).
|
|
func Decrypt(ciphertext string, key []byte) (string, error) {
|
|
if !strings.HasPrefix(ciphertext, encryptedPrefix) {
|
|
return ciphertext, nil // plaintext passthrough
|
|
}
|
|
|
|
data, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(ciphertext, encryptedPrefix))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to decode ciphertext: %w", err)
|
|
}
|
|
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create cipher: %w", err)
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create GCM: %w", err)
|
|
}
|
|
|
|
nonceSize := gcm.NonceSize()
|
|
if len(data) < nonceSize {
|
|
return "", fmt.Errorf("ciphertext too short")
|
|
}
|
|
|
|
nonce, sealed := data[:nonceSize], data[nonceSize:]
|
|
plaintext, err := gcm.Open(nil, nonce, sealed, nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("decryption failed (wrong key or corrupted data): %w", err)
|
|
}
|
|
|
|
return string(plaintext), nil
|
|
}
|
|
|
|
// IsEncrypted returns true if the value has the "enc:" prefix.
|
|
func IsEncrypted(value string) bool {
|
|
return strings.HasPrefix(value, encryptedPrefix)
|
|
}
|