package models import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "fmt" "time" ) // SecretScope controls which resources a secret is visible to. type SecretScope string const ( SecretScopeGlobal SecretScope = "global" // admin-only; available to all repos SecretScopeWorkspace SecretScope = "workspace" // available to all repos in a workspace SecretScopeRepo SecretScope = "repo" // specific repository SecretScopeEnv SecretScope = "env" // specific environment (highest priority) ) // Secret stores an AES-256-GCM encrypted key/value pair. // Values are write-only: the API never returns the plaintext after creation. // Uniqueness: (scope, scope_id, name) must be unique. type Secret struct { ID int64 `xorm:"'id' pk autoincr" json:"id"` Scope SecretScope `xorm:"'scope' varchar(20) notnull" json:"scope"` ScopeID int64 `xorm:"'scope_id'" json:"scopeId"` // 0 for global Name string `xorm:"'name' varchar(255) notnull" json:"name"` EncryptedValue string `xorm:"'encrypted_value' text notnull" json:"-"` // never serialised CreatedAt time.Time `xorm:"'created_at' created" json:"createdAt"` UpdatedAt time.Time `xorm:"'updated_at' updated" json:"updatedAt"` } // ── Encryption helpers ──────────────────────────────────────────────────────── // deriveKey produces a 32-byte AES key from the session secret via SHA-256. func deriveKey(sessionSecret string) []byte { h := sha256.Sum256([]byte(sessionSecret)) return h[:] } // EncryptSecret encrypts plaintext with AES-256-GCM and returns a base64 string // of the form: base64(nonce || ciphertext). func EncryptSecret(plaintext, sessionSecret string) (string, error) { key := deriveKey(sessionSecret) block, err := aes.NewCipher(key) if err != nil { return "", fmt.Errorf("aes cipher: %w", err) } gcm, err := cipher.NewGCM(block) if err != nil { return "", fmt.Errorf("gcm: %w", err) } nonce := make([]byte, gcm.NonceSize()) if _, err := rand.Read(nonce); err != nil { return "", fmt.Errorf("nonce: %w", err) } sealed := gcm.Seal(nonce, nonce, []byte(plaintext), nil) return base64.StdEncoding.EncodeToString(sealed), nil } // DecryptSecret reverses EncryptSecret. func DecryptSecret(encrypted, sessionSecret string) (string, error) { data, err := base64.StdEncoding.DecodeString(encrypted) if err != nil { return "", fmt.Errorf("base64: %w", err) } key := deriveKey(sessionSecret) block, err := aes.NewCipher(key) if err != nil { return "", fmt.Errorf("aes cipher: %w", err) } gcm, err := cipher.NewGCM(block) if err != nil { return "", fmt.Errorf("gcm: %w", err) } if len(data) < gcm.NonceSize() { return "", fmt.Errorf("ciphertext too short") } nonce, ct := data[:gcm.NonceSize()], data[gcm.NonceSize():] pt, err := gcm.Open(nil, nonce, ct, nil) if err != nil { return "", fmt.Errorf("decrypt: %w", err) } return string(pt), nil }