package handler import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "os" "strings" ) // getSecretKey returns the AES key derived from SECRET_KEY env var. // Mirrors Python's crypto._get_key(): first 32 bytes of utf-8 encoded key, zero-padded. func getSecretKey() []byte { keyStr := os.Getenv("SECRET_KEY") if keyStr == "" { keyStr = "dev-secret-key-change-in-production" } raw := []byte(keyStr) if len(raw) >= 32 { return raw[:32] } padded := make([]byte, 32) copy(padded, raw) return padded } // decryptPassword decrypts a base64-encoded AES-256-GCM ciphertext. // Mirrors Python's crypto.decrypt_password(). // Handles "enc:" prefix automatically. If not valid encrypted data, returns as-is. func decryptPassword(encrypted string) (string, error) { // Strip "enc:" prefix if present if strings.HasPrefix(encrypted, "enc:") { encrypted = strings.TrimPrefix(encrypted, "enc:") } raw, err := base64.StdEncoding.DecodeString(encrypted) if err != nil { return encrypted, nil // not valid base64 — plaintext } nonceSize := 12 if len(raw) <= nonceSize { return encrypted, nil } nonce := raw[:nonceSize] ct := raw[nonceSize:] key := getSecretKey() block, err := aes.NewCipher(key) if err != nil { return "", err } aesgcm, err := cipher.NewGCM(block) if err != nil { return "", err } plaintext, err := aesgcm.Open(nil, nonce, ct, nil) if err != nil { return encrypted, nil // decryption failed — return as-is } return string(plaintext), nil } // encryptPassword encrypts a plaintext password using AES-256-GCM. // Returns base64(nonce + ciphertext), same format as Python's encrypt_password. func encryptPassword(plaintext string) (string, error) { key := getSecretKey() block, err := aes.NewCipher(key) if err != nil { return "", err } aesgcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, 12) if _, err := rand.Read(nonce); err != nil { return "", err } ct := aesgcm.Seal(nil, nonce, []byte(plaintext), nil) return base64.StdEncoding.EncodeToString(append(nonce, ct...)), nil }