Orgs, Members, SSH and Admin page

This commit is contained in:
allanice001
2025-09-01 21:58:34 +01:00
parent 3f22521f49
commit 5425ed5dcc
61 changed files with 7138 additions and 819 deletions

85
internal/utils/crypto.go Normal file
View 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
}
// Gos 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)
}

101
internal/utils/keys.go Normal file
View File

@@ -0,0 +1,101 @@
package utils
import (
"encoding/base64"
"errors"
"fmt"
"github.com/glueops/autoglue/internal/db"
"github.com/glueops/autoglue/internal/db/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
func getMasterKey() ([]byte, error) {
var mk models.MasterKey
if err := db.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) ([]byte, error) {
var orgKey models.OrganizationKey
err := db.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()
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()
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.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.DB.Create(&orgKey).Error; err != nil {
return nil, fmt.Errorf("persist org key: %w", err)
}
return tenantKey, nil
}

View File

@@ -0,0 +1,71 @@
package utils
import (
"fmt"
"github.com/glueops/autoglue/internal/db"
"github.com/glueops/autoglue/internal/db/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
func EncryptForOrg(orgID uuid.UUID, plaintext []byte) (cipherB64, ivB64, tagB64 string, err error) {
tenantKey, err := getOrCreateTenantKey(orgID.String())
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) (string, error) {
tenantKey, err := getOrCreateTenantKey(orgID.String())
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
}
func EncryptAndUpsertCredential(orgID uuid.UUID, provider, plaintext string) error {
data, iv, tag, err := EncryptForOrg(orgID, []byte(plaintext))
if err != nil {
return err
}
var cred models.Credential
err = db.DB.Where("organization_id = ? AND provider = ?", orgID, provider).
First(&cred).Error
if err != nil && err != gorm.ErrRecordNotFound {
return err
}
cred.OrganizationID = orgID
cred.Provider = provider
cred.EncryptedData = data
cred.IV = iv
cred.Tag = tag
if cred.ID != uuid.Nil {
return db.DB.Save(&cred).Error
}
return db.DB.Create(&cred).Error
}