Files
Trackeep/backend/utils/security.go
T
Tomas Dvorak d27cf14110 first test
2026-02-08 14:14:55 +01:00

226 lines
5.4 KiB
Go

package utils
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"time"
)
// GenerateSecureSecret generates a cryptographically secure random secret
func GenerateSecureSecret(byteLength int) (string, error) {
bytes := make([]byte, byteLength)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
// GenerateSecureKey generates a hex-encoded encryption key
func GenerateSecureKey(bitLength int) (string, error) {
byteLength := bitLength / 8
bytes := make([]byte, byteLength)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
return hex.EncodeToString(bytes), nil
}
// GetOrCreateJWTSecret retrieves JWT secret from file or generates a new one
func GetOrCreateJWTSecret() (string, error) {
secretFile := "jwt_secret.key"
// Try to read existing secret
if secret, err := readSecretFromFile(secretFile); err == nil {
return secret, nil
}
// Generate new secret
secret, err := GenerateSecureSecret(32) // 32 bytes = 256 bits
if err != nil {
return "", fmt.Errorf("failed to generate JWT secret: %w", err)
}
// Save to file
if err := saveSecretToFile(secretFile, secret); err != nil {
return "", fmt.Errorf("failed to save JWT secret: %w", err)
}
return secret, nil
}
// GetOrCreateEncryptionKey retrieves encryption key from file or generates a new one
func GetOrCreateEncryptionKey() (string, error) {
keyFile := "encryption.key"
// Try to read existing key
if key, err := readSecretFromFile(keyFile); err == nil {
return key, nil
}
// Generate new key
key, err := GenerateSecureKey(256) // 256 bits
if err != nil {
return "", fmt.Errorf("failed to generate encryption key: %w", err)
}
// Save to file
if err := saveSecretToFile(keyFile, key); err != nil {
return "", fmt.Errorf("failed to save encryption key: %w", err)
}
return key, nil
}
// readSecretFromFile reads a secret from a file
func readSecretFromFile(filename string) (string, error) {
data, err := os.ReadFile(filename)
if err != nil {
return "", err
}
secret := string(data)
if secret == "" {
return "", fmt.Errorf("empty secret file")
}
return secret, nil
}
// saveSecretToFile saves a secret to a file with secure permissions
func saveSecretToFile(filename, secret string) error {
// Create the file with restricted permissions (only readable by owner)
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer file.Close()
_, err = file.WriteString(secret)
if err != nil {
return err
}
return nil
}
// ValidateSecretStrength checks if a secret meets minimum security requirements
func ValidateSecretStrength(secret string, minLength int) error {
if len(secret) < minLength {
return fmt.Errorf("secret too short: minimum %d characters required", minLength)
}
// Check entropy (basic check)
entropy := calculateEntropy(secret)
if entropy < 3.0 { // Minimum entropy threshold
return fmt.Errorf("secret has low entropy: %.2f (minimum 3.0)", entropy)
}
return nil
}
// calculateEntropy calculates the Shannon entropy of a string
func calculateEntropy(s string) float64 {
if len(s) == 0 {
return 0
}
// Count character frequencies
freq := make(map[rune]int)
for _, char := range s {
freq[char]++
}
// Calculate entropy
entropy := 0.0
length := float64(len(s))
for _, count := range freq {
if count > 0 {
p := float64(count) / length
entropy -= p * log2(p)
}
}
return entropy
}
// log2 calculates base-2 logarithm
func log2(x float64) float64 {
const ln2 = 0.6931471805599453 // ln(2)
return 1.0 / ln2 * logNatural(x)
}
// logNatural calculates natural logarithm using approximation
func logNatural(x float64) float64 {
if x <= 0 {
return 0
}
if x == 1 {
return 0
}
// Simple approximation for ln(x)
// For production, use math.Log from the standard library
n := 0.0
for x > 1.0 {
x /= 2.718281828459045 // e
n++
}
return n
}
// RotateSecret generates a new secret and updates the file
func RotateSecret(filename string) (string, error) {
// Generate new secret
var newSecret string
var err error
if filename == "jwt_secret.key" {
newSecret, err = GenerateSecureSecret(32)
} else if filename == "encryption.key" {
newSecret, err = GenerateSecureKey(256)
} else {
return "", fmt.Errorf("unknown secret file type: %s", filename)
}
if err != nil {
return "", fmt.Errorf("failed to generate new secret: %w", err)
}
// Backup old secret if it exists
if _, err := os.Stat(filename); err == nil {
backupFile := fmt.Sprintf("%s.backup.%d", filename, time.Now().Unix())
if err := os.Rename(filename, backupFile); err != nil {
return "", fmt.Errorf("failed to backup old secret: %w", err)
}
}
// Save new secret
if err := saveSecretToFile(filename, newSecret); err != nil {
return "", fmt.Errorf("failed to save new secret: %w", err)
}
return newSecret, nil
}
// GetSecretFilePath returns the full path to a secret file
func GetSecretFilePath(filename string) string {
// Store secrets in a secure directory
secretDir := os.Getenv("SECRET_DIR")
if secretDir == "" {
secretDir = "./secrets"
}
// Create directory if it doesn't exist
if err := os.MkdirAll(secretDir, 0700); err != nil {
// Fallback to current directory
return filename
}
return filepath.Join(secretDir, filename)
}