Fix: OIDC users losing credentials after re-login due to non-persistent encryption keys #363
@@ -70,7 +70,34 @@ class UserCrypto {
|
||||
}
|
||||
|
|
||||
|
||||
async setupOIDCUserEncryption(userId: string): Promise<void> {
|
||||
const DEK = crypto.randomBytes(UserCrypto.DEK_LENGTH);
|
||||
// Check if DEK already exists for this OIDC user
|
||||
const existingKEKSalt = await this.getKEKSalt(userId);
|
||||
const existingEncryptedDEK = await this.getEncryptedDEK(userId);
|
||||
|
||||
let DEK: Buffer;
|
||||
|
||||
if (existingKEKSalt && existingEncryptedDEK) {
|
||||
// User already has a persisted DEK, retrieve it
|
||||
const systemKey = this.deriveOIDCSystemKey(userId);
|
||||
DEK = this.decryptDEK(existingEncryptedDEK, systemKey);
|
||||
systemKey.fill(0);
|
||||
} else {
|
||||
// First time setup - create and persist new DEK
|
||||
DEK = crypto.randomBytes(UserCrypto.DEK_LENGTH);
|
||||
|
||||
// Generate a KEK salt for OIDC user (using a deterministic approach)
|
||||
const kekSalt = await this.generateKEKSalt();
|
||||
await this.storeKEKSalt(userId, kekSalt);
|
||||
|
||||
// Derive system key for OIDC user
|
||||
const systemKey = this.deriveOIDCSystemKey(userId);
|
||||
|
||||
// Encrypt and store the DEK
|
||||
const encryptedDEK = this.encryptDEK(DEK, systemKey);
|
||||
await this.storeEncryptedDEK(userId, encryptedDEK);
|
||||
|
||||
systemKey.fill(0);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
this.userSessions.set(userId, {
|
||||
@@ -135,34 +162,34 @@ class UserCrypto {
|
||||
async authenticateOIDCUser(userId: string): Promise<boolean> {
|
||||
|
The check for 
The check for `kekSalt` is redundant for OIDC users. The `kekSalt` is not used in the OIDC key derivation process, as `deriveOIDCSystemKey` uses the `userId` as its salt. Relying solely on `encryptedDEK` to determine if encryption is set up simplifies the code and removes an unnecessary database read.
```typescript
const encryptedDEK = await this.getEncryptedDEK(userId);
if (!encryptedDEK) {
// First time login or missing encryption data - set up encryption
await this.setupOIDCUserEncryption(userId);
return true;
}
```
2. Removed Redundant kekSalt LogicProblem IdentifiedCorrect - the Why It Was Redundant: Solution ImplementedIn
In Benefits:
### 2. **Removed Redundant kekSalt Logic**
#### Problem Identified
Correct - the `kekSalt` is generated and stored but **never actually used** for OIDC users. The `deriveOIDCSystemKey()` function uses `userId` directly as the salt, making the stored `kekSalt` completely redundant.
**Why It Was Redundant:**
```typescript
// kekSalt was generated and stored...
const kekSalt = await this.generateKEKSalt();
await this.storeKEKSalt(userId, kekSalt);
// ...but deriveOIDCSystemKey() doesn't use it!
private deriveOIDCSystemKey(userId: string): Buffer {
const systemSecret = process.env.OIDC_SYSTEM_SECRET || "termix-oidc-system-secret-default";
const salt = Buffer.from(userId, "utf8"); // ← Uses userId, not kekSalt!
return crypto.pbkdf2Sync(systemSecret, salt, 100000, UserCrypto.KEK_LENGTH, "sha256");
}
```
#### Solution Implemented
**In `setupOIDCUserEncryption()`:**
- ❌ Removed: `const kekSalt = await this.generateKEKSalt();`
- ❌ Removed: `await this.storeKEKSalt(userId, kekSalt);`
- ❌ Removed: `const existingKEKSalt = await this.getKEKSalt(userId);`
**In `authenticateOIDCUser()`:**
```typescript
// Before - Redundant check
const kekSalt = await this.getKEKSalt(userId);
const encryptedDEK = await this.getEncryptedDEK(userId);
if (!kekSalt || !encryptedDEK) {
// ...
}
// After - Only check what's needed
const encryptedDEK = await this.getEncryptedDEK(userId);
if (!encryptedDEK) {
// First time login or missing encryption data - set up encryption
await this.setupOIDCUserEncryption(userId);
return true;
}
```
**Benefits:**
- ✅ Removed unnecessary database operations
- ✅ Simplified code logic
- ✅ Removed potential confusion about kekSalt purpose
- ✅ Made it clear that OIDC and password-based auth have different key derivation methods
---
|
||||
try {
|
||||
const kekSalt = await this.getKEKSalt(userId);
|
||||
if (!kekSalt) {
|
||||
await this.setupOIDCUserEncryption(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
const systemKey = this.deriveOIDCSystemKey(userId);
|
||||
const encryptedDEK = await this.getEncryptedDEK(userId);
|
||||
if (!encryptedDEK) {
|
||||
systemKey.fill(0);
|
||||
|
||||
if (!kekSalt || !encryptedDEK) {
|
||||
// First time login or missing encryption data - set up encryption
|
||||
await this.setupOIDCUserEncryption(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Retrieve persisted DEK
|
||||
const systemKey = this.deriveOIDCSystemKey(userId);
|
||||
const DEK = this.decryptDEK(encryptedDEK, systemKey);
|
||||
systemKey.fill(0);
|
||||
|
||||
if (!DEK || DEK.length === 0) {
|
||||
// DEK decryption failed - recreate encryption
|
||||
await this.setupOIDCUserEncryption(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Clear any existing session
|
||||
const oldSession = this.userSessions.get(userId);
|
||||
if (oldSession) {
|
||||
oldSession.dataKey.fill(0);
|
||||
}
|
||||
|
||||
// Create new session with the persisted DEK
|
||||
this.userSessions.set(userId, {
|
||||
dataKey: Buffer.from(DEK),
|
||||
lastActivity: now,
|
||||
@@ -173,6 +200,7 @@ class UserCrypto {
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
// On error, set up fresh encryption
|
||||
await this.setupOIDCUserEncryption(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user
This new implementation introduces a critical race condition and contains redundant logic.
1. Critical Race Condition (Data Loss):
If two concurrent login requests for the same new OIDC user arrive, both can enter the
elseblock to create a new DEK. ThestoreEncryptedDEKfunction performs a non-atomicselect-then-update/insert, which can cause one request's DEK to overwrite the other's in the database. If a user's session uses a DEK that is different from the one stored, any data they save will be unrecoverable on their next login. This is a critical data loss scenario.2. Redundant
kekSaltLogic:The
kekSaltis generated, stored, and checked, but it's not used for OIDC key derivation (deriveOIDCSystemKeyuses theuserIdas the salt). This adds unnecessary complexity and database writes.Suggested Fix:
The following suggestion addresses both issues:
kekSalthandling.✅ Changes Made
1. Race Condition Mitigation (Critical Fix)
Problem Identified
You're right - the original implementation had a classic check-then-act race condition. Two concurrent login requests could both generate different DEKs, and whichever stored last would win, leaving the other session with a mismatched key.
Race Condition Scenario:
Solution Implemented
Added a "write-then-read-and-verify" pattern in
setupOIDCUserEncryption():How This Prevents Data Loss: