package lib import ( "crypto/aes" "crypto/cipher" "crypto/fips140" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "os" "time" ) var masterKey []byte // CryptoInit loads the master key from file func CryptoInit(keyPath string) error { var err error masterKey, err = os.ReadFile(keyPath) if err != nil { return err } if len(masterKey) != 32 { return fmt.Errorf("master key must be 32 bytes, got %d", len(masterKey)) } return nil } // CryptoFIPSEnabled returns true if FIPS 140-3 mode is active func CryptoFIPSEnabled() bool { return fips140.Enabled() } // CryptoStatus returns a string describing crypto status func CryptoStatus() string { if fips140.Enabled() { return "FIPS 140-3 ENABLED" } return "FIPS 140-3 DISABLED (set GODEBUG=fips140=on)" } // deriveNonce derives a deterministic nonce from data using AES func deriveNonce(data []byte, nonceSize int) []byte { block, _ := aes.NewCipher(masterKey) nonce := make([]byte, nonceSize) for i := 0; i < len(data); i += 16 { chunk := make([]byte, 16) end := i + 16 if end > len(data) { end = len(data) } copy(chunk, data[i:end]) encrypted := make([]byte, 16) block.Encrypt(encrypted, chunk) for j := 0; j < nonceSize && j < 16; j++ { nonce[j] ^= encrypted[j] } } return nonce } // CryptoEncryptBytes encrypts binary data with AES-GCM (deterministic nonce) func CryptoEncryptBytes(plaintext []byte) []byte { if len(plaintext) == 0 { return nil } block, _ := aes.NewCipher(masterKey) gcm, _ := cipher.NewGCM(block) nonce := deriveNonce(plaintext, gcm.NonceSize()) ciphertext := gcm.Seal(nil, nonce, plaintext, nil) return append(nonce, ciphertext...) } // CryptoDecryptBytes decrypts binary data with AES-GCM func CryptoDecryptBytes(ciphertext []byte) ([]byte, error) { if len(ciphertext) == 0 { return nil, nil } block, err := aes.NewCipher(masterKey) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } if len(ciphertext) < gcm.NonceSize() { return nil, fmt.Errorf("ciphertext too short") } nonce := ciphertext[:gcm.NonceSize()] return gcm.Open(nil, nonce, ciphertext[gcm.NonceSize():], nil) } // CryptoEncrypt encrypts a string, returns base64 func CryptoEncrypt(plaintext string) string { if plaintext == "" { return "" } encrypted := CryptoEncryptBytes([]byte(plaintext)) return base64.StdEncoding.EncodeToString(encrypted) } // CryptoDecrypt decrypts a base64 string func CryptoDecrypt(ciphertext string) string { if ciphertext == "" { return "" } data, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return "" } result, err := CryptoDecryptBytes(data) if err != nil { return "" } return string(result) } // cryptoEncryptSIV - internal, used by DB wrappers func cryptoEncryptSIV(plaintext string) string { return CryptoEncrypt(plaintext) } // cryptoDecryptSIV - internal, used by DB wrappers func cryptoDecryptSIV(ciphertext string) string { return CryptoDecrypt(ciphertext) } // NewID generates a cryptographically random 63-bit positive ID as hex string func NewID() string { b := make([]byte, 8) if _, err := rand.Read(b); err != nil { panic(err) } b[0] &= 0x7F // Clear high bit to ensure positive int64 return fmt.Sprintf("%016x", int64(b[0])<<56 | int64(b[1])<<48 | int64(b[2])<<40 | int64(b[3])<<32 | int64(b[4])<<24 | int64(b[5])<<16 | int64(b[6])<<8 | int64(b[7])) } // Token holds the authenticated dossier and expiration type Token struct { DossierID string `json:"d"` Exp int64 `json:"exp"` } // TokenCreate creates an encrypted token for a dossier // duration is how long until expiration (e.g., 4*time.Hour) func TokenCreate(dossierID string, duration time.Duration) string { t := Token{ DossierID: dossierID, Exp: time.Now().Unix() + int64(duration.Seconds()), } data, _ := json.Marshal(t) encrypted := CryptoEncryptBytes(data) return base64.URLEncoding.EncodeToString(encrypted) } // TokenParse decrypts and validates a token // Returns the token if valid, or error if expired/invalid func TokenParse(tokenStr string) (*Token, error) { data, err := base64.URLEncoding.DecodeString(tokenStr) if err != nil { return nil, fmt.Errorf("invalid token encoding") } decrypted, err := CryptoDecryptBytes(data) if err != nil { return nil, fmt.Errorf("invalid token") } var t Token if err := json.Unmarshal(decrypted, &t); err != nil { return nil, fmt.Errorf("invalid token format") } if time.Now().Unix() > t.Exp { return nil, fmt.Errorf("token expired") } return &t, nil }