Merge pull request #59 from lordmathis/fix/signing-key

Generate signing and encryption key to a file
This commit is contained in:
2025-10-11 21:31:56 +02:00
committed by GitHub
16 changed files with 397 additions and 263 deletions

26
.vscode/launch.json vendored
View File

@@ -2,13 +2,35 @@
"version": "0.2.0",
"configurations": [
{
"name": "Launch Lemma Server",
"name": "Launch Backend",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/server/cmd/server/main.go",
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/server/.env"
"envFile": "${workspaceFolder}/.env.local"
},
{
"name": "Launch Frontend",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["start"],
"cwd": "${workspaceFolder}/app",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env.local"
}
],
"compounds": [
{
"name": "Launch Backend + Frontend",
"configurations": ["Launch Backend", "Launch Frontend"],
"presentation": {
"hidden": false,
"group": "",
"order": 1
},
"stopAll": true
}
]
}

View File

@@ -28,7 +28,6 @@ Lemma can be configured using environment variables. Here are the available conf
- `LEMMA_ADMIN_EMAIL`: Email address for the admin account
- `LEMMA_ADMIN_PASSWORD`: Password for the admin account
- `LEMMA_ENCRYPTION_KEY`: Base64-encoded 32-byte key used for encrypting sensitive data
### Optional Environment Variables
@@ -39,21 +38,17 @@ Lemma can be configured using environment variables. Here are the available conf
- `LEMMA_PORT`: Port to run the server on (default: "8080")
- `LEMMA_DOMAIN`: Domain name where the application is hosted for cookie authentication
- `LEMMA_CORS_ORIGINS`: Comma-separated list of allowed CORS origins
- `LEMMA_JWT_SIGNING_KEY`: Key used for signing JWT tokens
- `LEMMA_ENCRYPTION_KEY`: Base64-encoded 32-byte key used for encrypting sensitive data. If not provided, a key will be automatically generated and stored in `{LEMMA_WORKDIR}/secrets/encryption_key`
- `LEMMA_JWT_SIGNING_KEY`: Key used for signing JWT tokens. If not provided, a key will be automatically generated and stored in `{LEMMA_WORKDIR}/secrets/jwt_signing_key`
- `LEMMA_LOG_LEVEL`: Logging level (defaults to DEBUG in development mode, INFO in production)
- `LEMMA_RATE_LIMIT_REQUESTS`: Number of allowed requests per window (default: 100)
- `LEMMA_RATE_LIMIT_WINDOW`: Duration of the rate limit window (default: 15m)
### Generating Encryption Keys
### Security Keys
The encryption key must be a base64-encoded 32-byte value. You can generate a secure encryption key using OpenSSL:
Both the encryption key and JWT signing key are automatically generated on first startup if not provided via environment variables. The keys are stored in `{LEMMA_WORKDIR}/secrets/` with restrictive file permissions (0600).
```bash
# Generate a random 32-byte key and encode it as base64
openssl rand -base64 32
```
Store the generated key securely - it will be needed to decrypt any data encrypted by the application. If the key is lost or changed, previously encrypted data will become inaccessible.
**Important**: Back up the `secrets` directory! If these keys are lost, encrypted data will become inaccessible and all users will need to re-authenticate.
## Running the backend server

6
app/package-lock.json generated
View File

@@ -3514,9 +3514,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001702",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz",
"integrity": "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==",
"version": "1.0.30001749",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz",
"integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==",
"dev": true,
"funding": [
{

View File

@@ -48,13 +48,21 @@ func DefaultConfig() *Config {
// validate checks if the configuration is valid
func (c *Config) validate() error {
if c.AdminEmail == "" || c.AdminPassword == "" {
return fmt.Errorf("LEMMA_ADMIN_EMAIL and LEMMA_ADMIN_PASSWORD must be set")
return fmt.Errorf(`admin credentials not configured
To get started, set these environment variables:
export LEMMA_ADMIN_EMAIL="admin@example.com"
export LEMMA_ADMIN_PASSWORD="your-secure-password"
Then start the server again.`)
}
// Validate encryption key
// Validate encryption key if provided (if not provided, it will be auto-generated)
if c.EncryptionKey != "" {
if err := secrets.ValidateKey(c.EncryptionKey); err != nil {
return fmt.Errorf("invalid LEMMA_ENCRYPTION_KEY: %w", err)
}
}
return nil
}

View File

@@ -168,7 +168,13 @@ func TestLoad(t *testing.T) {
setEnv(t, "LEMMA_ADMIN_PASSWORD", "password123")
setEnv(t, "LEMMA_ENCRYPTION_KEY", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=")
},
expectedError: "LEMMA_ADMIN_EMAIL and LEMMA_ADMIN_PASSWORD must be set",
expectedError: `admin credentials not configured
To get started, set these environment variables:
export LEMMA_ADMIN_EMAIL="admin@example.com"
export LEMMA_ADMIN_PASSWORD="your-secure-password"
Then start the server again.`,
},
{
name: "missing admin password",
@@ -177,16 +183,13 @@ func TestLoad(t *testing.T) {
setEnv(t, "LEMMA_ADMIN_EMAIL", "admin@example.com")
setEnv(t, "LEMMA_ENCRYPTION_KEY", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=")
},
expectedError: "LEMMA_ADMIN_EMAIL and LEMMA_ADMIN_PASSWORD must be set",
},
{
name: "missing encryption key",
setupEnv: func(t *testing.T) {
cleanup()
setEnv(t, "LEMMA_ADMIN_EMAIL", "admin@example.com")
setEnv(t, "LEMMA_ADMIN_PASSWORD", "password123")
},
expectedError: "invalid LEMMA_ENCRYPTION_KEY: encryption key is required",
expectedError: `admin credentials not configured
To get started, set these environment variables:
export LEMMA_ADMIN_EMAIL="admin@example.com"
export LEMMA_ADMIN_PASSWORD="your-secure-password"
Then start the server again.`,
},
{
name: "invalid encryption key",

View File

@@ -19,7 +19,22 @@ import (
// initSecretsService initializes the secrets service
func initSecretsService(cfg *Config) (secrets.Service, error) {
logging.Debug("initializing secrets service")
secretsService, err := secrets.NewService(cfg.EncryptionKey)
// Get or generate encryption key
encryptionKey := cfg.EncryptionKey
if encryptionKey == "" {
logging.Debug("no encryption key provided, loading/generating from file")
// Load or generate key from file
secretsDir := cfg.WorkDir + "/secrets"
var err error
encryptionKey, err = secrets.EnsureEncryptionKey(secretsDir)
if err != nil {
return nil, fmt.Errorf("failed to ensure encryption key: %w", err)
}
}
secretsService, err := secrets.NewService(encryptionKey)
if err != nil {
return nil, fmt.Errorf("failed to initialize secrets service: %w", err)
}
@@ -52,11 +67,14 @@ func initAuth(cfg *Config, database db.Database) (auth.JWTManager, auth.SessionM
// Get or generate JWT signing key
signingKey := cfg.JWTSigningKey
if signingKey == "" {
logging.Debug("no JWT signing key provided, generating new key")
logging.Debug("no JWT signing key provided, loading/generating from file")
// Load or generate key from file
secretsDir := cfg.WorkDir + "/secrets"
var err error
signingKey, err = database.EnsureJWTSecret()
signingKey, err = secrets.EnsureJWTSigningKey(secretsDir)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to ensure JWT secret: %w", err)
return nil, nil, nil, fmt.Errorf("failed to ensure JWT signing key: %w", err)
}
}

View File

@@ -68,12 +68,9 @@ type SessionStore interface {
CleanExpiredSessions() error
}
// SystemStore defines the methods for interacting with system settings and stats in the database
// SystemStore defines the methods for interacting with system stats in the database
type SystemStore interface {
GetSystemStats() (*UserStats, error)
EnsureJWTSecret() (string, error)
GetSystemSetting(key string) (string, error)
SetSystemSetting(key, value string) error
}
type StructScanner interface {

View File

@@ -5,5 +5,4 @@ DROP INDEX IF EXISTS idx_sessions_user_id;
DROP INDEX IF EXISTS idx_workspaces_user_id;
DROP TABLE IF EXISTS sessions;
DROP TABLE IF EXISTS workspaces;
DROP TABLE IF EXISTS system_settings;
DROP TABLE IF EXISTS users;

View File

@@ -46,14 +46,6 @@ CREATE TABLE IF NOT EXISTS sessions (
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
-- Create system_settings table for application settings
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);

View File

@@ -5,5 +5,4 @@ DROP INDEX IF EXISTS idx_sessions_user_id;
DROP INDEX IF EXISTS idx_workspaces_user_id;
DROP TABLE IF EXISTS sessions;
DROP TABLE IF EXISTS workspaces;
DROP TABLE IF EXISTS system_settings;
DROP TABLE IF EXISTS users;

View File

@@ -45,14 +45,6 @@ CREATE TABLE IF NOT EXISTS sessions (
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
-- Create system_settings table for application settings
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);

View File

@@ -26,7 +26,6 @@ func TestMigrate(t *testing.T) {
"users",
"workspaces",
"sessions",
"system_settings",
"schema_migrations",
}

View File

@@ -1,16 +1,9 @@
package db
import (
"crypto/rand"
"encoding/base64"
"fmt"
)
const (
// JWTSecretKey is the key for the JWT secret in the system settings
JWTSecretKey = "jwt_secret"
)
// UserStats represents system-wide statistics
type UserStats struct {
TotalUsers int `json:"totalUsers"`
@@ -18,83 +11,6 @@ type UserStats struct {
ActiveUsers int `json:"activeUsers"` // Users with activity in last 30 days
}
// EnsureJWTSecret makes sure a JWT signing secret exists in the database
// If no secret exists, it generates and stores a new one
func (db *database) EnsureJWTSecret() (string, error) {
log := getLogger().WithGroup("system")
// First, try to get existing secret
secret, err := db.GetSystemSetting(JWTSecretKey)
if err == nil {
return secret, nil
}
// Generate new secret if none exists
newSecret, err := generateRandomSecret(32) // 256 bits
if err != nil {
return "", fmt.Errorf("failed to generate JWT secret: %w", err)
}
// Store the new secret
err = db.SetSystemSetting(JWTSecretKey, newSecret)
if err != nil {
return "", fmt.Errorf("failed to store JWT secret: %w", err)
}
log.Info("new JWT secret generated and stored")
return newSecret, nil
}
// GetSystemSetting retrieves a system setting by key
func (db *database) GetSystemSetting(key string) (string, error) {
var value string
query := db.NewQuery().
Select("value").
From("system_settings").
Where("key = ").
Placeholder(key)
err := db.QueryRow(query.String(), query.args...).Scan(&value)
if err != nil {
return "", err
}
return value, nil
}
// SetSystemSetting stores or updates a system setting
func (db *database) SetSystemSetting(key, value string) error {
query := db.NewQuery().
Insert("system_settings", "key", "value").
Values(2).
AddArgs(key, value).
Write("ON CONFLICT(key) DO UPDATE SET value = ").
Placeholder(value)
_, err := db.Exec(query.String(), query.args...)
if err != nil {
return fmt.Errorf("failed to store system setting: %w", err)
}
return nil
}
// generateRandomSecret generates a cryptographically secure random string
func generateRandomSecret(bytes int) (string, error) {
log := getLogger().WithGroup("system")
log.Debug("generating random secret", "bytes", bytes)
b := make([]byte, bytes)
_, err := rand.Read(b)
if err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
secret := base64.StdEncoding.EncodeToString(b)
return secret, nil
}
// GetSystemStats returns system-wide statistics
func (db *database) GetSystemStats() (*UserStats, error) {
stats := &UserStats{}

View File

@@ -1,9 +1,7 @@
package db_test
import (
"encoding/base64"
"fmt"
"strings"
"testing"
"time"
@@ -25,126 +23,6 @@ func TestSystemOperations(t *testing.T) {
t.Fatalf("failed to run migrations: %v", err)
}
t.Run("GetSystemSettings", func(t *testing.T) {
t.Run("non-existent setting", func(t *testing.T) {
_, err := database.GetSystemSetting("nonexistent-key")
if err == nil {
t.Error("expected error for non-existent key, got nil")
}
})
t.Run("existing setting", func(t *testing.T) {
// First set a value
err := database.SetSystemSetting("test-key", "test-value")
if err != nil {
t.Fatalf("failed to set system setting: %v", err)
}
// Then get it back
value, err := database.GetSystemSetting("test-key")
if err != nil {
t.Fatalf("failed to get system setting: %v", err)
}
if value != "test-value" {
t.Errorf("got value %q, want %q", value, "test-value")
}
})
})
t.Run("SetSystemSettings", func(t *testing.T) {
testCases := []struct {
name string
key string
value string
wantErr bool
errContains string
}{
{
name: "new setting",
key: "new-key",
value: "new-value",
wantErr: false,
},
{
name: "update existing setting",
key: "update-key",
value: "original-value",
wantErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := database.SetSystemSetting(tc.key, tc.value)
if tc.wantErr {
if err == nil {
t.Error("expected error, got nil")
} else if !strings.Contains(err.Error(), tc.errContains) {
t.Errorf("error = %v, want error containing %v", err, tc.errContains)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify the setting was stored
stored, err := database.GetSystemSetting(tc.key)
if err != nil {
t.Fatalf("failed to retrieve stored setting: %v", err)
}
if stored != tc.value {
t.Errorf("got value %q, want %q", stored, tc.value)
}
// For the update case, test updating the value
if tc.name == "update existing setting" {
newValue := "updated-value"
err := database.SetSystemSetting(tc.key, newValue)
if err != nil {
t.Fatalf("failed to update setting: %v", err)
}
stored, err := database.GetSystemSetting(tc.key)
if err != nil {
t.Fatalf("failed to retrieve updated setting: %v", err)
}
if stored != newValue {
t.Errorf("got updated value %q, want %q", stored, newValue)
}
}
})
}
})
t.Run("EnsureJWTSecret", func(t *testing.T) {
// First call should generate a new secret
secret1, err := database.EnsureJWTSecret()
if err != nil {
t.Fatalf("failed to ensure JWT secret: %v", err)
}
// Verify the secret is a valid base64-encoded string of sufficient length
decoded, err := base64.StdEncoding.DecodeString(secret1)
if err != nil {
t.Errorf("secret is not valid base64: %v", err)
}
if len(decoded) < 32 {
t.Errorf("secret length = %d, want >= 32", len(decoded))
}
// Second call should return the same secret
secret2, err := database.EnsureJWTSecret()
if err != nil {
t.Fatalf("failed to get existing JWT secret: %v", err)
}
if secret2 != secret1 {
t.Errorf("got different secret on second call")
}
})
t.Run("GetSystemStats", func(t *testing.T) {
// Create some test users and sessions
users := []*models.User{

View File

@@ -0,0 +1,147 @@
package secrets
import (
"crypto/rand"
"encoding/base64"
"fmt"
"os"
"path/filepath"
)
const (
// KeyBytes is the size of keys in bytes (256 bits)
KeyBytes = 32
// JWTKeyFile is the filename for the JWT signing key
JWTKeyFile = "jwt_signing_key"
// EncryptionKeyFile is the filename for the encryption key
EncryptionKeyFile = "encryption_key"
// KeyPerm is the file permission for secret keys (owner read/write only)
KeyPerm = 0600
)
// EnsureJWTSigningKey ensures a JWT signing key exists in the secrets directory.
// If no key exists, it generates and stores a new one with restrictive permissions.
// Returns the base64-encoded signing key.
func EnsureJWTSigningKey(secretsDir string) (string, error) {
log := getLogger()
// Ensure the secrets directory exists with restrictive permissions
if err := os.MkdirAll(secretsDir, 0700); err != nil {
return "", fmt.Errorf("failed to create secrets directory: %w", err)
}
keyPath := filepath.Join(secretsDir, JWTKeyFile)
// Check if the key file already exists
if _, err := os.Stat(keyPath); err == nil {
// Key file exists, read it
keyBytes, err := os.ReadFile(keyPath)
if err != nil {
return "", fmt.Errorf("failed to read JWT signing key: %w", err)
}
key := string(keyBytes)
if key == "" {
return "", fmt.Errorf("JWT signing key file is empty")
}
log.Debug("loaded existing JWT signing key from file")
return key, nil
}
// Key file doesn't exist, generate a new key
log.Info("generating new JWT signing key")
key, err := generateJWTSigningKey()
if err != nil {
return "", fmt.Errorf("failed to generate JWT signing key: %w", err)
}
// Write the key to the file with restrictive permissions
if err := os.WriteFile(keyPath, []byte(key), KeyPerm); err != nil {
return "", fmt.Errorf("failed to write JWT signing key: %w", err)
}
// Double-check permissions (some systems might ignore mode in WriteFile)
if err := os.Chmod(keyPath, KeyPerm); err != nil {
return "", fmt.Errorf("failed to set JWT signing key permissions: %w", err)
}
log.Info("JWT signing key generated and stored", "path", keyPath)
return key, nil
}
// generateJWTSigningKey generates a cryptographically secure random signing key
func generateJWTSigningKey() (string, error) {
keyBytes := make([]byte, KeyBytes)
if _, err := rand.Read(keyBytes); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
// Encode to base64 for easy storage and handling
key := base64.StdEncoding.EncodeToString(keyBytes)
return key, nil
}
// EnsureEncryptionKey ensures an encryption key exists in the secrets directory.
// If no key exists, it generates and stores a new one with restrictive permissions.
// Returns the base64-encoded encryption key.
func EnsureEncryptionKey(secretsDir string) (string, error) {
log := getLogger()
// Ensure the secrets directory exists with restrictive permissions
if err := os.MkdirAll(secretsDir, 0700); err != nil {
return "", fmt.Errorf("failed to create secrets directory: %w", err)
}
keyPath := filepath.Join(secretsDir, EncryptionKeyFile)
// Check if the key file already exists
if _, err := os.Stat(keyPath); err == nil {
// Key file exists, read it
keyBytes, err := os.ReadFile(keyPath)
if err != nil {
return "", fmt.Errorf("failed to read encryption key: %w", err)
}
key := string(keyBytes)
if key == "" {
return "", fmt.Errorf("encryption key file is empty")
}
log.Debug("loaded existing encryption key from file")
return key, nil
}
// Key file doesn't exist, generate a new key
log.Info("generating new encryption key")
key, err := generateEncryptionKey()
if err != nil {
return "", fmt.Errorf("failed to generate encryption key: %w", err)
}
// Write the key to the file with restrictive permissions
if err := os.WriteFile(keyPath, []byte(key), KeyPerm); err != nil {
return "", fmt.Errorf("failed to write encryption key: %w", err)
}
// Double-check permissions (some systems might ignore mode in WriteFile)
if err := os.Chmod(keyPath, KeyPerm); err != nil {
return "", fmt.Errorf("failed to set encryption key permissions: %w", err)
}
log.Info("encryption key generated and stored", "path", keyPath)
return key, nil
}
// generateEncryptionKey generates a cryptographically secure random encryption key
func generateEncryptionKey() (string, error) {
keyBytes := make([]byte, KeyBytes)
if _, err := rand.Read(keyBytes); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
// Encode to base64 for easy storage and handling
key := base64.StdEncoding.EncodeToString(keyBytes)
return key, nil
}

View File

@@ -0,0 +1,169 @@
package secrets
import (
"os"
"path/filepath"
"testing"
)
func TestEnsureJWTSigningKey(t *testing.T) {
// Create a temporary directory for testing
tempDir := t.TempDir()
secretsDir := filepath.Join(tempDir, "secrets")
t.Run("generates new key if not exists", func(t *testing.T) {
key, err := EnsureJWTSigningKey(secretsDir)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if key == "" {
t.Fatal("expected non-empty key")
}
// Check that the key file was created
keyPath := filepath.Join(secretsDir, JWTKeyFile)
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
t.Fatal("expected key file to exist")
}
// Check file permissions
info, err := os.Stat(keyPath)
if err != nil {
t.Fatalf("failed to stat key file: %v", err)
}
perm := info.Mode().Perm()
if perm != KeyPerm {
t.Errorf("expected permissions %o, got %o", KeyPerm, perm)
}
})
t.Run("loads existing key", func(t *testing.T) {
// First call to generate the key
key1, err := EnsureJWTSigningKey(secretsDir)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
// Second call should load the same key
key2, err := EnsureJWTSigningKey(secretsDir)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if key1 != key2 {
t.Error("expected same key on subsequent calls")
}
})
t.Run("fails if key file is empty", func(t *testing.T) {
emptyDir := filepath.Join(tempDir, "empty_test")
keyPath := filepath.Join(emptyDir, JWTKeyFile)
// Create empty key file
if err := os.MkdirAll(emptyDir, 0700); err != nil {
t.Fatalf("failed to create directory: %v", err)
}
if err := os.WriteFile(keyPath, []byte(""), KeyPerm); err != nil {
t.Fatalf("failed to write empty file: %v", err)
}
_, err := EnsureJWTSigningKey(emptyDir)
if err == nil {
t.Error("expected error for empty key file")
}
})
}
func TestEnsureEncryptionKey(t *testing.T) {
// Create a temporary directory for testing
tempDir := t.TempDir()
secretsDir := filepath.Join(tempDir, "secrets")
t.Run("generates new key if not exists", func(t *testing.T) {
key, err := EnsureEncryptionKey(secretsDir)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if key == "" {
t.Fatal("expected non-empty key")
}
// Check that the key file was created
keyPath := filepath.Join(secretsDir, EncryptionKeyFile)
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
t.Fatal("expected key file to exist")
}
// Check file permissions
info, err := os.Stat(keyPath)
if err != nil {
t.Fatalf("failed to stat key file: %v", err)
}
perm := info.Mode().Perm()
if perm != KeyPerm {
t.Errorf("expected permissions %o, got %o", KeyPerm, perm)
}
})
t.Run("loads existing key", func(t *testing.T) {
// First call to generate the key
key1, err := EnsureEncryptionKey(secretsDir)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
// Second call should load the same key
key2, err := EnsureEncryptionKey(secretsDir)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if key1 != key2 {
t.Error("expected same key on subsequent calls")
}
})
t.Run("fails if key file is empty", func(t *testing.T) {
emptyDir := filepath.Join(tempDir, "empty_encryption_test")
keyPath := filepath.Join(emptyDir, EncryptionKeyFile)
// Create empty key file
if err := os.MkdirAll(emptyDir, 0700); err != nil {
t.Fatalf("failed to create directory: %v", err)
}
if err := os.WriteFile(keyPath, []byte(""), KeyPerm); err != nil {
t.Fatalf("failed to write empty file: %v", err)
}
_, err := EnsureEncryptionKey(emptyDir)
if err == nil {
t.Error("expected error for empty key file")
}
})
}
func TestGenerateJWTSigningKey(t *testing.T) {
key, err := generateJWTSigningKey()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if key == "" {
t.Fatal("expected non-empty key")
}
// Check that each generated key is unique
key2, err := generateJWTSigningKey()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if key == key2 {
t.Error("expected different keys on each generation")
}
}