0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

refactor(core): optimize password encryption process (#7091)

* refactor(core): add password encryption to worker pool

add password encryption to worker pool

* refactor(core): update password encryption method

update the aragon2i password encryption method

* chore: update comments

update comments

* chore(core): fix typo clean up code format

fix typo and clean up code format

* refactor(core): refactor path build logic

refactor path build logic

* refactor(core): replace piscina with tinypool

replace piscina with tinypool

* refactor(core): refactor worker

refactor worker script path build logic
This commit is contained in:
simeng-li 2025-03-13 11:18:35 +08:00 committed by GitHub
parent 6a2495aae7
commit 40efc34b68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 117 additions and 12 deletions

View file

@ -87,6 +87,7 @@
"p-map": "^7.0.2",
"p-retry": "^6.0.0",
"pg-protocol": "^1.6.0",
"pkg-dir": "^8.0.0",
"pluralize": "^8.0.0",
"qrcode": "^1.5.3",
"raw-body": "^3.0.0",
@ -96,6 +97,7 @@
"semver": "^7.3.8",
"snake-case": "^4.0.0",
"snakecase-keys": "^8.0.1",
"tinypool": "^1.0.2",
"zod": "^3.23.8"
},
"devDependencies": {
@ -154,7 +156,8 @@
"overrides": [
{
"files": [
"*.d.ts"
"*.d.ts",
"*.config.ts"
],
"rules": {
"import/no-unused-modules": "off"

View file

@ -1,6 +1,8 @@
import { UsersPasswordEncryptionMethod } from '@logto/schemas';
import RequestError from '../errors/RequestError/index.js';
import { executeLegacyHash, parseLegacyPassword } from './password.js';
import { encryptPassword, executeLegacyHash, parseLegacyPassword } from './password.js';
describe('parseLegacyPassword', () => {
it('should parse valid legacy password expression', () => {
@ -104,3 +106,24 @@ describe('executeLegacyHash', () => {
expect(result).toBe(parsedExpression.encryptedPassword);
});
});
describe('encryptPassword', () => {
const unsupportedEncryptionMethod = Object.values(UsersPasswordEncryptionMethod).filter(
(method) => method !== UsersPasswordEncryptionMethod.Argon2i
);
it.each(unsupportedEncryptionMethod)(
'should throw error for unsupported method %s',
async (method) => {
await expect(encryptPassword('password', method)).rejects.toThrow(
new RequestError({ code: 'password.unsupported_encryption_method', method })
);
}
);
it('should encrypt password with Argon2i', async () => {
const password = 'password';
const result = await encryptPassword(password, UsersPasswordEncryptionMethod.Argon2i);
expect(result).not.toBe(password);
});
});

View file

@ -3,12 +3,15 @@ import crypto from 'node:crypto';
import { type PasswordPolicyChecker } from '@logto/core-kit';
import { type User, UsersPasswordEncryptionMethod } from '@logto/schemas';
import { condObject } from '@silverhand/essentials';
import { argon2i } from 'hash-wasm';
import { z } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import assertThat from '#src/utils/assert-that.js';
import { EnvSet } from '../env-set/index.js';
import passwordEncryptionWorker from '../workers/password-encryption-worker.js';
import argon2iEncrypt from '../workers/tasks/argon2i.js';
import { safeParseJson } from './json.js';
type LegacyPassword = {
@ -122,15 +125,18 @@ export const encryptPassword = async (
new RequestError({ code: 'password.unsupported_encryption_method', method })
);
return argon2i({
password,
salt: crypto.randomBytes(16),
iterations: 256,
parallelism: 1,
memorySize: 4096,
hashLength: 32,
outputType: 'encoded',
});
// Encrypt password with Argon2i encryption method in a separate worker thread
if (EnvSet.values.isDevFeaturesEnabled) {
const result: unknown = await passwordEncryptionWorker.run(password);
if (typeof result !== 'string') {
throw new TypeError('Invalid password encryption worker response');
}
return result;
}
return argon2iEncrypt(password);
};
export const checkPasswordPolicyForUser = async (

View file

@ -0,0 +1,31 @@
import path from 'node:path';
import { packageDirectory } from 'pkg-dir';
import { Tinypool } from 'tinypool';
const rootDirectory = await packageDirectory();
if (!rootDirectory) {
throw new Error('Cannot find the root directory of the package');
}
/**
* User password encryption worker pool.
*
* @remarks
* This worker pool is used to encrypt user password with Argon2i encryption method.
* Since the encryption process is CPU intensive,
* to better protect the main thread I/O performance,
* we use separate thread threads to handle the encryption process.
*/
const passwordEncryptionWorker = new Tinypool({
filename: path.join(rootDirectory, 'build/workers/tasks/argon2i.js'),
maxThreads: 2,
// By default the worker will be terminated immediately after the task is completed.
// Since starting and terminating a worker thread can lead to some performance overhead,
// set the idle timeout to 5 seconds will keep the worker thread alive for concurrent requests.
// See {@link https://piscinajs.dev/api-reference/Instance/#constructor-new-piscinaoptions} for more details
idleTimeout: 5000,
});
export default passwordEncryptionWorker;

View file

@ -0,0 +1,21 @@
import crypto from 'node:crypto';
import { argon2i } from 'hash-wasm';
/**
* Encrypt the password with Argon2i encryption method.
*
* This method follows the recommended configuration settings from the [OWASP Password Storage Cheat Sheet](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Password_Storage_Cheat_Sheet.md?utm_source=chatgpt.com#argon2id),
* balancing CPU and memory usage while providing a high level of security.
*/
export default async function argon2iEncrypt(password: string): Promise<string> {
return argon2i({
password,
salt: crypto.randomBytes(16),
iterations: 8,
parallelism: 1,
memorySize: 8 * 1024,
hashLength: 32,
outputType: 'encoded',
});
}

View file

@ -4,6 +4,7 @@ import { defaultConfig } from '../../tsup.shared.config.js';
export const config = Object.freeze({
...defaultConfig,
entry: ['src/index.ts', 'src/workers/tasks/**/*.ts'],
outDir: 'build',
onSuccess: 'pnpm run copy:apidocs',
} satisfies Options);

20
pnpm-lock.yaml generated
View file

@ -3631,6 +3631,9 @@ importers:
pg-protocol:
specifier: ^1.6.0
version: 1.6.0
pkg-dir:
specifier: ^8.0.0
version: 8.0.0
pluralize:
specifier: ^8.0.0
version: 8.0.0
@ -3658,6 +3661,9 @@ importers:
snakecase-keys:
specifier: ^8.0.1
version: 8.0.1
tinypool:
specifier: ^1.0.2
version: 1.0.2
zod:
specifier: ^3.23.8
version: 3.23.8
@ -9614,6 +9620,10 @@ packages:
resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==}
engines: {node: '>=4.0.0'}
find-up-simple@1.0.1:
resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
engines: {node: '>=18'}
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
@ -12244,6 +12254,10 @@ packages:
resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==}
engines: {node: '>=10'}
pkg-dir@8.0.0:
resolution: {integrity: sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==}
engines: {node: '>=18'}
playwright-core@1.48.1:
resolution: {integrity: sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==}
engines: {node: '>=18'}
@ -20750,6 +20764,8 @@ snapshots:
dependencies:
array-back: 3.1.0
find-up-simple@1.0.1: {}
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
@ -24231,6 +24247,10 @@ snapshots:
dependencies:
find-up: 5.0.0
pkg-dir@8.0.0:
dependencies:
find-up-simple: 1.0.1
playwright-core@1.48.1: {}
playwright@1.48.1: