first test

This commit is contained in:
Tomas Dvorak
2026-02-08 14:14:55 +01:00
parent 18aa702174
commit d27cf14110
372 changed files with 98089 additions and 2585 deletions
+160
View File
@@ -0,0 +1,160 @@
package utils
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"os"
)
// GetEncryptionKey returns the encryption key from environment
func GetEncryptionKey() ([]byte, error) {
key := os.Getenv("ENCRYPTION_KEY")
if key == "" {
return nil, fmt.Errorf("ENCRYPTION_KEY environment variable not set")
}
// Hash the key to ensure it's exactly 32 bytes for AES-256
hash := sha256.Sum256([]byte(key))
return hash[:], nil
}
// Encrypt encrypts plaintext using AES-GCM
func Encrypt(plaintext string) (string, error) {
key, err := GetEncryptionKey()
if err != nil {
return "", err
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt decrypts ciphertext using AES-GCM
func Decrypt(ciphertext string) (string, error) {
key, err := GetEncryptionKey()
if err != nil {
return "", err
}
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return "", fmt.Errorf("ciphertext too short")
}
nonce, ciphertext_bytes := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext_bytes, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
// EncryptFile encrypts file content and returns the encrypted data
func EncryptFile(content []byte) ([]byte, error) {
key, err := GetEncryptionKey()
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// Prepend nonce to encrypted content
encrypted := gcm.Seal(nonce, nonce, content, nil)
return encrypted, nil
}
// DecryptFile decrypts file content
func DecryptFile(encryptedContent []byte) ([]byte, error) {
key, err := GetEncryptionKey()
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(encryptedContent) < nonceSize {
return nil, fmt.Errorf("encrypted content too short")
}
nonce, ciphertext := encryptedContent[:nonceSize], encryptedContent[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
// IsEncrypted checks if content appears to be encrypted (base64 encoded)
func IsEncrypted(content string) bool {
// Simple check: try to base64 decode and see if it looks like encrypted content
_, err := base64.StdEncoding.DecodeString(content)
return err == nil && len(content) > 32 // Encrypted content should be longer than 32 chars
}
// GenerateEncryptionKey generates a new random encryption key
func GenerateEncryptionKey() (string, error) {
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(key), nil
}
+184
View File
@@ -0,0 +1,184 @@
package utils
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// APIResponse represents a standard API response
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Message string `json:"message"`
Error string `json:"error,omitempty"`
Timestamp time.Time `json:"timestamp"`
RequestID string `json:"request_id,omitempty"`
}
// PaginatedResponse represents a paginated API response
type PaginatedResponse struct {
APIResponse
Pagination PaginationInfo `json:"pagination"`
}
// PaginationInfo contains pagination metadata
type PaginationInfo struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
// Success sends a successful response
func Success(c *gin.Context, data interface{}, message ...string) {
msg := "Operation successful"
if len(message) > 0 {
msg = message[0]
}
response := APIResponse{
Success: true,
Data: data,
Message: msg,
Timestamp: time.Now(),
RequestID: c.GetString("RequestID"),
}
c.JSON(http.StatusOK, response)
}
// Error sends an error response
func Error(c *gin.Context, statusCode int, err error, message ...string) {
msg := "An error occurred"
if len(message) > 0 {
msg = message[0]
}
response := APIResponse{
Success: false,
Message: msg,
Error: err.Error(),
Timestamp: time.Now(),
RequestID: c.GetString("RequestID"),
}
c.JSON(statusCode, response)
}
// ValidationError sends a validation error response
func ValidationError(c *gin.Context, errors interface{}) {
response := APIResponse{
Success: false,
Message: "Validation failed",
Error: "Invalid input data",
Data: errors,
Timestamp: time.Now(),
RequestID: c.GetString("RequestID"),
}
c.JSON(http.StatusBadRequest, response)
}
// Paginated sends a paginated response
func Paginated(c *gin.Context, data interface{}, pagination PaginationInfo, message ...string) {
msg := "Data retrieved successfully"
if len(message) > 0 {
msg = message[0]
}
response := PaginatedResponse{
APIResponse: APIResponse{
Success: true,
Data: data,
Message: msg,
Timestamp: time.Now(),
RequestID: c.GetString("RequestID"),
},
Pagination: pagination,
}
c.JSON(http.StatusOK, response)
}
// CalculatePagination calculates pagination information
func CalculatePagination(page, perPage int, total int64) PaginationInfo {
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 10
}
if perPage > 100 {
perPage = 100
}
totalPages := int((total + int64(perPage) - 1) / int64(perPage))
hasNext := page < totalPages
hasPrev := page > 1
return PaginationInfo{
Page: page,
PerPage: perPage,
Total: total,
TotalPages: totalPages,
HasNext: hasNext,
HasPrev: hasPrev,
}
}
// Created sends a created response
func Created(c *gin.Context, data interface{}, message ...string) {
msg := "Resource created successfully"
if len(message) > 0 {
msg = message[0]
}
response := APIResponse{
Success: true,
Data: data,
Message: msg,
Timestamp: time.Now(),
RequestID: c.GetString("RequestID"),
}
c.JSON(http.StatusCreated, response)
}
// Updated sends an updated response
func Updated(c *gin.Context, data interface{}, message ...string) {
msg := "Resource updated successfully"
if len(message) > 0 {
msg = message[0]
}
response := APIResponse{
Success: true,
Data: data,
Message: msg,
Timestamp: time.Now(),
RequestID: c.GetString("RequestID"),
}
c.JSON(http.StatusOK, response)
}
// Deleted sends a deleted response
func Deleted(c *gin.Context, message ...string) {
msg := "Resource deleted successfully"
if len(message) > 0 {
msg = message[0]
}
response := APIResponse{
Success: true,
Message: msg,
Timestamp: time.Now(),
RequestID: c.GetString("RequestID"),
}
c.JSON(http.StatusOK, response)
}
+225
View File
@@ -0,0 +1,225 @@
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)
}
+136
View File
@@ -0,0 +1,136 @@
package utils
import (
"fmt"
"regexp"
"strings"
"unicode/utf8"
)
// Validator provides common validation functions
type Validator struct {
errors map[string]string
}
// NewValidator creates a new validator instance
func NewValidator() *Validator {
return &Validator{
errors: make(map[string]string),
}
}
// Required checks if a field is not empty
func (v *Validator) Required(field, value string) *Validator {
if strings.TrimSpace(value) == "" {
v.errors[field] = fmt.Sprintf("%s is required", field)
}
return v
}
// MinLength checks if a field meets minimum length
func (v *Validator) MinLength(field, value string, min int) *Validator {
if utf8.RuneCountInString(value) < min {
v.errors[field] = fmt.Sprintf("%s must be at least %d characters", field, min)
}
return v
}
// MaxLength checks if a field exceeds maximum length
func (v *Validator) MaxLength(field, value string, max int) *Validator {
if utf8.RuneCountInString(value) > max {
v.errors[field] = fmt.Sprintf("%s must be at most %d characters", field, max)
}
return v
}
// Email checks if a field is a valid email
func (v *Validator) Email(field, value string) *Validator {
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(value) {
v.errors[field] = fmt.Sprintf("%s must be a valid email address", field)
}
return v
}
// URL checks if a field is a valid URL
func (v *Validator) URL(field, value string) *Validator {
urlRegex := regexp.MustCompile(`^https?://[^\s/$.?#].[^\s]*$`)
if !urlRegex.MatchString(value) {
v.errors[field] = fmt.Sprintf("%s must be a valid URL", field)
}
return v
}
// Match checks if a field matches a regex pattern
func (v *Validator) Match(field, value, pattern, message string) *Validator {
regex := regexp.MustCompile(pattern)
if !regex.MatchString(value) {
v.errors[field] = message
}
return v
}
// In checks if a field value is in the allowed values
func (v *Validator) In(field, value string, allowed []string) *Validator {
for _, allowedValue := range allowed {
if value == allowedValue {
return v
}
}
v.errors[field] = fmt.Sprintf("%s must be one of: %s", field, strings.Join(allowed, ", "))
return v
}
// HasErrors returns true if there are validation errors
func (v *Validator) HasErrors() bool {
return len(v.errors) > 0
}
// GetErrors returns all validation errors
func (v *Validator) GetErrors() map[string]string {
return v.errors
}
// GetError returns a specific field error
func (v *Validator) GetError(field string) string {
return v.errors[field]
}
// Clear clears all validation errors
func (v *Validator) Clear() *Validator {
v.errors = make(map[string]string)
return v
}
// ValidatePassword checks password strength
func (v *Validator) ValidatePassword(field, password string) *Validator {
if len(password) < 8 {
v.errors[field] = "Password must be at least 8 characters long"
return v
}
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
hasSpecial := regexp.MustCompile(`[!@#$%^&*(),.?":{}|<>]`).MatchString(password)
errorCount := 0
if !hasUpper {
errorCount++
}
if !hasLower {
errorCount++
}
if !hasNumber {
errorCount++
}
if !hasSpecial {
errorCount++
}
if errorCount > 1 {
v.errors[field] = "Password must contain at least 3 of: uppercase, lowercase, number, special character"
}
return v
}