server: refactor password hashing & comparison to module

For easier replacement should the hash algorithm ever be changed.
This commit is contained in:
Johann150 2022-12-25 15:49:06 +01:00
parent c2372315f7
commit 114d416de0
Signed by untrusted user: Johann150
GPG key ID: 9EE6577A2A06F8F1
16 changed files with 46 additions and 84 deletions

View file

@ -0,0 +1,10 @@
import bcrypt from 'bcryptjs';
export async function hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt(8);
return await bcrypt.hash(password, salt);
}
export async function comparePassword(password: string, hash: string): Promise<boolean> {
return await bcrypt.compare(password, hash);
}

View file

@ -1,11 +1,11 @@
import { generateKeyPair } from 'node:crypto'; import { generateKeyPair } from 'node:crypto';
import bcrypt from 'bcryptjs';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
import { Users, UsedUsernames } from '@/models/index.js'; import { Users, UsedUsernames } from '@/models/index.js';
import { UserProfile } from '@/models/entities/user-profile.js'; import { UserProfile } from '@/models/entities/user-profile.js';
import { genId } from '@/misc/gen-id.js'; import { genId } from '@/misc/gen-id.js';
import { toPunyNullable } from '@/misc/convert-host.js'; import { toPunyNullable } from '@/misc/convert-host.js';
import { hashPassword } from '@/misc/password.js';
import { UserKeypair } from '@/models/entities/user-keypair.js'; import { UserKeypair } from '@/models/entities/user-keypair.js';
import { usersChart } from '@/services/chart/index.js'; import { usersChart } from '@/services/chart/index.js';
import { UsedUsername } from '@/models/entities/used-username.js'; import { UsedUsername } from '@/models/entities/used-username.js';
@ -33,9 +33,7 @@ export async function signup(opts: {
throw new ApiError('INVALID_PASSWORD'); throw new ApiError('INVALID_PASSWORD');
} }
// Generate hash of password hash = await hashPassword(password);
const salt = await bcrypt.genSalt(8);
hash = await bcrypt.hash(password, salt);
} }
// Generate secret // Generate secret

View file

@ -1,4 +1,4 @@
import bcrypt from 'bcryptjs'; import { hashPassword } from '@/misc/password.js';
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
import { Users, UserProfiles } from '@/models/index.js'; import { Users, UserProfiles } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
@ -17,8 +17,6 @@ export const meta = {
password: { password: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
minLength: 8,
maxLength: 8,
}, },
}, },
}, },
@ -46,18 +44,15 @@ export default define(meta, paramDef, async (ps) => {
throw new ApiError('IS_ADMIN'); throw new ApiError('IS_ADMIN');
} }
const passwd = secureRndstr(8, true); const password = secureRndstr(8, true);
// Generate hash of password
const hash = bcrypt.hashSync(passwd);
await UserProfiles.update({ await UserProfiles.update({
userId: user.id, userId: user.id,
}, { }, {
password: hash, password: await hashPassword(password),
}); });
return { return {
password: passwd, password,
}; };
}); });

View file

@ -1,7 +1,7 @@
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import bcrypt from 'bcryptjs';
import * as cbor from 'cbor'; import * as cbor from 'cbor';
import { MINUTE } from '@/const.js'; import { MINUTE } from '@/const.js';
import { comparePassword } from '@/misc/password.js';
import { import {
UserProfiles, UserProfiles,
UserSecurityKeys, UserSecurityKeys,
@ -41,10 +41,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password if (!(await comparePassword(ps.password, profile.password!))) {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new ApiError('ACCESS_DENIED'); throw new ApiError('ACCESS_DENIED');
} }

View file

@ -1,8 +1,8 @@
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import bcrypt from 'bcryptjs';
import { UserProfiles, AttestationChallenges } from '@/models/index.js'; import { UserProfiles, AttestationChallenges } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js'; import { genId } from '@/misc/gen-id.js';
import { comparePassword } from '@/misc/password.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import define from '../../../define.js'; import define from '../../../define.js';
import { hash } from '../../../2fa.js'; import { hash } from '../../../2fa.js';
@ -29,10 +29,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password if (!(await comparePassword(ps.password, profile.password!))) {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new ApiError('ACCESS_DENIED'); throw new ApiError('ACCESS_DENIED');
} }

View file

@ -1,7 +1,7 @@
import bcrypt from 'bcryptjs';
import * as speakeasy from 'speakeasy'; import * as speakeasy from 'speakeasy';
import * as QRCode from 'qrcode'; import * as QRCode from 'qrcode';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { comparePassword } from '@/misc/password.js';
import { UserProfiles } from '@/models/index.js'; import { UserProfiles } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import define from '../../../define.js'; import define from '../../../define.js';
@ -26,10 +26,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password if (!(await comparePassword(ps.password, profile.password!))) {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new ApiError('ACCESS_DENIED'); throw new ApiError('ACCESS_DENIED');
} }

View file

@ -1,4 +1,4 @@
import bcrypt from 'bcryptjs'; import { comparePassword } from '@/misc/password.js';
import { UserProfiles, UserSecurityKeys, Users } from '@/models/index.js'; import { UserProfiles, UserSecurityKeys, Users } from '@/models/index.js';
import { publishMainStream } from '@/services/stream.js'; import { publishMainStream } from '@/services/stream.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
@ -25,10 +25,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password if (!(await comparePassword(ps.password, profile.password!))) {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new ApiError('ACCESS_DENIED'); throw new ApiError('ACCESS_DENIED');
} }

View file

@ -1,4 +1,4 @@
import bcrypt from 'bcryptjs'; import { comparePassword } from '@/misc/password.js';
import { UserProfiles } from '@/models/index.js'; import { UserProfiles } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import define from '../../../define.js'; import define from '../../../define.js';
@ -23,10 +23,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password if (!(await comparePassword(ps.password, profile.password!))) {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new ApiError('ACCESS_DENIED'); throw new ApiError('ACCESS_DENIED');
} }

View file

@ -1,4 +1,4 @@
import bcrypt from 'bcryptjs'; import { comparePassword, hashPassword } from '@/misc/password.js';
import { UserProfiles } from '@/models/index.js'; import { UserProfiles } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import define from '../../define.js'; import define from '../../define.js';
@ -24,18 +24,11 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password if (!(await comparePassword(ps.currentPassword, profile.password!))) {
const same = await bcrypt.compare(ps.currentPassword, profile.password!);
if (!same) {
throw new ApiError('ACCESS_DENIED'); throw new ApiError('ACCESS_DENIED');
} }
// Generate hash of password
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(ps.newPassword, salt);
await UserProfiles.update(user.id, { await UserProfiles.update(user.id, {
password: hash, password: await hashPassword(ps.newPassword),
}); });
}); });

View file

@ -1,4 +1,4 @@
import bcrypt from 'bcryptjs'; import { comparePassword } from '@/misc/password.js';
import { UserProfiles, Users } from '@/models/index.js'; import { UserProfiles, Users } from '@/models/index.js';
import { deleteAccount } from '@/services/delete-account.js'; import { deleteAccount } from '@/services/delete-account.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
@ -28,10 +28,7 @@ export default define(meta, paramDef, async (ps, user) => {
return; return;
} }
// Compare password if (!(await comparePassword(ps.password, profile.password!))) {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new ApiError('ACCESS_DENIED'); throw new ApiError('ACCESS_DENIED');
} }

View file

@ -1,4 +1,4 @@
import bcrypt from 'bcryptjs'; import { comparePassword } from '@/misc/password.js';
import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.js'; import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.js';
import { Users, UserProfiles } from '@/models/index.js'; import { Users, UserProfiles } from '@/models/index.js';
import generateUserToken from '../../common/generate-native-user-token.js'; import generateUserToken from '../../common/generate-native-user-token.js';
@ -28,10 +28,7 @@ export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password if (!(await comparePassword(ps.password, profile.password!))) {
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new ApiError('ACCESS_DENIED'); throw new ApiError('ACCESS_DENIED');
} }

View file

@ -1,6 +1,6 @@
import bcrypt from 'bcryptjs';
import { publishMainStream } from '@/services/stream.js'; import { publishMainStream } from '@/services/stream.js';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { comparePassword } from '@/misc/password.js';
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
import { Users, UserProfiles } from '@/models/index.js'; import { Users, UserProfiles } from '@/models/index.js';
import { sendEmail } from '@/services/send-email.js'; import { sendEmail } from '@/services/send-email.js';
@ -37,10 +37,9 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password if (!(await comparePassword(ps.password, profile.password!))) {
const same = await bcrypt.compare(ps.password, profile.password!); throw new ApiError('ACCESS_DENIED');
}
if (!same) throw new ApiError('ACCESS_DENIED');
if (ps.email != null) { if (ps.email != null) {
const available = await validateEmailForAccount(ps.email); const available = await validateEmailForAccount(ps.email);

View file

@ -1,4 +1,4 @@
import bcrypt from 'bcryptjs'; import { hashPassword } from '@/misc/password.js';
import { UserProfiles, PasswordResetRequests } from '@/models/index.js'; import { UserProfiles, PasswordResetRequests } from '@/models/index.js';
import { DAY, MINUTE } from '@/const.js'; import { DAY, MINUTE } from '@/const.js';
import define from '../define.js'; import define from '../define.js';
@ -43,12 +43,8 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError('NO_SUCH_RESET_REQUEST'); throw new ApiError('NO_SUCH_RESET_REQUEST');
} }
// Generate hash of password
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(ps.password, salt);
await UserProfiles.update(req.userId, { await UserProfiles.update(req.userId, {
password: hash, password: await hashPassword(ps.password),
}); });
await PasswordResetRequests.delete(req.id); await PasswordResetRequests.delete(req.id);

View file

@ -1,7 +1,6 @@
import { randomBytes } from 'node:crypto'; import { randomBytes } from 'node:crypto';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import Koa from 'koa'; import Koa from 'koa';
import bcrypt from 'bcryptjs';
import * as speakeasy from 'speakeasy'; import * as speakeasy from 'speakeasy';
import { SECOND, MINUTE, HOUR } from '@/const.js'; import { SECOND, MINUTE, HOUR } from '@/const.js';
import config from '@/config/index.js'; import config from '@/config/index.js';
@ -9,6 +8,7 @@ import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges }
import { ILocalUser } from '@/models/entities/user.js'; import { ILocalUser } from '@/models/entities/user.js';
import { genId } from '@/misc/gen-id.js'; import { genId } from '@/misc/gen-id.js';
import { getIpHash } from '@/misc/get-ip-hash.js'; import { getIpHash } from '@/misc/get-ip-hash.js';
import { comparePassword } from '@/misc/password.js';
import signin from '../common/signin.js'; import signin from '../common/signin.js';
import { verifyLogin, hash } from '../2fa.js'; import { verifyLogin, hash } from '../2fa.js';
import { limiter } from '../limiter.js'; import { limiter } from '../limiter.js';
@ -67,7 +67,7 @@ export default async (ctx: Koa.Context) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password // Compare password
const same = await bcrypt.compare(password, profile.password!); const same = await comparePassword(password, profile.password!);
async function fail(): void { async function fail(): void {
// Append signin history // Append signin history

View file

@ -1,7 +1,7 @@
import Koa from 'koa'; import Koa from 'koa';
import bcrypt from 'bcryptjs';
import { fetchMeta } from '@/misc/fetch-meta.js'; import { fetchMeta } from '@/misc/fetch-meta.js';
import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha.js'; import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha.js';
import { hashPassword } from '@/misc/password.js';
import { Users, RegistrationTickets, UserPendings } from '@/models/index.js'; import { Users, RegistrationTickets, UserPendings } from '@/models/index.js';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { sendEmail } from '@/services/send-email.js'; import { sendEmail } from '@/services/send-email.js';
@ -71,17 +71,13 @@ export default async (ctx: Koa.Context) => {
if (instance.emailRequiredForSignup) { if (instance.emailRequiredForSignup) {
const code = secureRndstr(16); const code = secureRndstr(16);
// Generate hash of password
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt);
await UserPendings.insert({ await UserPendings.insert({
id: genId(), id: genId(),
createdAt: new Date(), createdAt: new Date(),
code, code,
email: emailAddress, email: emailAddress,
username, username,
password: hash, password: await hashPassword(password),
}); });
const link = `${config.url}/signup-complete/${code}`; const link = `${config.url}/signup-complete/${code}`;

View file

@ -1,7 +1,7 @@
import bcrypt from 'bcryptjs';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
import { hashPassword } from '@/misc/password.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
import { UserProfile } from '@/models/entities/user-profile.js'; import { UserProfile } from '@/models/entities/user-profile.js';
import { genId } from '@/misc/gen-id.js'; import { genId } from '@/misc/gen-id.js';
@ -11,11 +11,7 @@ import { db } from '@/db/postgre.js';
import generateNativeUserToken from '@/server/api/common/generate-native-user-token.js'; import generateNativeUserToken from '@/server/api/common/generate-native-user-token.js';
export async function createSystemUser(username: string): Promise<User> { export async function createSystemUser(username: string): Promise<User> {
const password = uuid(); const password = await hashPassword(uuid());
// Generate hash of password
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt);
// Generate secret // Generate secret
const secret = generateNativeUserToken(); const secret = generateNativeUserToken();
@ -55,7 +51,7 @@ export async function createSystemUser(username: string): Promise<User> {
await transactionalEntityManager.insert(UserProfile, { await transactionalEntityManager.insert(UserProfile, {
userId: account.id, userId: account.id,
autoAcceptFollowed: false, autoAcceptFollowed: false,
password: hash, password,
}); });
await transactionalEntityManager.insert(UsedUsername, { await transactionalEntityManager.insert(UsedUsername, {