From ca5db1df33c35b3ceb3a565df1370fe78adaff7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=91=9E=E5=B8=8C?= Date: Fri, 4 Aug 2023 16:06:45 +0900 Subject: [PATCH] Use argon2ian instead of npm:argon2-browser --- web/argon2.ts | 109 ++++++++++++++++++++++++++++++++++++++++ web/routes/api/join.ts | 11 ++-- web/routes/api/login.ts | 16 ++---- web/util.ts | 29 ----------- 4 files changed, 115 insertions(+), 50 deletions(-) create mode 100644 web/argon2.ts diff --git a/web/argon2.ts b/web/argon2.ts new file mode 100644 index 0000000..583f8c4 --- /dev/null +++ b/web/argon2.ts @@ -0,0 +1,109 @@ +import { + type ArgonOptions, + ArgonWorker, +} from "https://deno.land/x/argon2ian@2.0.1/src/async.ts"; +import { + decode as decodeBase64, + encode as encodeBase64, +} from "std/encoding/base64.ts"; + +const worker = new ArgonWorker(); +const encoder = new TextEncoder(); + +const variants = ["argon2d", "argon2i", "argon2id"] as const; +const variantsMap = new Map(variants.map((v, i) => [v, i as 0 | 1 | 2])); + +const defaultOptions = { + variant: 2, // argon2id + m: 1 << 16, + t: 3, + p: 1, + // https://github.com/valpackett/argon2ian/blob/trunk/src/argon2.ts#L35 +} satisfies ArgonOptions; + +const generateSalt = (size = 16) => { + const buf = new Uint8Array(size); + crypto.getRandomValues(buf); + return buf; +}; + +const encodeHashString = (params: VerifyParams) => { + const { hash, salt, options } = params; + if (!options.variant || !options.m || !options.t || !options.p) { + throw new Error("Required options not provided."); + } + + const sections = [ + variants[options.variant], + "v=19", // argon2ian2 uses argon2 version 0x13 + `m=${options.m},t=${options.t},p=${options.p}`, + encodeBase64(salt), + encodeBase64(hash), + ] as const; + + return `$${sections.join("$")}`; +}; + +const decodeHashString = (encoded: string): VerifyParams => { + const [_0, variantStr, _v, paramsStr, salt, hash] = encoded.split("$"); + const params = paramsStr.split(",").reduce((acc, curr) => { + const [k, v] = curr.split("="); + acc[k] = parseInt(v, 10); + return acc; + }, {} as { [k: string]: number }); + + return { + hash: decodeBase64(hash), + salt: decodeBase64(salt), + options: { + variant: variantsMap.get(variantStr as (typeof variants)[number]), + ...params, + }, + }; +}; + +interface HashParams { + salt?: string | Uint8Array; + options?: ArgonOptions; +} + +export const hash = async ( + password: string | Uint8Array, + params: HashParams = {}, +) => { + await worker.ready; + + const options = { ...defaultOptions, ...params.options }; + + const salt = params.salt != null + ? typeof params.salt === "string" + ? encoder.encode(params.salt) + : params.salt + : generateSalt(); + + password = typeof password === "string" ? encoder.encode(password) : password; + + const hash = await worker.hash(password, salt, options); + + return encodeHashString({ hash, salt, options }); +}; + +interface VerifyParams { + hash: Uint8Array; + salt: Uint8Array; + options: ArgonOptions; +} + +export const verify = async ( + password: string | Uint8Array, + params: VerifyParams | string, +) => { + await worker.ready; + + if (typeof params === "string") params = decodeHashString(params); + + password = typeof password === "string" ? encoder.encode(password) : password; + + return params.hash === + (await worker.hash(password, params.salt, params.options)); +}; diff --git a/web/routes/api/join.ts b/web/routes/api/join.ts index 05a3c73..6d17182 100644 --- a/web/routes/api/join.ts +++ b/web/routes/api/join.ts @@ -2,7 +2,8 @@ import { Handlers, Status } from "fresh/server.ts"; import type { WithSession } from "fresh-session"; import { prisma } from "~/main.ts"; -import { argon2, redirect } from "~/util.ts"; +import { redirect } from "~/util.ts"; +import { hash } from "~/argon2.ts"; export const handler: Handlers = { async POST(req, ctx) { @@ -19,13 +20,7 @@ export const handler: Handlers = { const user = await prisma.user.create({ data: { email, - password: ( - await argon2.hash({ - pass: password, - salt: crypto.getRandomValues(new Uint8Array(16)), - type: argon2.ArgonType.Argon2id, - }) - ).encoded, + password: await hash(password), }, }); diff --git a/web/routes/api/login.ts b/web/routes/api/login.ts index 514719b..44e1f1f 100644 --- a/web/routes/api/login.ts +++ b/web/routes/api/login.ts @@ -1,9 +1,9 @@ import { Handlers, Status } from "fresh/server.ts"; import type { WithSession } from "fresh-session"; -import { decode } from "std/encoding/base64.ts"; import { prisma } from "~/main.ts"; -import { argon2, redirect } from "~/util.ts"; +import { redirect } from "~/util.ts"; +import { verify } from "~/argon2.ts"; export const handler: Handlers = { async POST(req, ctx) { @@ -18,17 +18,7 @@ export const handler: Handlers = { } const user = await prisma.user.findUnique({ where: { email } }); - if ( - !user || - user.password !== - ( - await argon2.hash({ - pass: password, - salt: decode(user.password.split("$")[4]), - type: argon2.ArgonType.Argon2id, - }) - ).encoded - ) { + if (!user || (await verify(password, user.password))) { return new Response("Invalid email/password", { status: Status.Unauthorized, }); diff --git a/web/util.ts b/web/util.ts index 560a500..41aa64a 100644 --- a/web/util.ts +++ b/web/util.ts @@ -31,32 +31,3 @@ export const checkPermission = async ( if (entries.find((e) => e.userId === user.id)) return true; return false; }; - -const globalThisShim = globalThis as { - process?: { versions?: { node?: unknown } }; -}; -const needPatch = globalThisShim.process !== undefined && - globalThisShim.process.versions !== undefined && - globalThisShim.process.versions.node !== undefined; -if (needPatch) { - Object.assign(globalThis, { - process: { - ...globalThisShim.process, - versions: { ...globalThisShim.process!.versions, node: undefined }, - }, - }); -} -import _argon2 from "https://esm.sh/argon2-browser@1.18.0/dist/argon2-bundled.min.js"; -export import argon2 = _argon2; -await argon2.hash({ pass: new Uint8Array(0), salt: new Uint8Array(8) }); -if (needPatch) { - Object.assign(globalThis, { - process: { - ...globalThisShim.process, - versions: { - ...globalThisShim.process!.versions, - node: globalThisShim.process!.versions!.node, - }, - }, - }); -}