diff --git a/CHANGELOG_CHERRYPICK.md b/CHANGELOG_CHERRYPICK.md index ffaef2c9ac..d6a3a29abf 100644 --- a/CHANGELOG_CHERRYPICK.md +++ b/CHANGELOG_CHERRYPICK.md @@ -33,7 +33,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGE - 활성화하면 글자를 표시하기 위한 여유 공간이 좁을 때 디자인이 상대적으로 어색하게 보일 수 있으며, 실험실 기능이므로 이 기능이 변경하는 부분을 확실히 알고 있는 사용자만 활성화할 것을 권장합니다. - 비밀번호 해싱 알고리즘이 `bcrypt`에서 `argon2`로 변경됨 - 이 변경으로 이후에 비밀번호를 변경하거나 신규로 가입한 사용자는 `argon2`를 사용하여 비밀번호 해시가 생성됩니다. - - 이전에 가입한 사용자는 비밀번호를 변경하지 않아도 `bcypt`를 사용할 수 있으며 여전히 기존과 동일하게 호환됩니다. + - 이전에 가입한 사용자는 로그인 시 자동으로 `bcypt`에서 `argon2`로 해시가 변경됩니다. ### General - Feat: 위젯 영역을 숨길 수 있음 @@ -89,7 +89,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGE - Fix: 노트 본문의 사용자 멘션 영역을 클릭하면 노트 상세 페이지가 표시됨 ### Server -- Enhance: 보안 향상을 위해 비밀번호 해싱 알고리즘이 `bcrypt`에서 `argon2`로 변경됨 (kokonect-link/cherrypick#511) +- Enhance: 보안 향상을 위해 비밀번호 해싱 알고리즘이 `bcrypt`에서 `argon2`로 변경됨 (kokonect-link/cherrypick#511), (1673beta/cherrypick#88) - 이제 72 바이트를 초과하는 비밀번호를 사용할 수 있습니다. - 이로써 `Sharkey`, `FireFish`, `IceShrimp` 등의 클라이언트에서 `CherryPick`으로 이전할 때 암호 호환성이 보장됩니다. - Fix: 이모지를 등록하거나 가져오려고 할 때 오류가 발생할 수 있음 (kokonect-link/cherrypick#487), (kokonect-link/cherrypick#508) diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts index 7de67dade1..fb1f1e8f2b 100644 --- a/packages/backend/src/core/CreateSystemUserService.ts +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -5,8 +5,6 @@ import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; -import * as argon2 from 'argon2'; -//import bcrypt from 'bcryptjs'; import { IsNull, DataSource } from 'typeorm'; import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; import { MiUser } from '@/models/User.js'; @@ -17,6 +15,7 @@ import { MiUsedUsername } from '@/models/UsedUsername.js'; import { DI } from '@/di-symbols.js'; import generateNativeUserToken from '@/misc/generate-native-user-token.js'; import { bindThis } from '@/decorators.js'; +import { hashPassword } from '@/misc/password.js'; @Injectable() export class CreateSystemUserService { @@ -34,7 +33,7 @@ export class CreateSystemUserService { // Generate hash of password //const salt = await bcrypt.genSalt(8); - const hash = await argon2.hash(password); + const hash = await hashPassword(password); // Generate secret const secret = generateNativeUserToken(); diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 5374ec09f6..f42254e338 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -5,8 +5,6 @@ import { generateKeyPair } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; -//import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; import { DataSource, IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; @@ -22,6 +20,7 @@ import { bindThis } from '@/decorators.js'; import UsersChart from '@/core/chart/charts/users.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserService } from '@/core/UserService.js'; +import { hashPassword } from '@/misc/password.js'; @Injectable() export class SignupService { @@ -70,8 +69,7 @@ export class SignupService { } // Generate hash of password - //const salt = await bcrypt.genSalt(8); - hash = await argon2.hash(password); + hash = await hashPassword(password); } // Generate secret diff --git a/packages/backend/src/misc/password.ts b/packages/backend/src/misc/password.ts new file mode 100644 index 0000000000..be40638c3f --- /dev/null +++ b/packages/backend/src/misc/password.ts @@ -0,0 +1,20 @@ +import { randomBytes } from 'crypto'; +import bcrypt from 'bcryptjs'; +import * as argon2 from 'argon2'; + +export async function hashPassword(password: string): Promise { + const salt = randomBytes(32); + return argon2.hash(password, { salt: salt, type: argon2.argon2id }); +} + +export async function comparePassword(password: string, hash: string): Promise { + if (isOldAlgorithm(hash)) { + return bcrypt.compare(password, hash); + } + + return argon2.verify(hash, password); +} + +export function isOldAlgorithm(hash: string): boolean { + return hash.startsWith('$2'); +} diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 1554510697..e2cd5d7c14 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -4,10 +4,9 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; import * as OTPAuth from 'otpauth'; import { IsNull } from 'typeorm'; +import { comparePassword, hashPassword, isOldAlgorithm } from '@/misc/password.js'; import { DI } from '@/di-symbols.js'; import type { SigninsRepository, @@ -124,7 +123,12 @@ export class SigninApiService { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); // Compare password - const same = await argon2.verify(profile.password!, password) || bcrypt.compareSync(password, profile.password!); + const same = await comparePassword(password, profile.password!); + + if (same && isOldAlgorithm(profile.password!)) { + profile.password = await hashPassword(password); + await this.userProfilesRepository.save(profile); + } const fail = async (status?: number, failure?: { id: string }) => { // Append signin history @@ -141,12 +145,6 @@ export class SigninApiService { if (!profile.twoFactorEnabled) { if (same) { - if (profile.password!.startsWith('$2')) { - const newHash = await argon2.hash(password); - this.userProfilesRepository.update(user.id, { - password: newHash, - }); - } return this.signinService.signin(request, reply, user); } else { return await fail(403, { @@ -163,12 +161,6 @@ export class SigninApiService { } try { - if (profile.password!.startsWith('$2')) { - const newHash = await argon2.hash(password); - this.userProfilesRepository.update(user.id, { - password: newHash, - }); - } await this.userAuthService.twoFactorAuthenticate(profile, token); } catch (e) { return await fail(403, { diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index f99e2761a3..16bf16a75f 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -4,9 +4,8 @@ */ import { Inject, Injectable } from '@nestjs/common'; -//import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; import { IsNull } from 'typeorm'; +import { hashPassword } from '@/misc/password.js'; import { DI } from '@/di-symbols.js'; import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -180,8 +179,7 @@ export class SignupApiService { const code = secureRndstr(16, { chars: L_CHARS }); // Generate hash of password - //const salt = await bcrypt.genSalt(8); - const hash = await argon2.hash(password); + const hash = await hashPassword(password); const pendingUser = await this.userPendingsRepository.insertOne({ id: this.idService.gen(), diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index 828dbae712..70d3e372a3 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -4,8 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -//import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; +import { hashPassword } from '@/misc/password.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; @@ -63,10 +62,10 @@ export default class extends Endpoint { // eslint- throw new Error('cannot reset password of root'); } - const passwd = secureRndstr(8); + const passwd = secureRndstr(16); // Generate hash of password - const hash = await argon2.hash(passwd); + const hash = await hashPassword(passwd); await this.userProfilesRepository.update({ userId: user.id, diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index d87df588bf..77c08bf855 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -3,8 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -//import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -14,6 +12,7 @@ import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/model import { WebAuthnService } from '@/core/WebAuthnService.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import { comparePassword } from '@/misc/password.js'; export const meta = { requireCredential: true, @@ -86,7 +85,7 @@ export default class extends Endpoint { } } - const passwordMatched = await argon2.verify(profile.password ?? '', ps.password); + const passwordMatched = await comparePassword(ps.password, profile.password ?? ''); if (!passwordMatched) { throw new ApiError(meta.errors.incorrectPassword); } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index 5621ff3dc1..a91e1fcab4 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -3,8 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -//import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UserProfilesRepository } from '@/models/_.js'; @@ -12,6 +10,7 @@ import { DI } from '@/di-symbols.js'; import { WebAuthnService } from '@/core/WebAuthnService.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import { comparePassword } from '@/misc/password.js'; export const meta = { requireCredential: true, @@ -217,7 +216,7 @@ export default class extends Endpoint { } } - const passwordMatched = await argon2.verify(profile.password ?? '', ps.password); + const passwordMatched = await comparePassword(ps.password, profile.password ?? ''); if (!passwordMatched) { throw new ApiError(meta.errors.incorrectPassword); } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index 7283159f87..a169d8b2eb 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -3,8 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -//import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; import * as OTPAuth from 'otpauth'; import * as QRCode from 'qrcode'; import { Inject, Injectable } from '@nestjs/common'; @@ -14,6 +12,7 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import { comparePassword } from '@/misc/password.js'; export const meta = { requireCredential: true, @@ -78,7 +77,7 @@ export default class extends Endpoint { // eslint- } } - const passwordMatched = await argon2.verify(profile.password ?? '', ps.password); + const passwordMatched = await comparePassword(ps.password, profile.password ?? ''); if (!passwordMatched) { throw new ApiError(meta.errors.incorrectPassword); } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index 982cb7aee5..b134801f1f 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -3,8 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -//import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js'; @@ -13,6 +11,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import { comparePassword } from '@/misc/password.js'; export const meta = { requireCredential: true, @@ -67,7 +66,7 @@ export default class extends Endpoint { // eslint- } } - const passwordMatched = await argon2.verify(profile.password ?? '', ps.password); + const passwordMatched = await comparePassword(ps.password, profile.password ?? ''); if (!passwordMatched) { throw new ApiError(meta.errors.incorrectPassword); } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index 8da331505b..0399698127 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -3,8 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -//import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -13,6 +11,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import { comparePassword } from '@/misc/password.js'; export const meta = { requireCredential: true, @@ -63,7 +62,7 @@ export default class extends Endpoint { // eslint- } } - const passwordMatched = await argon2.verify(profile.password ?? '', ps.password); + const passwordMatched = await comparePassword(ps.password, profile.password ?? ''); if (!passwordMatched) { throw new ApiError(meta.errors.incorrectPassword); } diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts index 6aedde717c..9230e97077 100644 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ b/packages/backend/src/server/api/endpoints/i/change-password.ts @@ -3,13 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -//import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import { hashPassword, comparePassword } from '@/misc/password.js'; export const meta = { requireCredential: true, @@ -51,15 +50,14 @@ export default class extends Endpoint { // eslint- } } - const passwordMatched = await argon2.verify(profile.password!, ps.currentPassword); + const passwordMatched = await comparePassword(ps.currentPassword, profile.password!); if (!passwordMatched) { throw new Error('incorrect password'); } // Generate hash of password - //const salt = await bcrypt.genSalt(8); - const hash = await argon2.hash(ps.newPassword); + const hash = await hashPassword(ps.newPassword); await this.userProfilesRepository.update(me.id, { password: hash, diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index af4d601ad6..4d0204aa92 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -3,14 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -//import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DeleteAccountService } from '@/core/DeleteAccountService.js'; import { DI } from '@/di-symbols.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import { comparePassword } from '@/misc/password.js'; export const meta = { requireCredential: true, @@ -60,7 +59,7 @@ export default class extends Endpoint { // eslint- return; } - const passwordMatched = await argon2.verify(profile.password!, ps.password); + const passwordMatched = await comparePassword(ps.password, profile.password!); if (!passwordMatched) { throw new Error('incorrect password'); } diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index e1cdfdc185..ddd04cf1d3 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -3,14 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -//import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import generateUserToken from '@/misc/generate-native-user-token.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { comparePassword } from '@/misc/password.js'; export const meta = { requireCredential: true, @@ -44,7 +43,7 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); // Compare password - const same = await argon2.verify(profile.password!, ps.password); + const same = await comparePassword(ps.password, profile.password!); if (!same) { throw new Error('incorrect password'); diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index 0be8bfb695..718b513eab 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -5,8 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; -//import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; +import { comparePassword } from '@/misc/password.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { MiMeta, UserProfilesRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -97,7 +96,7 @@ export default class extends Endpoint { // eslint- } } - const passwordMatched = await argon2.verify(profile.password!, ps.password); + const passwordMatched = await comparePassword(ps.password, profile.password!); if (!passwordMatched) { throw new ApiError(meta.errors.incorrectPassword); } diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index 1639b57bc5..56503913ee 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -3,13 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -//import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; import { Inject, Injectable } from '@nestjs/common'; import type { UserProfilesRepository, PasswordResetRequestsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; +import { hashPassword } from '@/misc/password.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; export const meta = { tags: ['reset password'], @@ -54,8 +54,8 @@ export default class extends Endpoint { // eslint- } // Generate hash of password - //const salt = await bcrypt.genSalt(8); - const hash = await argon2.hash(ps.password); + const passwd = secureRndstr(16); + const hash = await hashPassword(passwd); await this.userProfilesRepository.update(req.userId, { password: hash,