inou/lib/crypto.go

193 lines
4.7 KiB
Go

package lib
import (
"crypto/aes"
"crypto/cipher"
"crypto/fips140"
"crypto/rand"
"crypto/sha256"
"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 random 16-character hex ID from UUID + hash
func NewID() string {
// Generate UUID v4 (crypto random)
uuid := make([]byte, 16)
if _, err := rand.Read(uuid); err != nil {
panic(err)
}
// Set version (4) and variant bits per RFC 4122
uuid[6] = (uuid[6] & 0x0f) | 0x40
uuid[8] = (uuid[8] & 0x3f) | 0x80
// Hash the UUID with SHA-256
hash := sha256.Sum256(uuid)
// Take first 8 bytes and return as hex (16 chars)
return fmt.Sprintf("%016x",
uint64(hash[0])<<56 | uint64(hash[1])<<48 | uint64(hash[2])<<40 | uint64(hash[3])<<32 |
uint64(hash[4])<<24 | uint64(hash[5])<<16 | uint64(hash[6])<<8 | uint64(hash[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
}