mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 04:40:05 +01:00
feat: sdk migration in progress
This commit is contained in:
85
internal/utils/crypto.go
Normal file
85
internal/utils/crypto.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoActiveMasterKey = errors.New("no active master key found")
|
||||
ErrInvalidOrgID = errors.New("invalid organization ID")
|
||||
ErrCredentialNotFound = errors.New("credential not found")
|
||||
ErrInvalidMasterKeyLen = errors.New("invalid master key length")
|
||||
)
|
||||
|
||||
func randomBytes(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
return nil, fmt.Errorf("rand: %w", err)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func encryptAESGCM(plaintext, key []byte) (cipherNoTag, iv, tag []byte, _ error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("gcm: %w", err)
|
||||
}
|
||||
if gcm.NonceSize() != 12 {
|
||||
return nil, nil, nil, fmt.Errorf("unexpected nonce size: %d", gcm.NonceSize())
|
||||
}
|
||||
|
||||
iv, err = randomBytes(gcm.NonceSize())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Go’s GCM returns ciphertext||tag, with 16-byte tag.
|
||||
cipherWithTag := gcm.Seal(nil, iv, plaintext, nil)
|
||||
if len(cipherWithTag) < 16 {
|
||||
return nil, nil, nil, errors.New("ciphertext too short")
|
||||
}
|
||||
tagLen := 16
|
||||
cipherNoTag = cipherWithTag[:len(cipherWithTag)-tagLen]
|
||||
tag = cipherWithTag[len(cipherWithTag)-tagLen:]
|
||||
return cipherNoTag, iv, tag, nil
|
||||
}
|
||||
|
||||
func decryptAESGCM(cipherNoTag, key, iv, tag []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cipher: %w", err)
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gcm: %w", err)
|
||||
}
|
||||
if gcm.NonceSize() != len(iv) {
|
||||
return nil, fmt.Errorf("bad nonce size: %d", len(iv))
|
||||
}
|
||||
// Reattach tag
|
||||
cipherWithTag := append(append([]byte{}, cipherNoTag...), tag...)
|
||||
plain, err := gcm.Open(nil, iv, cipherWithTag, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gcm open: %w", err)
|
||||
}
|
||||
return plain, nil
|
||||
}
|
||||
|
||||
func EncodeB64(b []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
func DecodeB64(s string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(s)
|
||||
}
|
||||
27
internal/utils/helpers.go
Normal file
27
internal/utils/helpers.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ErrorResponse is a simple, reusable error payload.
|
||||
// swagger:model ErrorResponse
|
||||
type ErrorResponse struct {
|
||||
// A machine-readable error code, e.g. "validation_error"
|
||||
// example: validation_error
|
||||
Code string `json:"code"`
|
||||
// Human-readable message
|
||||
// example: slug is required
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func WriteJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func WriteError(w http.ResponseWriter, status int, code, msg string) {
|
||||
WriteJSON(w, status, ErrorResponse{Code: code, Message: msg})
|
||||
}
|
||||
107
internal/utils/keys.go
Normal file
107
internal/utils/keys.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func getMasterKey(db *gorm.DB) ([]byte, error) {
|
||||
var mk models.MasterKey
|
||||
if err := db.Where("is_active = ?", true).Order("created_at DESC").First(&mk).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrNoActiveMasterKey
|
||||
}
|
||||
return nil, fmt.Errorf("querying master key: %w", err)
|
||||
}
|
||||
|
||||
keyBytes, err := base64.StdEncoding.DecodeString(mk.Key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding master key: %w", err)
|
||||
}
|
||||
if len(keyBytes) != 32 {
|
||||
return nil, fmt.Errorf("%w: got %d, want 32", ErrInvalidMasterKeyLen, len(keyBytes))
|
||||
}
|
||||
return keyBytes, nil
|
||||
}
|
||||
|
||||
func getOrCreateTenantKey(orgID string, db *gorm.DB) ([]byte, error) {
|
||||
var orgKey models.OrganizationKey
|
||||
err := db.Where("organization_id = ?", orgID).First(&orgKey).Error
|
||||
if err == nil {
|
||||
encKeyB64 := orgKey.EncryptedKey
|
||||
ivB64 := orgKey.IV
|
||||
tagB64 := orgKey.Tag
|
||||
|
||||
encryptedKey, err := DecodeB64(encKeyB64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode enc key: %w", err)
|
||||
}
|
||||
|
||||
iv, err := DecodeB64(ivB64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode iv: %w", err)
|
||||
}
|
||||
|
||||
tag, err := DecodeB64(tagB64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode tag: %w", err)
|
||||
}
|
||||
|
||||
masterKey, err := getMasterKey(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return decryptAESGCM(encryptedKey, masterKey, iv, tag)
|
||||
}
|
||||
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create new tenant key and wrap with the current master key
|
||||
orgUUID, err := uuid.Parse(orgID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrInvalidOrgID, err)
|
||||
}
|
||||
|
||||
tenantKey, err := randomBytes(32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tenant key gen: %w", err)
|
||||
}
|
||||
|
||||
masterKey, err := getMasterKey(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encrypted, iv, tag, err := encryptAESGCM(tenantKey, masterKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wrap tenant key: %w", err)
|
||||
}
|
||||
|
||||
var mk models.MasterKey
|
||||
if err := db.Where("is_active = ?", true).Order("created_at DESC").First(&mk).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrNoActiveMasterKey
|
||||
}
|
||||
return nil, fmt.Errorf("querying master key: %w", err)
|
||||
}
|
||||
|
||||
orgKey = models.OrganizationKey{
|
||||
OrganizationID: orgUUID,
|
||||
MasterKeyID: mk.ID,
|
||||
EncryptedKey: EncodeB64(encrypted),
|
||||
IV: EncodeB64(iv),
|
||||
Tag: EncodeB64(tag),
|
||||
}
|
||||
if err := db.Create(&orgKey).Error; err != nil {
|
||||
return nil, fmt.Errorf("persist org key: %w", err)
|
||||
}
|
||||
return tenantKey, nil
|
||||
}
|
||||
44
internal/utils/org-crypto.go
Normal file
44
internal/utils/org-crypto.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func EncryptForOrg(orgID uuid.UUID, plaintext []byte, db *gorm.DB) (cipherB64, ivB64, tagB64 string, err error) {
|
||||
tenantKey, err := getOrCreateTenantKey(orgID.String(), db)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
ct, iv, tag, err := encryptAESGCM(plaintext, tenantKey)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
return EncodeB64(ct), EncodeB64(iv), EncodeB64(tag), nil
|
||||
}
|
||||
|
||||
func DecryptForOrg(orgID uuid.UUID, cipherB64, ivB64, tagB64 string, db *gorm.DB) (string, error) {
|
||||
tenantKey, err := getOrCreateTenantKey(orgID.String(), db)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ct, err := DecodeB64(cipherB64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decode cipher: %w", err)
|
||||
}
|
||||
iv, err := DecodeB64(ivB64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decode iv: %w", err)
|
||||
}
|
||||
tag, err := DecodeB64(tagB64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decode tag: %w", err)
|
||||
}
|
||||
plain, err := decryptAESGCM(ct, tenantKey, iv, tag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(plain), nil
|
||||
}
|
||||
Reference in New Issue
Block a user