From 5be627b869ad46435abbfd18700dc79c1f918c3f Mon Sep 17 00:00:00 2001
From: ThatOneCalculator <kainoa@t1c.dev>
Date: Thu, 30 Mar 2023 19:09:44 -0700
Subject: [PATCH] feat: :lock: add argon2 support

Passwords will be automatically re-hashed on sign-in. All new password hashes will be argon2 by default.  This uses argon2id and is not configurable. In the very unlikely case someone has more specific needs, a fork is recommended.  ChangeLog: Added  Co-authored-by: Chloe Kudryavtsev <code@toast.bunkerlabs.net>

Breaks Calckey -> Misskey migration, but fixes Foundkey -> Calckey migration
---
 packages/backend/src/misc/password.ts         | 20 +++++++++++++++++++
 .../backend/src/server/api/private/signin.ts  |  8 +++++++-
 2 files changed, 27 insertions(+), 1 deletion(-)
 create mode 100644 packages/backend/src/misc/password.ts

diff --git a/packages/backend/src/misc/password.ts b/packages/backend/src/misc/password.ts
new file mode 100644
index 000000000..c63f89f5c
--- /dev/null
+++ b/packages/backend/src/misc/password.ts
@@ -0,0 +1,20 @@
+import bcrypt from "bcryptjs";
+import * as argon2 from "argon2";
+
+export async function hashPassword(password: string): Promise<string> {
+	return argon2.hash(password);
+}
+
+export async function comparePassword(
+	password: string,
+	hash: string,
+): Promise<boolean> {
+	if (isOldAlgorithm(hash)) return bcrypt.compare(password, hash);
+
+	return argon2.verify(hash, password);
+}
+
+export function isOldAlgorithm(hash: string): boolean {
+	// bcrypt hashes start with $2[ab]$
+	return hash.startsWith("$2");
+}
diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts
index b06f47ed4..c8cd5fb2c 100644
--- a/packages/backend/src/server/api/private/signin.ts
+++ b/packages/backend/src/server/api/private/signin.ts
@@ -12,6 +12,7 @@ import {
 } from "@/models/index.js";
 import type { ILocalUser } from "@/models/entities/user.js";
 import { genId } from "@/misc/gen-id.js";
+import { comparePassword, hashPassword, isOldAlgorithm } from '@/misc/password.js';
 import { verifyLogin, hash } from "../2fa.js";
 import { randomBytes } from "node:crypto";
 import { IsNull } from "typeorm";
@@ -88,7 +89,12 @@ export default async (ctx: Koa.Context) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
 	// Compare password
-	const same = await bcrypt.compare(password, profile.password!);
+	const same = await comparePassword(password, profile.password!);
+
+	if (same && isOldAlgorithm(profile.password!)) {
+		profile.password = await hashPassword(password);
+		await UserProfiles.save(profile);
+	}
 
 	async function fail(status?: number, failure?: { id: string }) {
 		// Append signin history