diff --git a/src/app/auth/components/SignUp/useSignUp.ts b/src/app/auth/components/SignUp/useSignUp.ts index 6d73d37b9..8e9d1919d 100644 --- a/src/app/auth/components/SignUp/useSignUp.ts +++ b/src/app/auth/components/SignUp/useSignUp.ts @@ -88,7 +88,7 @@ export function useSignUp( } { const updateInfo: UpdateInfoFunction = async (email: string, password: string) => { // Setup hash and salt - const hashObj = passToHash({ password }); + const hashObj = await passToHash({ password }); const encPass = encryptText(hashObj.hash); const encSalt = encryptText(hashObj.salt); @@ -135,7 +135,7 @@ export function useSignUp( }; const doRegister = async (email: string, password: string, captcha: string) => { - const hashObj = passToHash({ password }); + const hashObj = await passToHash({ password }); const encPass = encryptText(hashObj.hash); const encSalt = encryptText(hashObj.salt); const mnemonic = bip39.generateMnemonic(256); @@ -198,7 +198,7 @@ export function useSignUp( password: string, captcha: string, ): Promise => { - const hashObj = passToHash({ password }); + const hashObj = await passToHash({ password }); const encPass = encryptText(hashObj.hash); const encSalt = encryptText(hashObj.salt); const mnemonic = bip39.generateMnemonic(256); diff --git a/src/app/auth/services/auth.service.ts b/src/app/auth/services/auth.service.ts index 9c3015cd3..1ab2cc18d 100644 --- a/src/app/auth/services/auth.service.ts +++ b/src/app/auth/services/auth.service.ts @@ -138,9 +138,9 @@ export const doLogin = async ( tfaCode: twoFactorCode, }; const cryptoProvider: CryptoProvider = { - encryptPasswordHash(password: Password, encryptedSalt: string): string { + async encryptPasswordHash(password: Password, encryptedSalt: string): Promise { const salt = decryptText(encryptedSalt); - const hashObj = passToHash({ password, salt }); + const hashObj = await passToHash({ password, salt }); return encryptText(hashObj.hash); }, async generateKeys(password: Password): Promise { @@ -225,7 +225,7 @@ export const getPasswordDetails = async ( } // Encrypt the password - const hashedCurrentPassword = passToHash({ password: currentPassword, salt }).hash; + const hashedCurrentPassword = (await passToHash({ password: currentPassword, salt })).hash; const encryptedCurrentPassword = encryptText(hashedCurrentPassword); return { salt, hashedCurrentPassword, encryptedCurrentPassword }; @@ -241,7 +241,7 @@ const updateCredentialsWithToken = async ( throw new Error('Invalid mnemonic'); } - const hashedNewPassword = passToHash({ password: newPassword }); + const hashedNewPassword = await passToHash({ password: newPassword }); const encryptedHashedNewPassword = encryptText(hashedNewPassword.hash); const encryptedHashedNewPasswordSalt = encryptText(hashedNewPassword.salt); @@ -266,7 +266,7 @@ const resetAccountWithToken = async (token: string | undefined, newPassword: str const encryptedNewMnemonic = encryptTextWithKey(newMnemonic, newPassword); - const hashedNewPassword = passToHash({ password: newPassword }); + const hashedNewPassword = await passToHash({ password: newPassword }); const encryptedHashedNewPassword = encryptText(hashedNewPassword.hash); const encryptedHashedNewPasswordSalt = encryptText(hashedNewPassword.salt); @@ -286,7 +286,7 @@ export const changePassword = async (newPassword: string, currentPassword: strin const { encryptedCurrentPassword } = await getPasswordDetails(currentPassword); // Encrypt the new password - const hashedNewPassword = passToHash({ password: newPassword }); + const hashedNewPassword = await passToHash({ password: newPassword }); const encryptedNewPassword = encryptText(hashedNewPassword.hash); const encryptedNewSalt = encryptText(hashedNewPassword.salt); @@ -342,13 +342,13 @@ export const generateNew2FA = (): Promise => { return authClient.generateTwoFactorAuthQR(token); }; -export const deactivate2FA = ( +export const deactivate2FA = async ( passwordSalt: string, deactivationPassword: string, deactivationCode: string, ): Promise => { const salt = decryptText(passwordSalt); - const hashObj = passToHash({ password: deactivationPassword, salt }); + const hashObj = await passToHash({ password: deactivationPassword, salt }); const encPass = encryptText(hashObj.hash); const token = localStorageService.get('xNewToken') || undefined; const authClient = SdkFactory.getNewApiInstance().createAuthClient(); @@ -372,7 +372,7 @@ export const getNewToken = async (): Promise => { export async function areCredentialsCorrect(password: string): Promise { const salt = await getSalt(); - const { hash: hashedPassword } = passToHash({ password, salt }); + const { hash: hashedPassword } = await passToHash({ password, salt }); const authClient = SdkFactory.getNewApiInstance().createAuthClient(); const token = localStorageService.get('xNewToken') ?? undefined; return authClient.areCredentialsCorrect(hashedPassword, token); diff --git a/src/app/crypto/services/utils.ts b/src/app/crypto/services/utils.ts index fcfd30dcf..2522b98c0 100644 --- a/src/app/crypto/services/utils.ts +++ b/src/app/crypto/services/utils.ts @@ -2,8 +2,23 @@ import CryptoJS from 'crypto-js'; import { DriveItemData } from '../../drive/types'; import { aes, items as itemUtils } from '@internxt/lib'; import { AdvancedSharedItem } from '../../share/types'; -import { createSHA512, createHMAC, sha256, createSHA256, sha512, ripemd160, blake3 } from 'hash-wasm'; +import { + createSHA512, + createHMAC, + sha256, + createSHA256, + sha512, + ripemd160, + blake3, + createSHA1, + pbkdf2, +} from 'hash-wasm'; import { Buffer } from 'buffer'; +import crypto from 'crypto'; + +const PBKDF2_ITERATION_NUMBER = 10000; +const PBKDF2_TAG_LEN = 32; +const SALT_LEN = 16; /** * Computes hmac-sha512 * @param {string} encryptionKeyHex - The hmac key in HEX format @@ -83,16 +98,60 @@ function getRipemd160FromHex(dataHex: string): Promise { return ripemd160(data); } -// Method to hash password. If salt is passed, use it, in other case use crypto lib for generate salt -function passToHash(passObject: PassObjectInterface): { salt: string; hash: string } { - const salt = passObject.salt ? CryptoJS.enc.Hex.parse(passObject.salt) : CryptoJS.lib.WordArray.random(128 / 8); - const hash = CryptoJS.PBKDF2(passObject.password, salt, { keySize: 256 / 32, iterations: 10000 }); - const hashedObjetc = { - salt: salt.toString(), - hash: hash.toString(), - }; +/** + * Converts HEX string to Uint8Array the same way CryptoJS did it (for compatibility) + * @param {string} hex - The input string in HEX + * @returns {Uint8Array} The resulting Uint8Array identical to what CryptoJS previously did + */ +function hex2oldEncoding(hex: string): Uint8Array { + const words: number[] = []; + for (let i = 0; i < hex.length; i += 8) { + words.push(parseInt(hex.slice(i, i + 8), 16) | 0); + } + const sigBytes = hex.length / 2; + const uint8Array = new Uint8Array(sigBytes); + + for (let i = 0; i < sigBytes; i++) { + uint8Array[i] = (words[i >>> 2] >>> ((3 - (i % 4)) * 8)) & 0xff; + } + + return uint8Array; +} +/** + * Computes PBKDF2 and outputs the result in HEX format + * @param {string} password - The password + * @param {number} salt - The salt + * @param {number}[iterations=PBKDF2_ITERATIONS] - The number of iterations to perform + * @param {number} [hashLength=PBKDF2_TAG_LEN] - The desired output length + * @returns {Promise} The result of PBKDF2 in HEX format + */ +function getPBKDF2( + password: string, + salt: string | Uint8Array, + iterations = PBKDF2_ITERATION_NUMBER, + hashLength = PBKDF2_TAG_LEN, +): Promise { + return pbkdf2({ + password, + salt, + iterations, + hashLength, + hashFunction: createSHA1(), + outputType: 'hex', + }); +} + +/** + * Password hash computation. If salt is passed, use it, otherwise generate salt + * @param {PassObjectInterface} passObject - The input object containing password and salt (optional) + * @returns {Promise<{salt: string; hash: string }>} The resulting hash and salt + */ +async function passToHash(passObject: PassObjectInterface): Promise<{ salt: string; hash: string }> { + const salt = passObject.salt ? passObject.salt : crypto.randomBytes(SALT_LEN).toString('hex'); + const encoded = hex2oldEncoding(salt); + const hash = await getPBKDF2(passObject.password, encoded); - return hashedObjetc; + return { salt, hash }; } // AES Plain text encryption method @@ -165,4 +224,6 @@ export { getSha512FromHex, getRipemd160FromHex, extendSecret, + getPBKDF2, + hex2oldEncoding, }; diff --git a/test/unit/services/utils.passToHash.test.ts b/test/unit/services/utils.passToHash.test.ts new file mode 100644 index 000000000..c9ad1bbf3 --- /dev/null +++ b/test/unit/services/utils.passToHash.test.ts @@ -0,0 +1,147 @@ +/** + * @jest-environment jsdom + */ + +import { getPBKDF2, passToHash } from '../../../src/app/crypto/services/utils'; + +import { describe, expect, it } from 'vitest'; +import CryptoJS from 'crypto-js'; + +describe('Test getPBKDF2 with RFC 6070 test vectors', () => { + it('getPBKDF2 should pass test 1 for PBKDF2-HMAC-SHA-1 from RFC 6070', async () => { + const password = 'password'; + const salt = 'salt'; + const iterations = 1; + const hashLength = 20; + const result = await getPBKDF2(password, salt, iterations, hashLength); + const testResult = '0c60c80f961f0e71f3a9b524af6012062fe037a6'; + expect(result).toBe(testResult); + }); + + it('getPBKDF2 should pass test 2 for PBKDF2-HMAC-SHA-1 from RFC 6070', async () => { + const password = 'password'; + const salt = 'salt'; + const iterations = 2; + const hashLength = 20; + const result = await getPBKDF2(password, salt, iterations, hashLength); + const testResult = 'ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957'; + expect(result).toBe(testResult); + }); + + it('getPBKDF2 should pass test 3 for PBKDF2-HMAC-SHA-1 from RFC 6070', async () => { + const password = 'password'; + const salt = 'salt'; + const iterations = 4096; + const hashLength = 20; + const result = await getPBKDF2(password, salt, iterations, hashLength); + const testResult = '4b007901b765489abead49d926f721d065a429c1'; + expect(result).toBe(testResult); + }); + + /*it('getPBKDF2 should pass test 4 for PBKDF2-HMAC-SHA-1 from RFC 6070', async () => { + const password = 'password'; + const salt = 'salt'; + const iterations = 16777216; + const hashLength = 20; + const result = await getPBKDF2(password, salt, iterations, hashLength); + const testResult = 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984'; + expect(result).toBe(testResult); + });*/ + + it('getPBKDF2 should pass test 5 for PBKDF2-HMAC-SHA-1 from RFC 6070', async () => { + const password = 'passwordPASSWORDpassword'; + const salt = 'saltSALTsaltSALTsaltSALTsaltSALTsalt'; + const iterations = 4096; + const hashLength = 25; + const result = await getPBKDF2(password, salt, iterations, hashLength); + const testResult = '3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038'; + expect(result).toBe(testResult); + }); + + it('getPBKDF2 should pass test 6 for PBKDF2-HMAC-SHA-1 from RFC 6070', async () => { + const password = 'pass\0word'; + const salt = 'sa\0lt'; + const iterations = 4096; + const hashLength = 16; + const result = await getPBKDF2(password, salt, iterations, hashLength); + const testResult = '56fa6aa75548099dcc37d7f03425e0c3'; + expect(result).toBe(testResult); + }); +}); + +describe('Test against other crypto libraries', () => { + it('PBKDF2 should be identical to CryptoJS result for a test string', async () => { + const password = 'Test between hash-wasm and CryptoJS'; + const salt = 'This is salt'; + const result = await getPBKDF2(password, salt); + const cryptoJSresult = CryptoJS.PBKDF2(password, salt, { keySize: 256 / 32, iterations: 10000 }).toString( + CryptoJS.enc.Hex, + ); + expect(result).toBe(cryptoJSresult); + }); + + it('PBKDF2 should be identical to CryptoJS result for an empty string', async () => { + const password = ''; + const salt = 'This is salt'; + const result = await getPBKDF2(password, salt); + const cryptoJSresult = CryptoJS.PBKDF2(password, salt, { keySize: 256 / 32, iterations: 10000 }).toString( + CryptoJS.enc.Hex, + ); + expect(result).toBe(cryptoJSresult); + }); +}); + +describe('Test passToHash', () => { + it('passToHash should return the same result for the same pwd and salt', async () => { + const password = 'Test password'; + const salt = '6c7c6b9938cb8bd0baf1c2d2171b96a0'; + const result1 = await passToHash({ password, salt }); + const result2 = await passToHash({ password, salt }); + expect(result1.hash).toBe(result2.hash); + expect(result1.salt).toBe(result2.salt); + }); + + it('passToHash should return the same result when re-computed', async () => { + const password = 'Test password'; + const salt = 'argon2id$6c7c6b9938cb8bd0baf1c2d2171b96a0'; + const result1 = await passToHash({ password, salt }); + const result2 = await passToHash({ password, salt: result1.salt }); + expect(result1.hash).toBe(result2.hash); + expect(result1.salt).toBe(result2.salt); + }); + + interface PassObjectInterface { + salt?: string | null; + password: string; + } + + function oldPassToHash(passObject: PassObjectInterface): { salt: string; hash: string } { + const salt = passObject.salt ? CryptoJS.enc.Hex.parse(passObject.salt) : CryptoJS.lib.WordArray.random(128 / 8); + const hash = CryptoJS.PBKDF2(passObject.password, salt, { keySize: 256 / 32, iterations: 10000 }); + const hashedObjetc = { + salt: salt.toString(), + hash: hash.toString(), + }; + + return hashedObjetc; + } + + it('passToHash should return the same result as the old function', async () => { + const password = 'Test password'; + const salt = '7121910994f21cd848c55e90835d7bd8'; + + const result = await passToHash({ password, salt }); + const oldResult = oldPassToHash({ password, salt }); + expect(result.salt).toBe(oldResult.salt); + expect(result.hash).toBe(oldResult.hash); + }); + + it('passToHash should sucessfully verify old function hash', async () => { + const password = 'Test password'; + const oldResult = oldPassToHash({ password }); + const result = await passToHash({ password, salt: oldResult.salt }); + + expect(result.salt).toBe(oldResult.salt); + expect(result.hash).toBe(oldResult.hash); + }); +});