Skip to content

Commit

Permalink
commit 25
Browse files Browse the repository at this point in the history
  • Loading branch information
prayanshchh committed Nov 4, 2024
1 parent b1d22ae commit 88d4c4d
Show file tree
Hide file tree
Showing 11 changed files with 61 additions and 32 deletions.
1 change: 0 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ ENCRYPTION_KEY=
# Example generation: openssl rand -hex 32
# Example format: HASH_PEPPER=YOUR_HEX_STRING
# WARNING: Keep this value secret and never commit it to version control
HASH_PEPPER =
# NOTE: Changing this value will invalidate all existing email hashes.
# Ensure database migration strategy is in place before changing.
HASH_PEPPER=
1 change: 1 addition & 0 deletions setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ export async function setHashPepper(): Promise<void> {
}
} catch (err) {
console.error("An error occurred:", err);
abort();
}
}

Expand Down
11 changes: 5 additions & 6 deletions src/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,24 +252,23 @@ userSchema.plugin(mongoosePaginate);

userSchema.pre("save", async function (next) {
if (this.isModified("email")) {
if (!process.env.HASH_PEPPER) {
if (!process.env.HASH_PEPPER || !process.env.ENCRYPTION_KEY) {
return next(
new Error("HASH_PEPPER environment variable is not configured"),
);
new Error("Required environment variables are not configured"),
);
}

try {
const decrypted = decryptEmail(this.email).decrypted;
if (!validator.isEmail(decrypted)) {
return next(new Error("The provided email address is invalid"));
return next(new Error("Invalid email format"));
}

this.hashedEmail = hashEmail(decrypted);
} catch (error: unknown) {
const errorMessage =

Check failure on line 269 in src/models/User.ts

View workflow job for this annotation

GitHub Actions / Check for linting, formatting, and type errors

'errorMessage' is assigned a value but never used
error instanceof Error ? error.message : "Unknown error";
return next(new Error(`Email validation failed: ${errorMessage}`));
}
return next(new Error("Email validation failed")); }
}
next();
});
Expand Down
7 changes: 6 additions & 1 deletion src/resolvers/Mutation/signUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,13 @@ export const signUp: MutationResolvers["signUp"] = async (_parent, args) => {
);
}

const encryptedEmail = encryptEmail(normalizedEmail);
let encryptedEmail;

try {
encryptedEmail = encryptEmail(normalizedEmail);
} catch (error) {

Check failure on line 79 in src/resolvers/Mutation/signUp.ts

View workflow job for this annotation

GitHub Actions / Check for linting, formatting, and type errors

'error' is defined but never used
throw new Error("can't encrypt email");
}
const hashedPassword = await bcrypt.hash(args.data.password, 12);

// Upload file
Expand Down
15 changes: 12 additions & 3 deletions src/resolvers/Organization/members.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { GraphQLError } from "graphql";
import { logger } from "../../libraries";
import { User } from "../../models";
import type { InterfaceUser } from "../../models";
import type { OrganizationResolvers } from "../../types/generatedGraphQLTypes";
Expand All @@ -24,15 +26,22 @@ export const members: OrganizationResolvers["members"] = async (parent) => {

const decryptedUsers = users.map((user: InterfaceUser) => {
if (!user.email) {
console.warn(`User ${user._id} has no email`);
logger.warn('User missing email field', { userId: user._id });
return user;
}
try {
const { decrypted } = decryptEmail(user.email);
if (!decrypted) {
throw new Error('Decryption resulted in null or empty email');
}
return { ...user, email: decrypted };
} catch (error) {
console.error(`Failed to decrypt email`, error);
return user;
logger.error('Email decryption failed', {
userId: user._id,
error: error instanceof Error ? error.message : 'Unknown error'
});
// Consider throwing an error instead of silently continuing
throw new GraphQLError('Failed to process user data');
}
});

Expand Down
17 changes: 10 additions & 7 deletions src/resolvers/Post/creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ export const creator: PostResolvers["creator"] = async (parent) => {
try {
const decryptionResult = decryptEmail(creator.email);
if (!decryptionResult?.decrypted) {
console.error("Invalid decryption result");
throw new Error("Failed to decrypt email");
throw new Error("Invalid user data");
}
creator.email = decryptionResult.decrypted;
} catch (error) {
console.error("Failed to decrypt email:", error);
throw new Error("Failed to process creator email");
}
return {
...creator,
email: decryptionResult.decrypted
};
} catch (error) {
// Log the full error internally but don't expose details
console.error("[Creator Resolver] Decryption error:", error);
throw new Error("Unable to process user data");
}
}

return creator;
Expand Down
8 changes: 4 additions & 4 deletions src/resolvers/Query/me.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ export const me: QueryResolvers["me"] = async (_parent, _args, context) => {
const { decrypted } = decryptEmail(currentUser.email);
currentUser.email = decrypted;
}
catch(error)
{
console.error(`Failed to decrypt email`, error);
}
catch (error) {
console.error(`Failed to decrypt email`, error);
throw new Error('Unable to decrypt email');
}

return {
user: currentUser as InterfaceUser,
Expand Down
7 changes: 5 additions & 2 deletions src/utilities/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export function encryptEmail(email: string): string {

if (!encryptionKey) {
throw new Error("Encryption key is not defined.");
} else if (encryptionKey.length !== 64) {
} else if (encryptionKey.length !== 64 || !isValidHex(encryptionKey)) {
throw new Error(
"Encryption key must be a 256-bit hexadecimal string (64 characters).",
"Encryption key must be a valid 256-bit hexadecimal string (64 characters).",
);
}

Expand Down Expand Up @@ -65,6 +65,9 @@ export function decryptEmail(encryptedData: string): {
throw new Error("Invalid encrypted data: input is too short.");
}
const [iv, authTagHex, encryptedHex] = encryptedData.split(":");
if (!iv || !authTagHex || !encryptedHex) {
throw new Error("Invalid encrypted data format. Expected format 'iv:authTag:encryptedData'.");
}
if (!isValidHex(iv)) {
throw new Error("Invalid IV: not a hex string");
}
Expand Down
18 changes: 11 additions & 7 deletions src/utilities/hashEmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,17 @@ export function hashEmail(email: string): string {
*/

export function compareHashedEmails(a: string, b: string): boolean {
if (!a || !b || typeof a !== "string" || typeof b !== "string") {
return false;
}

if (!/^[0-9a-f]{64}$/i.test(a) || !/^[0-9a-f]{64}$/i.test(b)) {
return false;
}
// Ensure consistent timing regardless of input validity
const isValid =
!!a && !!b &&
typeof a === "string" &&
typeof b === "string" &&
/^[0-9a-f]{64}$/i.test(a) &&
/^[0-9a-f]{64}$/i.test(b);

if (!isValid) {
return false;
}

try {
return crypto.timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers/userAndOrg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const createTestUser = async (): Promise<TestUserType> => {
const email = `${nanoid(8)}${["", ".", "_"][Math.floor(Math.random() * 3)]}${nanoid(8)}@${["gmail.com", "example.com", "test.org"][Math.floor(Math.random() * 3)]}`;
const hashedEmail = hashEmail(email);

if (!hashedEmail || hashedEmail.length < 32) {
if (!hashedEmail || hashedEmail.length == 64) {
throw new Error("Invalid hashed email generated");
}
const encryptedEmail = encryptEmail(email);
Expand Down
6 changes: 6 additions & 0 deletions tests/utilities/encryptionModule.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ describe("encryptionModule", () => {
const iv = generateRandomIV();
expect(iv).toMatch(/^[0-9a-f]+$/i);
});

it("should handle malformed encrypted data gracefully", () => {
expect(() => decryptEmail("invalid:format")).toThrow();
expect(() => decryptEmail("invalid:format:data")).toThrow();
expect(() => decryptEmail("::::")).toThrow();
});
});

describe("encryptEmail and decryptEmail", () => {
Expand Down

0 comments on commit 88d4c4d

Please sign in to comment.