Changeset 0.9.4 (#54)
This commit is contained in:
@@ -15,6 +15,8 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/config"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/crypto"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
@@ -32,12 +34,13 @@ type Claims struct {
|
||||
}
|
||||
|
||||
type AuthHandler struct {
|
||||
cfg *config.Config
|
||||
stores store.Stores
|
||||
cfg *config.Config
|
||||
stores store.Stores
|
||||
uekCache *crypto.UEKCache
|
||||
}
|
||||
|
||||
func NewAuthHandler(cfg *config.Config, s store.Stores) *AuthHandler {
|
||||
return &AuthHandler{cfg: cfg, stores: s}
|
||||
func NewAuthHandler(cfg *config.Config, s store.Stores, uekCache *crypto.UEKCache) *AuthHandler {
|
||||
return &AuthHandler{cfg: cfg, stores: s, uekCache: uekCache}
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
@@ -90,6 +93,12 @@ func (h *AuthHandler) Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate and store the User Encryption Key (vault)
|
||||
if err := h.initVault(c.Request.Context(), user.ID, req.Password); err != nil {
|
||||
log.Printf("⚠ Failed to init vault for user %s: %v", user.ID, err)
|
||||
// Non-fatal: user can still use the app, vault will init on next login
|
||||
}
|
||||
|
||||
if !user.IsActive {
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "Account created but requires admin approval",
|
||||
@@ -133,6 +142,9 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Vault: unwrap UEK into session cache (or init if first login post-migration)
|
||||
h.unlockVault(c.Request.Context(), user, req.Password)
|
||||
|
||||
h.stores.Users.UpdateLastLogin(c.Request.Context(), user.ID)
|
||||
|
||||
tokens, err := h.generateTokens(user)
|
||||
@@ -189,6 +201,11 @@ func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
h.stores.Users.RevokeRefreshToken(c.Request.Context(), tokenHash)
|
||||
}
|
||||
|
||||
// Evict UEK from session cache
|
||||
if userID, exists := c.Get("user_id"); exists {
|
||||
h.uekCache.Evict(userID.(string))
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "logged out"})
|
||||
}
|
||||
|
||||
@@ -239,6 +256,85 @@ func hashToken(token string) string {
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
// ── Vault Lifecycle ─────────────────────────
|
||||
|
||||
// initVault generates a UEK, wraps it with the user's password, and stores
|
||||
// the encrypted UEK + salt + nonce on the user record. Called on registration
|
||||
// and on first login for pre-migration users.
|
||||
func (h *AuthHandler) initVault(ctx context.Context, userID, password string) error {
|
||||
if h.uekCache == nil {
|
||||
return nil // Vault not configured (e.g. tests)
|
||||
}
|
||||
|
||||
uek, err := crypto.GenerateUEK()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
salt, err := crypto.GenerateSalt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pdk := crypto.DeriveKeyFromPassword(password, salt)
|
||||
encryptedUEK, nonce, err := crypto.WrapUEK(uek, pdk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = database.DB.ExecContext(ctx, `
|
||||
UPDATE users
|
||||
SET encrypted_uek = $1, uek_salt = $2, uek_nonce = $3, vault_set = true
|
||||
WHERE id = $4
|
||||
`, encryptedUEK, salt, nonce, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache the UEK for the session
|
||||
h.uekCache.Store(userID, uek)
|
||||
log.Printf(" 🔐 Vault initialized for user %s", userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// unlockVault unwraps the UEK on login and caches it. If the user doesn't
|
||||
// have a vault yet (pre-migration account), initializes one.
|
||||
func (h *AuthHandler) unlockVault(ctx context.Context, user *models.User, password string) {
|
||||
if h.uekCache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var vaultSet bool
|
||||
var encryptedUEK, salt, nonce []byte
|
||||
|
||||
err := database.DB.QueryRowContext(ctx, `
|
||||
SELECT vault_set, encrypted_uek, uek_salt, uek_nonce
|
||||
FROM users WHERE id = $1
|
||||
`, user.ID).Scan(&vaultSet, &encryptedUEK, &salt, &nonce)
|
||||
if err != nil {
|
||||
log.Printf("⚠ Vault query failed for user %s: %v", user.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !vaultSet {
|
||||
// Pre-migration user: initialize vault on first login
|
||||
if err := h.initVault(ctx, user.ID, password); err != nil {
|
||||
log.Printf("⚠ Vault init on login failed for user %s: %v", user.ID, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Derive PDK from password and unwrap UEK
|
||||
pdk := crypto.DeriveKeyFromPassword(password, salt)
|
||||
uek, err := crypto.UnwrapUEK(encryptedUEK, nonce, pdk)
|
||||
if err != nil {
|
||||
log.Printf("⚠ Vault unlock failed for user %s: %v", user.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.uekCache.Store(user.ID, uek)
|
||||
}
|
||||
|
||||
// BootstrapAdmin creates/updates the admin user from env vars (K8s secret).
|
||||
func BootstrapAdmin(cfg *config.Config, s store.Stores) {
|
||||
if cfg.AdminUsername == "" || cfg.AdminPassword == "" {
|
||||
|
||||
Reference in New Issue
Block a user