Changeset 0.10.0 (#56)
This commit is contained in:
@@ -3,12 +3,14 @@ package handlers
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/crypto"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
)
|
||||
|
||||
@@ -36,11 +38,13 @@ type profileResponse struct {
|
||||
}
|
||||
|
||||
// SettingsHandler manages user profile and preferences.
|
||||
type SettingsHandler struct{}
|
||||
type SettingsHandler struct {
|
||||
uekCache *crypto.UEKCache
|
||||
}
|
||||
|
||||
// NewSettingsHandler creates a new handler.
|
||||
func NewSettingsHandler() *SettingsHandler {
|
||||
return &SettingsHandler{}
|
||||
func NewSettingsHandler(uekCache *crypto.UEKCache) *SettingsHandler {
|
||||
return &SettingsHandler{uekCache: uekCache}
|
||||
}
|
||||
|
||||
// ── Get Profile ─────────────────────────────
|
||||
@@ -155,6 +159,9 @@ func (h *SettingsHandler) ChangePassword(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Re-wrap UEK with new password so personal BYOK keys remain accessible
|
||||
h.rewrapVault(userID, req.CurrentPassword, req.NewPassword)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "password updated"})
|
||||
}
|
||||
|
||||
@@ -207,3 +214,60 @@ func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
|
||||
|
||||
h.GetSettings(c)
|
||||
}
|
||||
|
||||
// ── Vault Re-wrap ──────────────────────────
|
||||
|
||||
// rewrapVault decrypts the UEK with the old password and re-encrypts it with
|
||||
// the new password. This keeps personal BYOK keys accessible after a password
|
||||
// change. Uses a new salt for forward secrecy.
|
||||
func (h *SettingsHandler) rewrapVault(userID, oldPassword, newPassword string) {
|
||||
if h.uekCache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var vaultSet bool
|
||||
var encUEK, salt, nonce []byte
|
||||
err := database.DB.QueryRow(`
|
||||
SELECT vault_set, encrypted_uek, uek_salt, uek_nonce
|
||||
FROM users WHERE id = $1
|
||||
`, userID).Scan(&vaultSet, &encUEK, &salt, &nonce)
|
||||
if err != nil || !vaultSet || len(encUEK) == 0 {
|
||||
return // No vault to re-wrap
|
||||
}
|
||||
|
||||
// Decrypt UEK with old password
|
||||
oldPDK := crypto.DeriveKeyFromPassword(oldPassword, salt)
|
||||
uek, err := crypto.UnwrapUEK(encUEK, nonce, oldPDK)
|
||||
if err != nil {
|
||||
log.Printf("⚠ Vault re-wrap failed for user %s (unwrap): %v", userID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Re-wrap with new password using fresh salt
|
||||
newSalt, err := crypto.GenerateSalt()
|
||||
if err != nil {
|
||||
log.Printf("⚠ Vault re-wrap failed for user %s (salt): %v", userID, err)
|
||||
return
|
||||
}
|
||||
|
||||
newPDK := crypto.DeriveKeyFromPassword(newPassword, newSalt)
|
||||
newEncUEK, newNonce, err := crypto.WrapUEK(uek, newPDK)
|
||||
if err != nil {
|
||||
log.Printf("⚠ Vault re-wrap failed for user %s (wrap): %v", userID, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = database.DB.Exec(`
|
||||
UPDATE users
|
||||
SET encrypted_uek = $1, uek_salt = $2, uek_nonce = $3, updated_at = NOW()
|
||||
WHERE id = $4
|
||||
`, newEncUEK, newSalt, newNonce, userID)
|
||||
if err != nil {
|
||||
log.Printf("⚠ Vault re-wrap failed for user %s (persist): %v", userID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Session cache: UEK itself hasn't changed, just its wrapper
|
||||
h.uekCache.Store(userID, uek)
|
||||
log.Printf(" 🔐 Vault re-wrapped for user %s", userID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user