Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[_] switch to pbkdf2 from hash-wasm #1451

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
6 changes: 3 additions & 3 deletions src/app/auth/components/SignUp/useSignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -198,7 +198,7 @@ export function useSignUp(
password: string,
captcha: string,
): Promise<RegisterDetails> => {
const hashObj = passToHash({ password });
const hashObj = await passToHash({ password });
const encPass = encryptText(hashObj.hash);
const encSalt = encryptText(hashObj.salt);
const mnemonic = bip39.generateMnemonic(256);
Expand Down
18 changes: 9 additions & 9 deletions src/app/auth/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
const salt = decryptText(encryptedSalt);
const hashObj = passToHash({ password, salt });
const hashObj = await passToHash({ password, salt });
return encryptText(hashObj.hash);
},
async generateKeys(password: Password): Promise<Keys> {
Expand Down Expand Up @@ -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 };
Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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);

Expand Down Expand Up @@ -342,13 +342,13 @@ export const generateNew2FA = (): Promise<TwoFactorAuthQR> => {
return authClient.generateTwoFactorAuthQR(token);
};

export const deactivate2FA = (
export const deactivate2FA = async (
passwordSalt: string,
deactivationPassword: string,
deactivationCode: string,
): Promise<void> => {
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();
Expand All @@ -372,7 +372,7 @@ export const getNewToken = async (): Promise<string> => {

export async function areCredentialsCorrect(password: string): Promise<boolean> {
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);
Expand Down
81 changes: 71 additions & 10 deletions src/app/crypto/services/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -83,16 +98,60 @@ function getRipemd160FromHex(dataHex: string): Promise<string> {
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<string>} The result of PBKDF2 in HEX format
*/
function getPBKDF2(
password: string,
salt: string | Uint8Array,
iterations = PBKDF2_ITERATION_NUMBER,
hashLength = PBKDF2_TAG_LEN,
): Promise<string> {
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
Expand Down Expand Up @@ -165,4 +224,6 @@ export {
getSha512FromHex,
getRipemd160FromHex,
extendSecret,
getPBKDF2,
hex2oldEncoding,
};
147 changes: 147 additions & 0 deletions test/unit/services/utils.passToHash.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading