From 3e71d9697afb86be1dced535bd72c48b6f403a79 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Mon, 28 Oct 2024 19:24:16 +0530 Subject: [PATCH] feat: #8504 crypto helper methods --- packages/auth/src/helpers/crypto-helper.ts | 78 ++++++++++++++++++++++ packages/auth/src/index.ts | 2 +- packages/auth/src/social-auth.service.ts | 34 ++++++---- 3 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 packages/auth/src/helpers/crypto-helper.ts diff --git a/packages/auth/src/helpers/crypto-helper.ts b/packages/auth/src/helpers/crypto-helper.ts new file mode 100644 index 00000000000..e5b98af710f --- /dev/null +++ b/packages/auth/src/helpers/crypto-helper.ts @@ -0,0 +1,78 @@ +import { randomBytes, scrypt, timingSafeEqual } from 'crypto'; +import { promisify } from 'util'; +import { environment } from '@gauzy/config'; + +export class CryptoHelper { + private static readonly DEFAULT_SALT_ROUNDS = environment.USER_PASSWORD_BCRYPT_SALT_ROUNDS; // Default salt rounds if not specified + private static readonly KEY_LENGTH = 64; // Length of the derived key in bytes for the hash + private static readonly MAX_SALT_LENGTH = 16; // Maximum allowable salt length + private static readonly scryptAsync = promisify(scrypt); // Promisified scrypt function for async usage + + /** + * Ensures that the salt length is within an acceptable range. + * If rounds are specified outside the range, they will default to + * either 1 or 16 bytes based on the boundaries. + * + * @param rounds - Number of rounds intended for salt length. + * @returns Validated salt length in bytes. + */ + private static getSaltLength(rounds: number): number { + return Math.max(1, Math.min(rounds, this.MAX_SALT_LENGTH)); + } + + /** + * Synchronously generates a salt string in hexadecimal format. + * Uses `randomBytes` to create a salt of a specified length derived from + * the provided `rounds` parameter, defaulting to `DEFAULT_SALT_ROUNDS`. + * + * @param rounds - Number of rounds to determine the salt length, default is 10. + * @returns Hexadecimal string representing the salt. + */ + static genSaltSync(rounds: number = this.DEFAULT_SALT_ROUNDS): string { + const saltLength = this.getSaltLength(rounds); + return randomBytes(saltLength).toString('hex'); + } + + /** + * Asynchronously hashes a given data string or buffer with a generated salt. + * It uses Node.js `scrypt` function to derive a cryptographic hash based on + * the specified `saltLength`. The resulting salt and derived hash are combined + * into a single string, separated by a specified `separator`, defaulting to '.'. + * + * @param data - The data to hash, typically a string or Buffer. + * @param saltLength - Optional length for the salt; defaults to `DEFAULT_SALT_ROUNDS`. + * @param separator - Separator for salt and hash, defaults to '.'. + * @returns A promise resolving to a formatted string in the format 'salt.separator.hash'. + */ + static async hash( + data: string | Buffer, + saltLength: number = this.DEFAULT_SALT_ROUNDS, + separator: string = '.' + ): Promise { + const salt = this.genSaltSync(saltLength); // Generate salt synchronously + const derivedHash = (await this.scryptAsync(data, salt, this.KEY_LENGTH)) as Buffer; // Derive hash asynchronously + return `${salt}${separator}${derivedHash.toString('hex')}`; // Combine salt and hash for storage + } + + /** + * Asynchronously compares data against a stored encrypted hash to verify a match. + * This method splits the encrypted hash into salt and derived hash, rehashes + * the provided data using the same salt, and performs a secure, timing-safe + * comparison to avoid timing attacks. + * + * @param data - The data to verify, typically a plaintext string or Buffer. + * @param encrypted - The previously stored hash in the format 'salt:hash'. + * @param separator - Optional separator used to split the salt and hash; defaults to '.'. + * @returns A promise resolving to `true` if the data matches the stored hash, `false` otherwise. + */ + static async compare( + data: string | Buffer, // The input data to verify + encrypted: string, // The stored encrypted hash to compare against + separator: string = '.' // Optional separator; defaults to '.' + ): Promise { + const [salt, storedKey] = encrypted.split(separator); // Separate stored hash into salt and derived hash + const buffer = Buffer.from(storedKey, 'hex'); // Convert derived hash to a buffer for secure comparison + const derivedHash = (await this.scryptAsync(data, salt, this.KEY_LENGTH)) as Buffer; // Re-hash with the same salt + return timingSafeEqual(derivedHash, buffer); // Secure comparison to avoid timing attacks + } +} diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 34a05aea52b..7d914e0940d 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -1,4 +1,4 @@ export * from './social-auth.module'; export * from './social-auth.service'; - +export * from './helpers/crypto-helper'; export * from './internal'; diff --git a/packages/auth/src/social-auth.service.ts b/packages/auth/src/social-auth.service.ts index eea54c61e0b..f4d5909a6ea 100644 --- a/packages/auth/src/social-auth.service.ts +++ b/packages/auth/src/social-auth.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { ConfigService, IEnvironment } from '@gauzy/config'; import * as bcrypt from 'bcrypt'; +import { Response } from 'express'; +import { environment } from '@gauzy/config'; /** * Base class for social authentication. @@ -17,18 +18,17 @@ export abstract class BaseSocialAuth { @Injectable() export class SocialAuthService extends BaseSocialAuth { - protected readonly configService: ConfigService; - protected readonly saltRounds: number; - protected readonly clientBaseUrl: string; - constructor() { super(); - this.configService = new ConfigService(); - this.saltRounds = this.configService.get('USER_PASSWORD_BCRYPT_SALT_ROUNDS') as number; - this.clientBaseUrl = this.configService.get('clientBaseUrl') as keyof IEnvironment; } - public validateOAuthLoginEmail(args: []): any { } + /** + * Validate the email provided during OAuth login. + * + * @param args - An array containing the email to validate. + * @returns An object indicating whether the email is valid and an optional message. + */ + public validateOAuthLoginEmail(args: []): any {} /** * Generate a hash for the provided password. @@ -38,7 +38,10 @@ export class SocialAuthService extends BaseSocialAuth { */ public async getPasswordHash(password: string): Promise { try { - return await bcrypt.hash(password, this.saltRounds); + // Generate bcrypt hash using provided password and salt rounds from environment + const salt = await bcrypt.genSalt(environment.USER_PASSWORD_BCRYPT_SALT_ROUNDS); + // Use bcrypt to hash the password + return await bcrypt.hash(password, salt); } catch (error) { // Handle the error appropriately, e.g., log it or throw a custom error console.error('Error in getPasswordHash:', error); @@ -54,11 +57,16 @@ export class SocialAuthService extends BaseSocialAuth { * @param res - Express response object. * @returns The redirect response. */ - async routeRedirect(success: boolean, auth: { jwt: string; userId: string }, res: any) { + async routeRedirect(success: boolean, auth: { jwt: string; userId: string }, res: Response) { const { userId, jwt } = auth; - const redirectPath = success ? `#/sign-in/success?jwt=${jwt}&userId=${userId}` : `#/auth/register`; - const redirectUrl = `${this.clientBaseUrl}/${redirectPath}`; + // Construct the redirect path based on success status + const redirectPath = success + ? `#/sign-in/success?jwt=${encodeURIComponent(jwt)}&userId=${encodeURIComponent(userId)}` + : '#/auth/register'; + + // Construct the redirect URL + const redirectUrl = `${environment.clientBaseUrl}/${redirectPath}`; return res.redirect(redirectUrl); }