Files
autoglue/internal/keys/keys.go
2025-11-02 13:19:30 +00:00

150 lines
3.4 KiB
Go

package keys
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"time"
"github.com/glueops/autoglue/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type GenOpts struct {
Alg string // "RS256"|"RS384"|"RS512"|"EdDSA"
Bits int // RSA bits (2048/3072/4096). ignored for EdDSA
KID string // optional; if empty we generate one
NBF *time.Time
EXP *time.Time
}
func GenerateAndStore(db *gorm.DB, encKeyB64 string, opts GenOpts) (*models.SigningKey, error) {
if opts.KID == "" {
opts.KID = uuid.NewString()
}
var pubPEM, privPEM []byte
var alg = opts.Alg
switch alg {
case "RS256", "RS384", "RS512":
if opts.Bits == 0 {
opts.Bits = 3072
}
priv, err := rsa.GenerateKey(rand.Reader, opts.Bits)
if err != nil {
return nil, err
}
privDER := x509.MarshalPKCS1PrivateKey(priv)
privPEM = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privDER})
pubDER := x509.MarshalPKCS1PublicKey(&priv.PublicKey)
pubPEM = pem.EncodeToMemory(&pem.Block{Type: "RSA PUBLIC KEY", Bytes: pubDER})
case "EdDSA":
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
privDER, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, err
}
privPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER})
pubDER, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return nil, err
}
pubPEM = pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER})
default:
return nil, fmt.Errorf("unsupported alg: %s", alg)
}
privateOut := string(privPEM)
if encKeyB64 != "" {
enc, err := encryptAESGCM(encKeyB64, privPEM)
if err != nil {
return nil, err
}
privateOut = enc
}
rec := models.SigningKey{
Kid: opts.KID,
Alg: alg,
Use: "sig",
IsActive: true,
PublicPEM: string(pubPEM),
PrivatePEM: privateOut,
NotBefore: opts.NBF,
ExpiresAt: opts.EXP,
}
if err := db.Create(&rec).Error; err != nil {
return nil, err
}
return &rec, nil
}
func encryptAESGCM(b64 string, plaintext []byte) (string, error) {
key, err := decode32ByteKey(b64)
if err != nil {
return "", err
}
if len(key) != 32 {
return "", errors.New("JWT_PRIVATE_ENC_KEY must be 32 bytes (base64url)")
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
aead, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, aead.NonceSize())
if _, err = rand.Read(nonce); err != nil {
return "", err
}
out := aead.Seal(nonce, nonce, plaintext, nil)
return "enc:aesgcm:" + base64.RawStdEncoding.EncodeToString(out), nil
}
func decryptAESGCM(b64 string, enc string) ([]byte, error) {
if !bytes.HasPrefix([]byte(enc), []byte("enc:aesgcm:")) {
return nil, errors.New("not encrypted")
}
key, err := decode32ByteKey(b64)
if err != nil {
return nil, err
}
blob, err := base64.RawStdEncoding.DecodeString(enc[len("enc:aesgcm:"):])
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := aead.NonceSize()
if len(blob) < nonceSize {
return nil, errors.New("ciphertext too short")
}
nonce, ct := blob[:nonceSize], blob[nonceSize:]
return aead.Open(nil, nonce, ct, nil)
}