mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 21:00:06 +01:00
150 lines
3.4 KiB
Go
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)
|
|
}
|