Skip to content

Commit d9a0e2b

Browse files
committed
fix: close three encryption safety gaps
1. Block legacy (v1) key rotation on encrypted projects — rotating without a master key would generate a new one, orphaning all S3 DEKs. 2. Hard-fail on invalid KEK in session_message.py (was soft-fail with LOG.warning, now LOG.error + return) to match skill_learner.py. 3. Remove EncryptProject retry guard — the 400 on already-enabled projects blocked crash recovery despite EncryptObject being idempotent.
1 parent d1a1d7b commit d9a0e2b

2 files changed

Lines changed: 20 additions & 6 deletions

File tree

src/server/api/go/internal/modules/handler/admin.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,11 @@ func (h *AdminHandler) EncryptProject(c *gin.Context) {
248248
return
249249
}
250250

251-
if project.EncryptionEnabled {
252-
c.JSON(http.StatusBadRequest, serializer.ParamErr("project encryption is already enabled", nil))
253-
return
254-
}
251+
// No early-return if EncryptionEnabled is already true — allow idempotent
252+
// retry so that partial failures (crash after flag set, before all objects
253+
// encrypted) can be recovered by re-calling this endpoint.
254+
// EncryptObject skips already-encrypted objects, and the DB UPDATE is a no-op
255+
// when the flag is already true.
255256

256257
// Set encryption_enabled = true FIRST for crash safety.
257258
// If we crash after this but before encrypting all objects, reads will use KEK
@@ -387,6 +388,16 @@ func (h *AdminHandler) RotateProjectSecretKey(c *gin.Context) {
387388
// Get the current KEK (master_key) from context — nil for legacy keys.
388389
masterKey := middleware.GetUserKEK(c)
389390

391+
// Guard: if the project has encryption enabled, we MUST have the current
392+
// master_key to re-wrap it. Legacy (v1) tokens don't carry a master_key,
393+
// so rotating them would generate a brand-new key and orphan all existing
394+
// S3 DEKs — irreversible data loss.
395+
if project.EncryptionEnabled && masterKey == nil {
396+
c.JSON(http.StatusBadRequest, serializer.ParamErr(
397+
"cannot rotate: project has encryption enabled but current API key has no embedded master key; re-issue a v2 key first", nil))
398+
return
399+
}
400+
390401
// Generates new auth_secret, re-wraps the same master_key.
391402
output, err := h.projectSvc.RotateSecretKey(c.Request.Context(), project.ID, masterKey)
392403
if err != nil {

src/server/core/acontext_core/service/session_message.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,16 @@ async def insert_new_message(body: InsertNewMessage, message: Message):
8787
wide["lock_retries"] = body.lock_retry_count
8888
wide["process_rightnow"] = body.process_rightnow
8989

90-
# Decode user KEK from base64 if present in the message
90+
# Decode user KEK from base64 if present in the message.
91+
# Hard-fail on invalid KEK — continuing with None would silently store
92+
# plaintext, inconsistent with skill_learner.py's hard-fail pattern.
9193
user_kek_bytes = None
9294
if body.user_kek:
9395
try:
9496
user_kek_bytes = base64.b64decode(body.user_kek)
9597
except Exception:
96-
LOG.warning("session_message.invalid_user_kek", session_id=str(body.session_id))
98+
LOG.error("session_message.invalid_user_kek", session_id=str(body.session_id))
99+
return
97100

98101
try:
99102
if pending_count > (

0 commit comments

Comments
 (0)