From 81a7a4f271a3c1acba5ea2b9fb9a1d4e32b8e019 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Mon, 2 Dec 2024 11:13:10 +0100 Subject: [PATCH 1/5] Switched to argon2 --- package.json | 3 +- .../modals/CreateFolderModal/index.tsx | 14 +- src/helpers/crypt/crypt.ts | 175 +++++++---- src/helpers/crypt/passToHash.spec.ts | 295 ++++++++++++++++++ src/services/AuthService.ts | 32 +- src/services/drive/database/driveLocalDB.ts | 2 +- yarn.lock | 45 ++- 7 files changed, 473 insertions(+), 93 deletions(-) create mode 100644 src/helpers/crypt/passToHash.spec.ts diff --git a/package.json b/package.json index e2030616b..911ce34a0 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@internxt/lib": "^1.2.0", "@internxt/mobile-sdk": "^0.2.41", "@internxt/rn-crypto": "^0.1.12", - "@internxt/sdk": "^1.4.96", + "@internxt/sdk": "^1.7.0", "@react-native-async-storage/async-storage": "1.21.0", "@react-navigation/bottom-tabs": "^6.2.0", "@react-navigation/native": "^6.1.18", @@ -78,6 +78,7 @@ "expo-status-bar": "~1.11.1", "expo-system-ui": "~2.9.4", "expo-updates": "~0.24.13", + "hash-wasm": "^4.12.0", "intl": "^1.2.5", "jwt-decode": "^3.1.2", "lodash": "^4.17.20", diff --git a/src/components/modals/CreateFolderModal/index.tsx b/src/components/modals/CreateFolderModal/index.tsx index 9c70aa44b..1eafb9c4d 100644 --- a/src/components/modals/CreateFolderModal/index.tsx +++ b/src/components/modals/CreateFolderModal/index.tsx @@ -1,18 +1,18 @@ import React, { useState } from 'react'; import { View } from 'react-native'; -import strings from '../../../../assets/lang/strings'; -import CenterModal from '../CenterModal'; -import AppButton from '../../AppButton'; -import AppTextInput from '../../AppTextInput'; +import { useDrive } from '@internxt-mobile/hooks/drive'; import drive from '@internxt-mobile/services/drive'; +import { driveLocalDB } from '@internxt-mobile/services/drive/database'; +import { useTailwind } from 'tailwind-rn'; +import strings from '../../../../assets/lang/strings'; import notificationsService from '../../../services/NotificationsService'; import { NotificationType } from '../../../types'; import { BaseModalProps } from '../../../types/ui'; -import { useTailwind } from 'tailwind-rn'; +import AppButton from '../../AppButton'; import AppText from '../../AppText'; -import { useDrive } from '@internxt-mobile/hooks/drive'; -import { driveLocalDB } from '@internxt-mobile/services/drive/database'; +import AppTextInput from '../../AppTextInput'; +import CenterModal from '../CenterModal'; interface CreateFolderModalProps extends BaseModalProps { onFolderCreated: () => void; diff --git a/src/helpers/crypt/crypt.ts b/src/helpers/crypt/crypt.ts index 339b79367..e8e9af940 100644 --- a/src/helpers/crypt/crypt.ts +++ b/src/helpers/crypt/crypt.ts @@ -1,25 +1,31 @@ import CryptoJS from 'crypto-js'; -import crypto from 'react-native-crypto'; +import { argon2id, createSHA1, pbkdf2 } from 'hash-wasm'; +// import crypto from 'react-native-crypto'; +import * as crypto from 'crypto'; import { constants } from '../../services/AppService'; import errorService from '../../services/ErrorService'; import AesUtils from '../aesUtils'; const password = constants.CRYPTO_SECRET || ''; // Force env var loading +/** + * Argon2id parameters taken from RFC9106 (variant for memory-constrained environments) + * * @constant + * @type {number} + * @default + */ +const ARGON2ID_PARALLELISM = 4; +const ARGON2ID_ITERATIONS = 3; +const ARGON2ID_MEMORY = 65536; +const ARGON2ID_TAG_LEN = 32; +const ARGON2ID_SALT_LEN = 16; + +const PBKDF2_ITERATIONS = 10000; +const PBKDF2_TAG_LEN = 32; + interface PassObjectInterface { + salt?: string | null; password: string; - salt?: string; -} - -export 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(), - }; - - return hashedObjetc; } // AES Plain text encryption method @@ -56,32 +62,6 @@ export function decryptTextWithKey(encryptedText: string, keyToDecrypt: string): } } -export function probabilisticEncryption(content: string): string | null { - try { - const b64 = crypto.createCipher('aes-256-gcm', constants.CRYPTO_SECRET); - - b64.write(content); - - const e64 = Buffer.concat([b64.update(content), b64.final()]).toString('base64'); - const eHex = Buffer.from(e64, 'base64').toString('hex'); - - return eHex; - } catch (error) { - return null; - } -} - -export function probabilisticDecryption(cipherText: string): string | null { - try { - const decrypt = crypto.createDecipher('aes-256-gcm', constants.CRYPTO_SECRET); - const plain = Buffer.concat([decrypt.update(cipherText), decrypt.final()]).toString('utf8'); - - return plain; - } catch (error) { - return null; - } -} - export function isValidFilename(filename: string) { const EXCLUDED = ['..']; if (EXCLUDED.includes(filename)) { @@ -104,31 +84,102 @@ export function encryptFilename(filename: string, folderId: string): string { return AesUtils.encrypt(filename, `${CRYPTO_KEY}-${folderId}`); } -export function deterministicEncryption(content: string, salt?: string | number): string | null { - try { - const key = Buffer.from(constants.CRYPTO_SECRET as string).toString('hex'); - const iv = salt ? Buffer.from(salt.toString()).toString('hex') : key; - const encrypt = crypto.createCipheriv('aes-256-gcm', key, iv); - const b64 = Buffer.concat([encrypt.update(content), encrypt.final()]).toString('base64'); - const eHex = Buffer.from(b64).toString('hex'); - - return eHex; - } catch (e) { - return null; +/** + * 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 + */ +export function getPBKDF2( + password: string, + salt: string | Uint8Array, + iterations = PBKDF2_ITERATIONS, + hashLength = PBKDF2_TAG_LEN, +): Promise { + return pbkdf2({ + password, + salt, + iterations, + hashLength, + hashFunction: createSHA1(), + outputType: 'hex', + }); +} + +/** + * Computes Argon2 and outputs the result in HEX format + * @param {string} password - The password + * @param {number} salt - The salt + * @param {number} [parallelism=ARGON2ID_PARALLELISM] - The parallelism degree + * @param {number}[iterations=ARGON2ID_ITERATIONS] - The number of iterations to perform + * @param {number}[memorySize=ARGON2ID_MEMORY] - The number of KB of memeory to use + * @param {number} [hashLength=ARGON2ID_TAG_LEN] - The desired output length + * @param {'hex'|'binary'|'encoded'} [outputType="encoded"] - The output type + * @returns {Promise} The result of Argon2 + */ +export function getArgon2( + password: string, + salt: string, + parallelism: number = ARGON2ID_PARALLELISM, + iterations: number = ARGON2ID_ITERATIONS, + memorySize: number = ARGON2ID_MEMORY, + hashLength: number = ARGON2ID_TAG_LEN, + outputType: 'hex' | 'binary' | 'encoded' = 'encoded', +): Promise { + return argon2id({ + password, + salt, + parallelism, + iterations, + memorySize, + hashLength, + outputType, + }); +} + +/** + * 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 + */ +export 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; } -export function deterministicDecryption(cipherText: string, salt?: string | number): string | null { - try { - const key = Buffer.from(constants.CRYPTO_SECRET as string).toString('hex'); - const iv = salt ? Buffer.from(salt.toString()).toString('hex') : key; - const reb64 = Buffer.from(cipherText).toString('hex'); - const bytes = Buffer.from(reb64).toString('base64'); - const decrypt = crypto.createDecipheriv('aes-256-gcm', key, iv); - const plain = Buffer.concat([decrypt.update(Buffer.from(bytes)), decrypt.final()]).toString('utf8'); - - return plain; - } catch (e) { - return null; +/** + * Password hash computation. If no salt or salt starts with 'argon2id$' - uses Argon2, else - PBKDF2 + * @param {PassObjectInterface} passObject - The input object containing password and salt (optional) + * @returns {Promise<{salt: string; hash: string }>} The resulting hash and salt + */ +export async function passToHash(passObject: PassObjectInterface): Promise<{ salt: string; hash: string }> { + let salt; + let hash; + + if (!passObject.salt) { + const argonSalt = crypto.randomBytes(ARGON2ID_SALT_LEN).toString('hex'); + hash = await getArgon2(passObject.password, argonSalt); + salt = 'argon2id$' + argonSalt; + } else if (passObject.salt.startsWith('argon2id$')) { + const argonSalt = passObject.salt.replace('argon2id$', ''); + hash = await getArgon2(passObject.password, argonSalt); + salt = passObject.salt; + } else { + salt = passObject.salt; + const encoded = hex2oldEncoding(salt); + hash = await getPBKDF2(passObject.password, encoded); } + return { salt, hash }; } diff --git a/src/helpers/crypt/passToHash.spec.ts b/src/helpers/crypt/passToHash.spec.ts new file mode 100644 index 000000000..cbd070ede --- /dev/null +++ b/src/helpers/crypt/passToHash.spec.ts @@ -0,0 +1,295 @@ +/** + * @jest-environment jsdom + */ + +import CryptoJS from 'crypto-js'; +import { getArgon2, getPBKDF2, hex2oldEncoding, passToHash } from './crypt'; + +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 getArgon2 with test vectors from the reference implementation that won Password Hashing Competition', () => { + it('getArgon2 should pass test 1', async () => { + const password = 'password'; + const salt = 'somesalt'; + const parallelism = 1; + const iterations = 2; + const memorySize = 65536; + const hashLength = 32; + const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); + const testResult = '09316115d5cf24ed5a15a31a3ba326e5cf32edc24702987c02b6566f61913cf7'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 2', async () => { + const password = 'password'; + const salt = 'somesalt'; + const parallelism = 1; + const iterations = 2; + const memorySize = 262144; + const hashLength = 32; + const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); + const testResult = '78fe1ec91fb3aa5657d72e710854e4c3d9b9198c742f9616c2f085bed95b2e8c'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 3', async () => { + const password = 'password'; + const salt = 'somesalt'; + const parallelism = 1; + const iterations = 2; + const memorySize = 256; + const hashLength = 32; + const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); + const testResult = '9dfeb910e80bad0311fee20f9c0e2b12c17987b4cac90c2ef54d5b3021c68bfe'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 4', async () => { + const password = 'password'; + const salt = 'somesalt'; + const parallelism = 2; + const iterations = 2; + const memorySize = 256; + const hashLength = 32; + const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); + const testResult = '6d093c501fd5999645e0ea3bf620d7b8be7fd2db59c20d9fff9539da2bf57037'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 5', async () => { + const password = 'password'; + const salt = 'somesalt'; + const parallelism = 1; + const iterations = 1; + const memorySize = 65536; + const hashLength = 32; + const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); + const testResult = 'f6a5adc1ba723dddef9b5ac1d464e180fcd9dffc9d1cbf76cca2fed795d9ca98'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 6', async () => { + const password = 'password'; + const salt = 'somesalt'; + const parallelism = 1; + const iterations = 4; + const memorySize = 65536; + const hashLength = 32; + const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); + const testResult = '9025d48e68ef7395cca9079da4c4ec3affb3c8911fe4f86d1a2520856f63172c'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 7', async () => { + const password = 'differentpassword'; + const salt = 'somesalt'; + const parallelism = 1; + const iterations = 2; + const memorySize = 65536; + const hashLength = 32; + const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); + const testResult = '0b84d652cf6b0c4beaef0dfe278ba6a80df6696281d7e0d2891b817d8c458fde'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 8', async () => { + const password = 'password'; + const salt = 'diffsalt'; + const parallelism = 1; + const iterations = 2; + const memorySize = 65536; + const hashLength = 32; + const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); + const testResult = 'bdf32b05ccc42eb15d58fd19b1f856b113da1e9a5874fdcc544308565aa8141c'; + 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 be identical to getArgon2 for an empry salt', async () => { + const password = 'Test password'; + const result = await passToHash({ password }); + const salt: string = result.salt.split('$').pop() ?? ''; + const argon2Result = await getArgon2(password, salt); + expect(result.hash).toBe(argon2Result); + }); + + it('passToHash should be identical to getArgon2 in argon mode', async () => { + const password = 'Test password'; + const salt = 'argon2id$6c7c6b9938cb8bd0baf1c2d2171b96a0'; + const result = await passToHash({ password, salt }); + const argon2Result = await getArgon2(password, '6c7c6b9938cb8bd0baf1c2d2171b96a0'); + expect(result.hash).toBe(argon2Result); + }); + + it('passToHash should be identical to getPBKDF2 in PBKDF2 mode', async () => { + const password = 'Test password'; + const salt = '1238cb8bd0baf1c2d2171b96a0'; + const result = await passToHash({ password, salt }); + const encoded_salt = hex2oldEncoding(salt); + const pbkdf2Result = await getPBKDF2(password, encoded_salt); + expect(result.hash).toBe(pbkdf2Result); + }); + + it('passToHash should return the same result for the given password and salt (argon mode)', async () => { + const password = 'Test password'; + const salt = 'argon2id$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 for the same pwd and salt (PBKDF2)', 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 for PBKDF2 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 return 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); + }); + + it('passToHash should throw an error if salt is empty', async () => { + const password = 'Test password'; + const salt = 'argon2id$'; + await expect(passToHash({ password, salt })).rejects.toThrow('Salt must be specified'); + }); + + it('passToHash should throw an error if password is empty', async () => { + const password = ''; + const salt = 'argon2id$6c7c6b9938cb8bd0baf1c2d2171b96a0'; + await expect(passToHash({ password, salt })).rejects.toThrow('Password must be specified'); + }); + + it('passToHash should throw an error if salt is less than 8 bytes', async () => { + const password = 'Test password'; + const salt = 'argon2id$6c'; + await expect(passToHash({ password, salt })).rejects.toThrow('Salt should be at least 8 bytes long'); + }); +}); diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index b466ca39a..947560c88 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -69,9 +69,9 @@ class AuthService { tfaCode, }, { - 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(): Promise { @@ -79,6 +79,14 @@ class AuthService { privateKeyEncrypted: '', publicKey: '', revocationCertificate: '', + ecc: { + privateKeyEncrypted: '', + publicKey: '', + }, + kyber: { + publicKey: '', + privateKeyEncrypted: '', + }, }; return keys; }, @@ -113,7 +121,7 @@ class AuthService { public async doRecoverPassword(newPassword: string): Promise { const xUser = await asyncStorageService.getUser(); const mnemonic = xUser.mnemonic; - const hashPass = passToHash({ password: newPassword }); + const hashPass = await passToHash({ password: newPassword }); const encryptedPassword = encryptText(hashPass.hash); const encryptedSalt = encryptText(hashPass.salt); const encryptedMnemonic = encryptTextWithKey(mnemonic, newPassword); @@ -140,10 +148,10 @@ class AuthService { if (!salt) { throw new Error('Internal server error. Please try later.'); } - const hashedCurrentPassword = passToHash({ password: params.password, salt }).hash; + const hashedCurrentPassword = (await passToHash({ password: params.password, salt })).hash; const encCurrentPass = encryptText(hashedCurrentPassword); - const hashedNewPassword = passToHash({ password: params.newPassword }); + const hashedNewPassword = await passToHash({ password: params.newPassword }); const encNewPass = encryptText(hashedNewPassword.hash); const encryptedNewSalt = encryptText(hashedNewPassword.salt); @@ -198,13 +206,13 @@ class AuthService { public async areCredentialsCorrect({ email, password }: { email: string; password: string }) { const plainSalt = await this.getSalt(email); - const { hash: hashedPassword } = passToHash({ password, salt: plainSalt }); + const { hash: hashedPassword } = await passToHash({ password, salt: plainSalt }); return this.sdk.auth.areCredentialsCorrect(email, hashedPassword) || false; } public async doRegister(params: RegisterParams) { - const hashObj = passToHash({ password: params.password }); + const hashObj = await passToHash({ password: params.password }); const encPass = encryptText(hashObj.hash); const encSalt = encryptText(hashObj.salt); const mnemonic = await this.getNewBits(); @@ -221,6 +229,14 @@ class AuthService { privateKeyEncrypted: '', publicKey: '', revocationCertificate: '', + ecc: { + privateKeyEncrypted: '', + publicKey: '', + }, + kyber: { + publicKey: '', + privateKeyEncrypted: '', + }, }, captcha: params.captcha, }; @@ -247,7 +263,7 @@ class AuthService { public async disable2FA(encryptedSalt: string, password: string, code: string) { const salt = decryptText(encryptedSalt); - const { hash } = passToHash({ password: password, salt }); + const { hash } = await passToHash({ password: password, salt }); const encryptedPassword = encryptText(hash); return this.sdk.auth.disableTwoFactorAuth(encryptedPassword, code); diff --git a/src/services/drive/database/driveLocalDB.ts b/src/services/drive/database/driveLocalDB.ts index af216d90c..49dbe4a6b 100644 --- a/src/services/drive/database/driveLocalDB.ts +++ b/src/services/drive/database/driveLocalDB.ts @@ -75,7 +75,7 @@ class DriveLocalDB { } public async saveFolderContent( - folderRecordData: { id: number; parentId: number; name: string; updatedAt: string }, + folderRecordData: { id: number; parentId: number; name: string; updatedAt: Date }, items: DriveItemData[], ) { const { id, parentId, name, updatedAt } = folderRecordData; diff --git a/yarn.lock b/yarn.lock index 4a2bbf3f1..6092895cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1765,14 +1765,14 @@ dependencies: buffer "^6.0.3" -"@internxt/sdk@^1.4.96": - version "1.5.19" - resolved "https://npm.pkg.github.com/download/@internxt/sdk/1.5.19/efbd2745afe86b551a32235ac92b1a54615aa949#efbd2745afe86b551a32235ac92b1a54615aa949" - integrity sha512-KIVs/KPQL1Hs1AAIG9d35Co+fKEMUQTnpW8XrxYw+uXJzvW7UWrw5Wl79nppw5x4HSx9E9Kg7y2LznyEtSjejA== +"@internxt/sdk@^1.7.0": + version "1.7.0" + resolved "https://npm.pkg.github.com/download/@internxt/sdk/1.7.0/8a277934c8065f39d43d0adb9c8751510d8820a9#8a277934c8065f39d43d0adb9c8751510d8820a9" + integrity sha512-6ysUQZu8JrEz/jN4JdzUmYL3tOOcYqWXigGPuCU2BDEPa652AsSQt80OG2pFAJzuj7+1CMUCqiqZJulmwL8dBg== dependencies: - axios "^0.24.0" + axios "^0.28.0" query-string "^7.1.0" - uuid "^8.3.2" + uuid "^11.0.2" "@isaacs/cliui@^8.0.2": version "8.0.2" @@ -4102,13 +4102,6 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.2.tgz#0aa167216965ac9474ccfa83892cfb6b3e1e52ef" integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw== -axios@^0.24.0: - version "0.24.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6" - integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA== - dependencies: - follow-redirects "^1.14.4" - axios@^0.27.2: version "0.27.2" resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" @@ -4117,6 +4110,15 @@ axios@^0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" +axios@^0.28.0: + version "0.28.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.28.1.tgz#2a7bcd34a3837b71ee1a5ca3762214b86b703e70" + integrity sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-core@^7.0.0-bridge.0: version "7.0.0-bridge.0" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" @@ -6786,7 +6788,7 @@ flow-parser@^0.206.0: resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.206.0.tgz#f4f794f8026535278393308e01ea72f31000bfef" integrity sha512-HVzoK3r6Vsg+lKvlIZzaWNBVai+FXTX1wdYhz/wVlH13tb/gOdLXmlTqy6odmTBhT5UoWUbq0k8263Qhr9d88w== -follow-redirects@^1.14.4, follow-redirects@^1.14.9: +follow-redirects@^1.14.9, follow-redirects@^1.15.0: version "1.15.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== @@ -7281,6 +7283,11 @@ hash-base@~3.0: inherits "^2.0.1" safe-buffer "^5.0.1" +hash-wasm@^4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/hash-wasm/-/hash-wasm-4.12.0.tgz#f9f1a9f9121e027a9acbf6db5d59452ace1ef9bb" + integrity sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ== + hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" @@ -10501,6 +10508,11 @@ property-expr@^2.0.4: resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" @@ -13161,6 +13173,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@^11.0.2: + version "11.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.3.tgz#248451cac9d1a4a4128033e765d137e2b2c49a3d" + integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg== + uuid@^3.0.1, uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" From 362c492ec79a9243a861fb10a972a5441722cc89 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Mon, 2 Dec 2024 11:52:04 +0100 Subject: [PATCH 2/5] Added call to update hash when login to new argon2 hash --- src/services/AuthService.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index 947560c88..41794aea4 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -105,6 +105,10 @@ class AuthService { const refreshedTokens = await this.refreshAuthToken(loginResult.newToken); if (!refreshedTokens?.token || !refreshedTokens?.newToken) throw new Error('Unable to refresh auth tokens'); + + const argon2 = await passToHash({ password }); + await this.sdk.auth.upgradeHash(argon2.hash, argon2.salt); + return { ...loginResult, token: refreshedTokens.token, From b3dad562c0c438488b77b5bf6593a3796f2d5317 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Mon, 2 Dec 2024 12:24:18 +0100 Subject: [PATCH 3/5] Minor change --- src/helpers/crypt/passToHash.spec.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/helpers/crypt/passToHash.spec.ts b/src/helpers/crypt/passToHash.spec.ts index cbd070ede..08901f5f1 100644 --- a/src/helpers/crypt/passToHash.spec.ts +++ b/src/helpers/crypt/passToHash.spec.ts @@ -1,7 +1,3 @@ -/** - * @jest-environment jsdom - */ - import CryptoJS from 'crypto-js'; import { getArgon2, getPBKDF2, hex2oldEncoding, passToHash } from './crypt'; From 848dd7119161642ed92651a03468a0fe9405cf93 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Mon, 2 Dec 2024 12:34:19 +0100 Subject: [PATCH 4/5] Reduced duplicated lines in passToHash tests --- src/helpers/crypt/passToHash.spec.ts | 306 +++++++++++++-------------- 1 file changed, 153 insertions(+), 153 deletions(-) diff --git a/src/helpers/crypt/passToHash.spec.ts b/src/helpers/crypt/passToHash.spec.ts index 08901f5f1..4d14e5b54 100644 --- a/src/helpers/crypt/passToHash.spec.ts +++ b/src/helpers/crypt/passToHash.spec.ts @@ -1,163 +1,163 @@ import CryptoJS from 'crypto-js'; import { getArgon2, getPBKDF2, hex2oldEncoding, passToHash } from './crypt'; -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); - }); +const testPBKDF2 = async ( + password: string, + salt: string, + iterations: number, + hashLength: number, + expectedHash: string, +) => { + const result = await getPBKDF2(password, salt, iterations, hashLength); + expect(result).toBe(expectedHash); +}; + +const testArgon2 = async ( + password: string, + salt: string, + parallelism: number, + iterations: number, + memorySize: number, + hashLength: number, + expectedHash: string, +) => { + const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); + expect(result).toBe(expectedHash); +}; - 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 getPBKDF2 with RFC 6070 test vectors', () => { + const pbkdf2TestCases = [ + { + password: 'password', + salt: 'salt', + iterations: 1, + hashLength: 20, + expected: '0c60c80f961f0e71f3a9b524af6012062fe037a6', + }, + { + password: 'password', + salt: 'salt', + iterations: 2, + hashLength: 20, + expected: 'ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957', + }, + { + password: 'password', + salt: 'salt', + iterations: 4096, + hashLength: 20, + expected: '4b007901b765489abead49d926f721d065a429c1', + }, + { + password: 'password', + salt: 'salt', + iterations: 16777216, + hashLength: 20, + expected: 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984', + }, + { + password: 'passwordPASSWORDpassword', + salt: 'saltSALTsaltSALTsaltSALTsaltSALTsalt', + iterations: 4096, + hashLength: 25, + expected: '3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038', + }, + { + password: 'pass\0word', + salt: 'sa\0lt', + iterations: 4096, + hashLength: 16, + expected: '56fa6aa75548099dcc37d7f03425e0c3', + }, + ]; + + pbkdf2TestCases.forEach(({ password, salt, iterations, hashLength, expected }, index) => { + it(`getPBKDF2 should pass test ${index + 1}`, async () => { + await testPBKDF2(password, salt, iterations, hashLength, expected); + }); }); }); -describe('Test getArgon2 with test vectors from the reference implementation that won Password Hashing Competition', () => { - it('getArgon2 should pass test 1', async () => { - const password = 'password'; - const salt = 'somesalt'; - const parallelism = 1; - const iterations = 2; - const memorySize = 65536; - const hashLength = 32; - const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); - const testResult = '09316115d5cf24ed5a15a31a3ba326e5cf32edc24702987c02b6566f61913cf7'; - expect(result).toBe(testResult); - }); - - it('getArgon2 should pass test 2', async () => { - const password = 'password'; - const salt = 'somesalt'; - const parallelism = 1; - const iterations = 2; - const memorySize = 262144; - const hashLength = 32; - const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); - const testResult = '78fe1ec91fb3aa5657d72e710854e4c3d9b9198c742f9616c2f085bed95b2e8c'; - expect(result).toBe(testResult); - }); - - it('getArgon2 should pass test 3', async () => { - const password = 'password'; - const salt = 'somesalt'; - const parallelism = 1; - const iterations = 2; - const memorySize = 256; - const hashLength = 32; - const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); - const testResult = '9dfeb910e80bad0311fee20f9c0e2b12c17987b4cac90c2ef54d5b3021c68bfe'; - expect(result).toBe(testResult); - }); - - it('getArgon2 should pass test 4', async () => { - const password = 'password'; - const salt = 'somesalt'; - const parallelism = 2; - const iterations = 2; - const memorySize = 256; - const hashLength = 32; - const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); - const testResult = '6d093c501fd5999645e0ea3bf620d7b8be7fd2db59c20d9fff9539da2bf57037'; - expect(result).toBe(testResult); - }); - - it('getArgon2 should pass test 5', async () => { - const password = 'password'; - const salt = 'somesalt'; - const parallelism = 1; - const iterations = 1; - const memorySize = 65536; - const hashLength = 32; - const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); - const testResult = 'f6a5adc1ba723dddef9b5ac1d464e180fcd9dffc9d1cbf76cca2fed795d9ca98'; - expect(result).toBe(testResult); - }); - - it('getArgon2 should pass test 6', async () => { - const password = 'password'; - const salt = 'somesalt'; - const parallelism = 1; - const iterations = 4; - const memorySize = 65536; - const hashLength = 32; - const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); - const testResult = '9025d48e68ef7395cca9079da4c4ec3affb3c8911fe4f86d1a2520856f63172c'; - expect(result).toBe(testResult); - }); - - it('getArgon2 should pass test 7', async () => { - const password = 'differentpassword'; - const salt = 'somesalt'; - const parallelism = 1; - const iterations = 2; - const memorySize = 65536; - const hashLength = 32; - const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); - const testResult = '0b84d652cf6b0c4beaef0dfe278ba6a80df6696281d7e0d2891b817d8c458fde'; - expect(result).toBe(testResult); - }); - - it('getArgon2 should pass test 8', async () => { - const password = 'password'; - const salt = 'diffsalt'; - const parallelism = 1; - const iterations = 2; - const memorySize = 65536; - const hashLength = 32; - const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); - const testResult = 'bdf32b05ccc42eb15d58fd19b1f856b113da1e9a5874fdcc544308565aa8141c'; - expect(result).toBe(testResult); +describe('Test getArgon2 with reference test vectors', () => { + const argon2TestCases = [ + { + password: 'password', + salt: 'somesalt', + parallelism: 1, + iterations: 2, + memorySize: 65536, + hashLength: 32, + expected: '09316115d5cf24ed5a15a31a3ba326e5cf32edc24702987c02b6566f61913cf7', + }, + { + password: 'password', + salt: 'somesalt', + parallelism: 1, + iterations: 2, + memorySize: 262144, + hashLength: 32, + expected: '78fe1ec91fb3aa5657d72e710854e4c3d9b9198c742f9616c2f085bed95b2e8c', + }, + { + password: 'password', + salt: 'somesalt', + parallelism: 1, + iterations: 2, + memorySize: 256, + hashLength: 32, + expected: '9dfeb910e80bad0311fee20f9c0e2b12c17987b4cac90c2ef54d5b3021c68bfe', + }, + { + password: 'password', + salt: 'somesalt', + parallelism: 2, + iterations: 2, + memorySize: 256, + hashLength: 32, + expected: '6d093c501fd5999645e0ea3bf620d7b8be7fd2db59c20d9fff9539da2bf57037', + }, + { + password: 'password', + salt: 'somesalt', + parallelism: 1, + iterations: 1, + memorySize: 65536, + hashLength: 32, + expected: 'f6a5adc1ba723dddef9b5ac1d464e180fcd9dffc9d1cbf76cca2fed795d9ca98', + }, + { + password: 'password', + salt: 'somesalt', + parallelism: 1, + iterations: 4, + memorySize: 65536, + hashLength: 32, + expected: '9025d48e68ef7395cca9079da4c4ec3affb3c8911fe4f86d1a2520856f63172c', + }, + { + password: 'differentpassword', + salt: 'somesalt', + parallelism: 1, + iterations: 2, + memorySize: 65536, + hashLength: 32, + expected: '0b84d652cf6b0c4beaef0dfe278ba6a80df6696281d7e0d2891b817d8c458fde', + }, + { + password: 'password', + salt: 'diffsalt', + parallelism: 1, + iterations: 2, + memorySize: 65536, + hashLength: 32, + expected: 'bdf32b05ccc42eb15d58fd19b1f856b113da1e9a5874fdcc544308565aa8141c', + }, + ]; + + argon2TestCases.forEach(({ password, salt, parallelism, iterations, memorySize, hashLength, expected }, index) => { + it(`getArgon2 should pass test ${index + 1}`, async () => { + await testArgon2(password, salt, parallelism, iterations, memorySize, hashLength, expected); + }); }); }); From 41a07efe10e624afbec692754ee7707c3c554ed6 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Tue, 24 Dec 2024 14:15:42 +0100 Subject: [PATCH 5/5] Update logic to login with new argon2 hash --- src/helpers/crypt/crypt.ts | 6 +- src/helpers/crypt/passToHash.spec.ts | 318 +++++++++++++-------------- src/services/AuthService.ts | 21 +- 3 files changed, 176 insertions(+), 169 deletions(-) diff --git a/src/helpers/crypt/crypt.ts b/src/helpers/crypt/crypt.ts index e8e9af940..5ab3783e9 100644 --- a/src/helpers/crypt/crypt.ts +++ b/src/helpers/crypt/crypt.ts @@ -122,11 +122,11 @@ export function getPBKDF2( export function getArgon2( password: string, salt: string, + outputType: 'hex' | 'binary' | 'encoded' = 'encoded', parallelism: number = ARGON2ID_PARALLELISM, iterations: number = ARGON2ID_ITERATIONS, memorySize: number = ARGON2ID_MEMORY, hashLength: number = ARGON2ID_TAG_LEN, - outputType: 'hex' | 'binary' | 'encoded' = 'encoded', ): Promise { return argon2id({ password, @@ -170,11 +170,11 @@ export async function passToHash(passObject: PassObjectInterface): Promise<{ sal if (!passObject.salt) { const argonSalt = crypto.randomBytes(ARGON2ID_SALT_LEN).toString('hex'); - hash = await getArgon2(passObject.password, argonSalt); + hash = await getArgon2(passObject.password, argonSalt, 'hex'); salt = 'argon2id$' + argonSalt; } else if (passObject.salt.startsWith('argon2id$')) { const argonSalt = passObject.salt.replace('argon2id$', ''); - hash = await getArgon2(passObject.password, argonSalt); + hash = await getArgon2(passObject.password, argonSalt, 'hex'); salt = passObject.salt; } else { salt = passObject.salt; diff --git a/src/helpers/crypt/passToHash.spec.ts b/src/helpers/crypt/passToHash.spec.ts index 4d14e5b54..ec04f3418 100644 --- a/src/helpers/crypt/passToHash.spec.ts +++ b/src/helpers/crypt/passToHash.spec.ts @@ -1,163 +1,163 @@ import CryptoJS from 'crypto-js'; import { getArgon2, getPBKDF2, hex2oldEncoding, passToHash } from './crypt'; -const testPBKDF2 = async ( - password: string, - salt: string, - iterations: number, - hashLength: number, - expectedHash: string, -) => { - const result = await getPBKDF2(password, salt, iterations, hashLength); - expect(result).toBe(expectedHash); -}; - -const testArgon2 = async ( - password: string, - salt: string, - parallelism: number, - iterations: number, - memorySize: number, - hashLength: number, - expectedHash: string, -) => { - const result = await getArgon2(password, salt, parallelism, iterations, memorySize, hashLength, 'hex'); - expect(result).toBe(expectedHash); -}; - describe('Test getPBKDF2 with RFC 6070 test vectors', () => { - const pbkdf2TestCases = [ - { - password: 'password', - salt: 'salt', - iterations: 1, - hashLength: 20, - expected: '0c60c80f961f0e71f3a9b524af6012062fe037a6', - }, - { - password: 'password', - salt: 'salt', - iterations: 2, - hashLength: 20, - expected: 'ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957', - }, - { - password: 'password', - salt: 'salt', - iterations: 4096, - hashLength: 20, - expected: '4b007901b765489abead49d926f721d065a429c1', - }, - { - password: 'password', - salt: 'salt', - iterations: 16777216, - hashLength: 20, - expected: 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984', - }, - { - password: 'passwordPASSWORDpassword', - salt: 'saltSALTsaltSALTsaltSALTsaltSALTsalt', - iterations: 4096, - hashLength: 25, - expected: '3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038', - }, - { - password: 'pass\0word', - salt: 'sa\0lt', - iterations: 4096, - hashLength: 16, - expected: '56fa6aa75548099dcc37d7f03425e0c3', - }, - ]; - - pbkdf2TestCases.forEach(({ password, salt, iterations, hashLength, expected }, index) => { - it(`getPBKDF2 should pass test ${index + 1}`, async () => { - await testPBKDF2(password, salt, iterations, hashLength, expected); - }); + 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 getArgon2 with reference test vectors', () => { - const argon2TestCases = [ - { - password: 'password', - salt: 'somesalt', - parallelism: 1, - iterations: 2, - memorySize: 65536, - hashLength: 32, - expected: '09316115d5cf24ed5a15a31a3ba326e5cf32edc24702987c02b6566f61913cf7', - }, - { - password: 'password', - salt: 'somesalt', - parallelism: 1, - iterations: 2, - memorySize: 262144, - hashLength: 32, - expected: '78fe1ec91fb3aa5657d72e710854e4c3d9b9198c742f9616c2f085bed95b2e8c', - }, - { - password: 'password', - salt: 'somesalt', - parallelism: 1, - iterations: 2, - memorySize: 256, - hashLength: 32, - expected: '9dfeb910e80bad0311fee20f9c0e2b12c17987b4cac90c2ef54d5b3021c68bfe', - }, - { - password: 'password', - salt: 'somesalt', - parallelism: 2, - iterations: 2, - memorySize: 256, - hashLength: 32, - expected: '6d093c501fd5999645e0ea3bf620d7b8be7fd2db59c20d9fff9539da2bf57037', - }, - { - password: 'password', - salt: 'somesalt', - parallelism: 1, - iterations: 1, - memorySize: 65536, - hashLength: 32, - expected: 'f6a5adc1ba723dddef9b5ac1d464e180fcd9dffc9d1cbf76cca2fed795d9ca98', - }, - { - password: 'password', - salt: 'somesalt', - parallelism: 1, - iterations: 4, - memorySize: 65536, - hashLength: 32, - expected: '9025d48e68ef7395cca9079da4c4ec3affb3c8911fe4f86d1a2520856f63172c', - }, - { - password: 'differentpassword', - salt: 'somesalt', - parallelism: 1, - iterations: 2, - memorySize: 65536, - hashLength: 32, - expected: '0b84d652cf6b0c4beaef0dfe278ba6a80df6696281d7e0d2891b817d8c458fde', - }, - { - password: 'password', - salt: 'diffsalt', - parallelism: 1, - iterations: 2, - memorySize: 65536, - hashLength: 32, - expected: 'bdf32b05ccc42eb15d58fd19b1f856b113da1e9a5874fdcc544308565aa8141c', - }, - ]; - - argon2TestCases.forEach(({ password, salt, parallelism, iterations, memorySize, hashLength, expected }, index) => { - it(`getArgon2 should pass test ${index + 1}`, async () => { - await testArgon2(password, salt, parallelism, iterations, memorySize, hashLength, expected); - }); +describe('Test getArgon2 with test vectors from the reference implementation that won Password Hashing Competition', () => { + it('getArgon2 should pass test 1', async () => { + const password = 'password'; + const salt = 'somesalt'; + const parallelism = 1; + const iterations = 2; + const memorySize = 65536; + const hashLength = 32; + const result = await getArgon2(password, salt, 'hex', parallelism, iterations, memorySize, hashLength); + const testResult = '09316115d5cf24ed5a15a31a3ba326e5cf32edc24702987c02b6566f61913cf7'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 2', async () => { + const password = 'password'; + const salt = 'somesalt'; + const parallelism = 1; + const iterations = 2; + const memorySize = 262144; + const hashLength = 32; + const result = await getArgon2(password, salt, 'hex', parallelism, iterations, memorySize, hashLength); + const testResult = '78fe1ec91fb3aa5657d72e710854e4c3d9b9198c742f9616c2f085bed95b2e8c'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 3', async () => { + const password = 'password'; + const salt = 'somesalt'; + const parallelism = 1; + const iterations = 2; + const memorySize = 256; + const hashLength = 32; + const result = await getArgon2(password, salt, 'hex', parallelism, iterations, memorySize, hashLength); + const testResult = '9dfeb910e80bad0311fee20f9c0e2b12c17987b4cac90c2ef54d5b3021c68bfe'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 4', async () => { + const password = 'password'; + const salt = 'somesalt'; + const parallelism = 2; + const iterations = 2; + const memorySize = 256; + const hashLength = 32; + const result = await getArgon2(password, salt, 'hex', parallelism, iterations, memorySize, hashLength); + const testResult = '6d093c501fd5999645e0ea3bf620d7b8be7fd2db59c20d9fff9539da2bf57037'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 5', async () => { + const password = 'password'; + const salt = 'somesalt'; + const parallelism = 1; + const iterations = 1; + const memorySize = 65536; + const hashLength = 32; + const result = await getArgon2(password, salt, 'hex', parallelism, iterations, memorySize, hashLength); + const testResult = 'f6a5adc1ba723dddef9b5ac1d464e180fcd9dffc9d1cbf76cca2fed795d9ca98'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 6', async () => { + const password = 'password'; + const salt = 'somesalt'; + const parallelism = 1; + const iterations = 4; + const memorySize = 65536; + const hashLength = 32; + const result = await getArgon2(password, salt, 'hex', parallelism, iterations, memorySize, hashLength); + const testResult = '9025d48e68ef7395cca9079da4c4ec3affb3c8911fe4f86d1a2520856f63172c'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 7', async () => { + const password = 'differentpassword'; + const salt = 'somesalt'; + const parallelism = 1; + const iterations = 2; + const memorySize = 65536; + const hashLength = 32; + const result = await getArgon2(password, salt, 'hex', parallelism, iterations, memorySize, hashLength); + const testResult = '0b84d652cf6b0c4beaef0dfe278ba6a80df6696281d7e0d2891b817d8c458fde'; + expect(result).toBe(testResult); + }); + + it('getArgon2 should pass test 8', async () => { + const password = 'password'; + const salt = 'diffsalt'; + const parallelism = 1; + const iterations = 2; + const memorySize = 65536; + const hashLength = 32; + const result = await getArgon2(password, salt, 'hex', parallelism, iterations, memorySize, hashLength); + const testResult = 'bdf32b05ccc42eb15d58fd19b1f856b113da1e9a5874fdcc544308565aa8141c'; + expect(result).toBe(testResult); }); }); @@ -188,7 +188,7 @@ describe('Test passToHash', () => { const password = 'Test password'; const result = await passToHash({ password }); const salt: string = result.salt.split('$').pop() ?? ''; - const argon2Result = await getArgon2(password, salt); + const argon2Result = await getArgon2(password, salt, 'hex'); expect(result.hash).toBe(argon2Result); }); @@ -196,7 +196,7 @@ describe('Test passToHash', () => { const password = 'Test password'; const salt = 'argon2id$6c7c6b9938cb8bd0baf1c2d2171b96a0'; const result = await passToHash({ password, salt }); - const argon2Result = await getArgon2(password, '6c7c6b9938cb8bd0baf1c2d2171b96a0'); + const argon2Result = await getArgon2(password, '6c7c6b9938cb8bd0baf1c2d2171b96a0', 'hex'); expect(result.hash).toBe(argon2Result); }); @@ -262,7 +262,7 @@ describe('Test passToHash', () => { expect(result.hash).toBe(oldResult.hash); }); - it('passToHash should return sucessfully verify old function hash', async () => { + 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 }); @@ -271,19 +271,19 @@ describe('Test passToHash', () => { expect(result.hash).toBe(oldResult.hash); }); - it('passToHash should throw an error if salt is empty', async () => { + it('passToHash in argon2 mode should throw an error if salt is given but the actual value is empty', async () => { const password = 'Test password'; const salt = 'argon2id$'; await expect(passToHash({ password, salt })).rejects.toThrow('Salt must be specified'); }); - it('passToHash should throw an error if password is empty', async () => { + it('passToHash in argon2 mode should throw an error if password is empty', async () => { const password = ''; const salt = 'argon2id$6c7c6b9938cb8bd0baf1c2d2171b96a0'; await expect(passToHash({ password, salt })).rejects.toThrow('Password must be specified'); }); - it('passToHash should throw an error if salt is less than 8 bytes', async () => { + it('passToHash in argon2 mode should throw an error if salt is less than 8 bytes', async () => { const password = 'Test password'; const salt = 'argon2id$6c'; await expect(passToHash({ password, salt })).rejects.toThrow('Salt should be at least 8 bytes long'); diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index 41794aea4..ae7b34b47 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -106,13 +106,20 @@ class AuthService { if (!refreshedTokens?.token || !refreshedTokens?.newToken) throw new Error('Unable to refresh auth tokens'); - const argon2 = await passToHash({ password }); - await this.sdk.auth.upgradeHash(argon2.hash, argon2.salt); + const salt = await this.getSalt(email); + + let changedPasswordNewToken; + let changedPasswordToken; + if (!salt.startsWith('argon2id$')) { + const changePasswordResponse = await this.doChangePassword({ password: password, newPassword: password }); + changedPasswordToken = changePasswordResponse.token; + changedPasswordNewToken = changePasswordResponse.newToken; + } return { ...loginResult, - token: refreshedTokens.token, - newToken: refreshedTokens.newToken, + token: changedPasswordToken ?? refreshedTokens.token, + newToken: changedPasswordNewToken ?? refreshedTokens.newToken, }; } @@ -175,11 +182,11 @@ class AuthService { } const changePasswordResult = await this.sdk.users.changePasswordLegacy({ - currentEncryptedPassword: encCurrentPass, - newEncryptedSalt: encryptedNewSalt, encryptedMnemonic, - newEncryptedPassword: encNewPass, encryptedPrivateKey: privateKeyFinalValue, + newEncryptedSalt: encryptedNewSalt, + newEncryptedPassword: encNewPass, + currentEncryptedPassword: encCurrentPass, }); return {