From 4b1cecf2bd2b60d5b12f7b04993a0598ad314f5c Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Wed, 3 Dec 2025 17:16:14 +0800 Subject: [PATCH] feat(theme): migrate `bcrypt-ts` to `hash-wasm` (#774) --- pnpm-lock.yaml | 23 ++++--- pnpm-workspace.yaml | 2 +- theme/package.json | 2 +- theme/src/client/composables/encrypt.ts | 65 ++++++++----------- .../src/node/config/extendsBundlerOptions.ts | 2 +- theme/src/node/pages/encryptPage.ts | 9 +-- theme/src/node/pages/extendsPage.ts | 10 ++- theme/src/node/pages/pageBulletin.ts | 5 +- theme/src/node/prepare/prepareEncrypt.ts | 26 ++++---- theme/src/node/theme.ts | 2 +- theme/src/node/utils/encrypt.ts | 15 +++-- 11 files changed, 78 insertions(+), 83 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3ba072b..c3f86645 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -209,9 +209,6 @@ catalogs: '@vueuse/integrations': specifier: ^14.1.0 version: 14.1.0 - bcrypt-ts: - specifier: ^7.1.0 - version: 7.1.0 cac: specifier: ^6.7.14 version: 6.7.14 @@ -236,6 +233,9 @@ catalogs: handlebars: specifier: ^4.7.8 version: 4.7.8 + hash-wasm: + specifier: ^4.12.0 + version: 4.12.0 image-size: specifier: ^2.0.2 version: 2.0.2 @@ -863,9 +863,6 @@ importers: '@vueuse/core': specifier: catalog:prod version: 14.1.0(vue@3.5.25(typescript@5.9.3)) - bcrypt-ts: - specifier: catalog:prod - version: 7.1.0 chokidar: specifier: ^5.0.0 version: 5.0.0 @@ -878,6 +875,9 @@ importers: gray-matter: specifier: catalog:prod version: 4.0.3 + hash-wasm: + specifier: catalog:prod + version: 4.12.0 js-yaml: specifier: catalog:prod version: 4.1.1 @@ -3299,10 +3299,6 @@ packages: bcp-47@2.1.0: resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==} - bcrypt-ts@7.1.0: - resolution: {integrity: sha512-t/Dqr9YzYmn/+oPQBgotBPUuezpZD5CPBwapM5Ep1p3zsLmEycMdXOfZpWbztSBWJ41DlB7EluJBUDsAGSiUeQ==} - engines: {node: '>=20'} - birpc@2.6.1: resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==} @@ -4821,6 +4817,9 @@ packages: hash-sum@2.0.0: resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} + hash-wasm@4.12.0: + resolution: {integrity: sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==} + hashery@1.2.0: resolution: {integrity: sha512-43XJKpwle72Ik5Zpam7MuzRWyNdwwdf6XHlh8wCj2PggvWf+v/Dm5B0dxGZOmddidgeO6Ofu9As/o231Ti/9PA==} engines: {node: '>=20'} @@ -10207,8 +10206,6 @@ snapshots: is-alphanumerical: 2.0.1 is-decimal: 2.0.1 - bcrypt-ts@7.1.0: {} - birpc@2.6.1: {} birpc@2.8.0: {} @@ -11983,6 +11980,8 @@ snapshots: hash-sum@2.0.0: {} + hash-wasm@4.12.0: {} + hashery@1.2.0: dependencies: hookified: 1.13.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 30c0d59d..983437c0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -88,7 +88,6 @@ catalogs: '@pengzhanbo/utils': ^2.1.2 '@vueuse/core': ^14.1.0 '@vueuse/integrations': ^14.1.0 - bcrypt-ts: ^7.1.0 cac: ^6.7.14 chart.js: ^4.5.1 chokidar: 5.0.0 @@ -99,6 +98,7 @@ catalogs: focus-trap: ^7.6.6 gray-matter: ^4.0.3 handlebars: ^4.7.8 + hash-wasm: ^4.12.0 image-size: ^2.0.2 js-yaml: ^4.1.1 katex: ^0.16.25 diff --git a/theme/package.json b/theme/package.json index e92abfab..ee4b9820 100644 --- a/theme/package.json +++ b/theme/package.json @@ -129,11 +129,11 @@ "@vuepress/plugin-sitemap": "catalog:vuepress", "@vuepress/plugin-watermark": "catalog:vuepress", "@vueuse/core": "catalog:prod", - "bcrypt-ts": "catalog:prod", "chokidar": "catalog:prod", "dayjs": "catalog:prod", "esbuild": "catalog:prod", "gray-matter": "catalog:prod", + "hash-wasm": "catalog:prod", "js-yaml": "catalog:prod", "katex": "catalog:prod", "local-pkg": "catalog:prod", diff --git a/theme/src/client/composables/encrypt.ts b/theme/src/client/composables/encrypt.ts index e968aaa0..a75c4d58 100644 --- a/theme/src/client/composables/encrypt.ts +++ b/theme/src/client/composables/encrypt.ts @@ -1,7 +1,7 @@ import type { InjectionKey, Ref } from 'vue' import type { EncryptDataRule } from './encrypt-data.js' -import { hasOwn, useSessionStorage } from '@vueuse/core' -import { compare, genSaltSync } from 'bcrypt-ts/browser' +import { computedAsync, useSessionStorage } from '@vueuse/core' +import { bcryptVerify, md5 } from 'hash-wasm' import { computed, inject, provide } from 'vue' import { useRoute } from 'vuepress/client' import { removeLeadingSlash } from 'vuepress/shared' @@ -21,28 +21,14 @@ export const EncryptSymbol: InjectionKey = Symbol( const storage = useSessionStorage('2a0a3d6afb2fdf1f', () => { if (__VUEPRESS_SSR__) { - return { s: ['', ''] as const, g: '', p: {} as Record } + return { g: '', p: [] as string[] } } return { - s: [genSaltSync(10), genSaltSync(10)] as const, - g: '' as string, - p: {} as Record, + g: '', + p: [] as string[], } }) -function mergeHash(hash: string) { - const [left, right] = storage.value.s - return left + hash + right -} - -function splitHash(hash: string) { - const [left, right] = storage.value.s - if (!hash.startsWith(left) || !hash.endsWith(right)) - return '' - - return hash.slice(left.length, hash.length - right.length) -} - const compareCache = new Map() async function compareDecrypt(content: string, hash: string, separator = ':'): Promise { const key = [content, hash].join(separator) @@ -50,7 +36,7 @@ async function compareDecrypt(content: string, hash: string, separator = ':'): P return compareCache.get(key)! try { - const result = await compare(content, hash) + const result = await bcryptVerify({ password: content, hash }) compareCache.set(key, result) return result } @@ -98,14 +84,17 @@ export function setupEncrypt(): void { : false }) - const isGlobalDecrypted = computed(() => { + const isGlobalDecrypted = computedAsync(async () => { + const hash = storage.value.g if (!encrypt.value.global) return true - const hash = splitHash(storage.value.g) - - return !!hash && encrypt.value.admins.includes(hash) - }) + for (const admin of encrypt.value.admins) { + if (hash && hash === await md5(admin)) + return true + } + return false + }, !encrypt.value.global) const hashList = computed(() => { const pagePath = route.path @@ -122,24 +111,27 @@ export function setupEncrypt(): void { return [pageRule, ...rules].filter(Boolean) as EncryptDataRule[] }) - const isPageDecrypted = computed(() => { + const isPageDecrypted = computedAsync(async () => { if (!hasPageEncrypt.value) return true - const hash = splitHash(storage.value.g || '') - if (hash && encrypt.value.admins.includes(hash)) - return true + const hash = storage.value.g + + for (const admin of encrypt.value.admins) { + if (hash && hash === await md5(admin)) + return true + } for (const { key, rules } of hashList.value) { - if (hasOwn(storage.value.p, key)) { - const hash = splitHash(storage.value.p[key]) - if (hash && rules.includes(hash)) + const hash = storage.value.p[key] + for (const rule of rules) { + if (hash && hash === await md5(rule)) return true } } return false - }) + }, !hasPageEncrypt.value) provide(EncryptSymbol, { hasPageEncrypt, @@ -173,7 +165,7 @@ export function useEncryptCompare(): { for (const admin of encrypt.value.admins) { if (await compareDecrypt(password, admin, encrypt.value.separator)) { - storage.value.g = mergeHash(admin) + storage.value.g = await md5(admin) return true } } @@ -195,10 +187,7 @@ export function useEncryptCompare(): { for (const rule of rules) { if (await compareDecrypt(password, rule, encrypt.value.separator)) { decrypted = true - storage.value.p = { - ...storage.value.p, - [key]: mergeHash(rule), - } + storage.value.p[key] = await md5(rule) break } } diff --git a/theme/src/node/config/extendsBundlerOptions.ts b/theme/src/node/config/extendsBundlerOptions.ts index 7307bf60..1e49122a 100644 --- a/theme/src/node/config/extendsBundlerOptions.ts +++ b/theme/src/node/config/extendsBundlerOptions.ts @@ -15,7 +15,7 @@ export function extendsBundlerOptions(bundlerOptions: any, app: App): void { addViteOptimizeDepsInclude( bundlerOptions, app, - ['@vueuse/core', 'bcrypt-ts/browser', '@vuepress/helper/client', '@iconify/vue', '@iconify/vue/offline', '@vuepress/plugin-git/client', '@vuepress/plugin-markdown-chart/client'], + ['@vueuse/core', 'hash-wasm', '@vuepress/helper/client', '@iconify/vue', '@iconify/vue/offline', '@vuepress/plugin-git/client', '@vuepress/plugin-markdown-chart/client'], ) addViteOptimizeDepsExclude(bundlerOptions, app, '@theme') diff --git a/theme/src/node/pages/encryptPage.ts b/theme/src/node/pages/encryptPage.ts index ffdb8b1c..a4cf6927 100644 --- a/theme/src/node/pages/encryptPage.ts +++ b/theme/src/node/pages/encryptPage.ts @@ -1,14 +1,15 @@ import type { Page } from 'vuepress/core' import type { ThemePageData } from '../../shared/index.js' import { toArray } from '@pengzhanbo/utils' +import pMap from 'p-map' import { genEncrypt } from '../utils/index.js' -export function encryptPage( +export async function encryptPage( page: Page, -): void { - const password = toArray(page.frontmatter.password) +): Promise { + const password = toArray(page.frontmatter.password) as string[] if (password.length) { - page.data._e = password.map(pwd => genEncrypt(pwd as string)).join(':') + page.data._e = (await pMap(password, item => genEncrypt(item))).join(':') } delete page.frontmatter.password } diff --git a/theme/src/node/pages/extendsPage.ts b/theme/src/node/pages/extendsPage.ts index e555dd32..3e65d2f7 100644 --- a/theme/src/node/pages/extendsPage.ts +++ b/theme/src/node/pages/extendsPage.ts @@ -1,18 +1,16 @@ import type { Page } from 'vuepress/core' import type { ThemePageData } from '../../shared/index.js' -import { getThemeConfig } from '../loadConfig/index.js' import { autoCategory } from './autoCategory.js' import { encryptPage } from './encryptPage.js' import { enableBulletin } from './pageBulletin.js' -export function extendsPageData( +export async function extendsPageData( page: Page, -): void { - const options = getThemeConfig() +): Promise { cleanPageData(page) - encryptPage(page) autoCategory(page) - enableBulletin(page, options) + enableBulletin(page) + await encryptPage(page) } function cleanPageData(page: Page) { diff --git a/theme/src/node/pages/pageBulletin.ts b/theme/src/node/pages/pageBulletin.ts index 71da0e60..0935ab08 100644 --- a/theme/src/node/pages/pageBulletin.ts +++ b/theme/src/node/pages/pageBulletin.ts @@ -1,11 +1,12 @@ import type { Page } from 'vuepress/core' -import type { ThemeOptions, ThemePageData } from '../../shared/index.js' +import type { ThemePageData } from '../../shared/index.js' import { isFunction, isPlainObject } from '@vuepress/helper' +import { getThemeConfig } from '../loadConfig/index.js' export function enableBulletin( page: Page, - options: ThemeOptions, ): void { + const options = getThemeConfig() if (isPlainObject(options.bulletin)) { const enablePage = options.bulletin.enablePage page.data.bulletin = (isFunction(enablePage) ? enablePage(page) : enablePage) ?? true diff --git a/theme/src/node/prepare/prepareEncrypt.ts b/theme/src/node/prepare/prepareEncrypt.ts index 1f0f8a98..b1adf32c 100644 --- a/theme/src/node/prepare/prepareEncrypt.ts +++ b/theme/src/node/prepare/prepareEncrypt.ts @@ -4,6 +4,7 @@ import type { EncryptOptions, ThemePageData } from '../../shared/index.js' import type { FsCache } from '../utils/index.js' import { isEmptyObject, isNumber, isString, toArray } from '@pengzhanbo/utils' import { encodeData, removeLeadingSlash } from '@vuepress/helper' +import pMap from 'p-map' import { getThemeConfig } from '../loadConfig/index.js' import { createFsCache, genEncrypt, hash, perf, resolveContent, writeTemp } from '../utils/index.js' @@ -34,7 +35,7 @@ export async function prepareEncrypt(app: App): Promise { if (!contentHash || contentHash !== currentHash || !resolvedEncrypt) { contentHash = currentHash - resolvedEncrypt = resolveEncrypt(encrypt) + resolvedEncrypt = await resolveEncrypt(encrypt) } await writeTemp(app, 'internal/encrypt.js', resolveContent(app, { name: 'encrypt', @@ -46,12 +47,12 @@ export async function prepareEncrypt(app: App): Promise { perf.log('prepare:encrypt') } -function resolveEncrypt(encrypt?: EncryptOptions): EncryptConfig { +async function resolveEncrypt(encrypt?: EncryptOptions): Promise { const admin = encrypt?.admin - ? toArray(encrypt.admin) - .filter(isStringLike) - .map(item => genEncrypt(item)) - .join(separator) + ? (await pMap( + toArray(encrypt.admin).filter(isStringLike), + item => genEncrypt(item), + )).join(separator) : '' const encryptRules = Object.keys(encrypt?.rules ?? {}).reduce((acc, key) => { @@ -63,14 +64,13 @@ function resolveEncrypt(encrypt?: EncryptOptions): EncryptConfig { const keys = Object.keys(encryptRules) if (!isEmptyObject(encryptRules)) { - Object.keys(encryptRules).forEach((key) => { + for (const key of keys) { const index = keys.indexOf(key) - - rules[String(index)] = toArray(encryptRules[key]) - .filter(isStringLike) - .map(item => genEncrypt(item)) - .join(separator) - }) + rules[String(index)] = (await pMap( + toArray(encryptRules[key]).filter(isStringLike), + item => genEncrypt(item), + )).join(separator) + } } return [encrypt?.global ?? false, separator, admin, keys, rules] diff --git a/theme/src/node/theme.ts b/theme/src/node/theme.ts index 02bb1fd6..fde119e2 100644 --- a/theme/src/node/theme.ts +++ b/theme/src/node/theme.ts @@ -72,7 +72,7 @@ export function plumeTheme(options: ThemeOptions = {}): Theme { templateBuildRenderer, - extendsPage: page => extendsPageData(page as Page), + extendsPage: async page => await extendsPageData(page as Page), onInitialized: async app => await createPages(app), diff --git a/theme/src/node/utils/encrypt.ts b/theme/src/node/utils/encrypt.ts index 1313efcc..fdd15bb9 100644 --- a/theme/src/node/utils/encrypt.ts +++ b/theme/src/node/utils/encrypt.ts @@ -1,6 +1,13 @@ -import { random } from '@pengzhanbo/utils' -import { genSaltSync, hashSync } from 'bcrypt-ts' +import crypto from 'node:crypto' +import { bcrypt } from 'hash-wasm' -export function genEncrypt(pwd: string): string { - return hashSync(String(pwd), genSaltSync(random(8, 16))) +export async function genEncrypt(password: string): Promise { + const salt = new Uint8Array(16) + crypto.getRandomValues(salt) + return bcrypt({ + password, + salt, + costFactor: 11, + outputType: 'encoded', + }) }