Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions model/getUserProfile.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const supabase = require("../dbConnection.js");
const { decrypt } = require("../services/encryptionService");

const LEGACY_HEX_TRIPLET_REGEX = /^[0-9a-f]+:[0-9a-f]+:[0-9a-f]+$/i;

function parseEncryptedPayload(rawValue) {
if (typeof rawValue !== "string") return null;
const trimmed = rawValue.trim();
Expand All @@ -23,6 +25,14 @@ function parseEncryptedPayload(rawValue) {
}
}

function looksLikeUndecryptedCiphertext(value) {
if (typeof value !== "string") return false;
const trimmed = value.trim();
if (!trimmed) return false;
if (LEGACY_HEX_TRIPLET_REGEX.test(trimmed)) return true;
return parseEncryptedPayload(trimmed) !== null;
}

async function maybeDecryptLegacyField(value) {
if (!value) return value;
const encryptedObj = parseEncryptedPayload(value);
Expand All @@ -31,8 +41,8 @@ async function maybeDecryptLegacyField(value) {
try {
return await decrypt(encryptedObj.encrypted, encryptedObj.iv, encryptedObj.authTag);
} catch (_error) {
// Keep profile fetch resilient for mixed plaintext/encrypted legacy rows.
return value;
// If decryption fails, do not leak encrypted payload text to clients.
return null;
}
}

Expand All @@ -44,10 +54,23 @@ async function decryptSensitiveFields(profile) {
const decryptedContact = await maybeDecryptLegacyField(profile.contact_number);
const decryptedAddress = await maybeDecryptLegacyField(profile.address);

const normalizedContact =
decryptedContact == null
? null
: looksLikeUndecryptedCiphertext(decryptedContact)
? null
: decryptedContact;
const normalizedAddress =
decryptedAddress == null
? null
: looksLikeUndecryptedCiphertext(decryptedAddress)
? null
: decryptedAddress;

return {
...profile,
contact_number: decryptedContact,
address: decryptedAddress,
contact_number: normalizedContact,
address: normalizedAddress,
};
}

Expand Down
31 changes: 27 additions & 4 deletions model/updateUserProfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const supabase = require("../dbConnection.js");
const { decode } = require("base64-arraybuffer");
const { encrypt, decrypt } = require("../services/encryptionService");

const LEGACY_HEX_TRIPLET_REGEX = /^[0-9a-f]+:[0-9a-f]+:[0-9a-f]+$/i;

function parseEncryptedPayload(rawValue) {
if (typeof rawValue !== "string") return null;
const trimmed = rawValue.trim();
Expand All @@ -24,6 +26,14 @@ function parseEncryptedPayload(rawValue) {
}
}

function looksLikeUndecryptedCiphertext(value) {
if (typeof value !== "string") return false;
const trimmed = value.trim();
if (!trimmed) return false;
if (LEGACY_HEX_TRIPLET_REGEX.test(trimmed)) return true;
return parseEncryptedPayload(trimmed) !== null;
}

async function maybeDecryptLegacyField(value) {
if (!value) return value;
const encryptedObj = parseEncryptedPayload(value);
Expand All @@ -32,8 +42,8 @@ async function maybeDecryptLegacyField(value) {
try {
return await decrypt(encryptedObj.encrypted, encryptedObj.iv, encryptedObj.authTag);
} catch (_error) {
// Keep profile reads resilient for mixed plaintext/encrypted legacy rows.
return value;
// If decryption fails, do not leak encrypted payload text to clients.
return null;
}
}

Expand All @@ -45,10 +55,23 @@ async function decryptSensitiveFields(profile) {
const decryptedContact = await maybeDecryptLegacyField(profile.contact_number);
const decryptedAddress = await maybeDecryptLegacyField(profile.address);

const normalizedContact =
decryptedContact == null
? null
: looksLikeUndecryptedCiphertext(decryptedContact)
? null
: decryptedContact;
const normalizedAddress =
decryptedAddress == null
? null
: looksLikeUndecryptedCiphertext(decryptedAddress)
? null
: decryptedAddress;

return {
...profile,
contact_number: decryptedContact,
address: decryptedAddress,
contact_number: normalizedContact,
address: normalizedAddress,
};
}

Expand Down
12 changes: 6 additions & 6 deletions services/userProfileService.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,21 +251,21 @@ async function updateCanonicalProfile({ actor, targetLookup, body }) {
attributes.address = null;
}

const updatedProfile = await updateUser({
await updateUser({
userId: existingProfile.user_id,
attributes
});

const mergedProfile = updatedProfile || existingProfile;

if (updates.userImage) {
mergedProfile.image_url = await saveImage(updates.userImage, existingProfile.user_id);
await saveImage(updates.userImage, existingProfile.user_id);
}

const preferences = await fetchUserPreferences(existingProfile.user_id);
// Re-read canonical profile so the response always includes decrypted
// sensitive fields from profile_encrypted (contactNumber/address).
const refreshed = await getCanonicalProfile({ userId: existingProfile.user_id });

return {
...buildProfileResponse(mergedProfile, preferences),
...refreshed,
message: 'Profile updated successfully',
meta: {
updatedBy: actor?.userId || null
Expand Down
Loading