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:
parent
6a2495aae7
commit
40efc34b68
7 changed files with 117 additions and 12 deletions
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 (
|
||||
|
|
31
packages/core/src/workers/password-encryption-worker.ts
Normal file
31
packages/core/src/workers/password-encryption-worker.ts
Normal 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;
|
21
packages/core/src/workers/tasks/argon2i.ts
Normal file
21
packages/core/src/workers/tasks/argon2i.ts
Normal 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',
|
||||
});
|
||||
}
|
|
@ -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
20
pnpm-lock.yaml
generated
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue