feat(theme): migrate bcrypt-ts to hash-wasm (#774)

This commit is contained in:
pengzhanbo 2025-12-03 17:16:14 +08:00 committed by GitHub
parent 32f4a8be5a
commit 4b1cecf2bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 78 additions and 83 deletions

23
pnpm-lock.yaml generated
View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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<Encrypt> = Symbol(
const storage = useSessionStorage('2a0a3d6afb2fdf1f', () => {
if (__VUEPRESS_SSR__) {
return { s: ['', ''] as const, g: '', p: {} as Record<string, string> }
return { g: '', p: [] as string[] }
}
return {
s: [genSaltSync(10), genSaltSync(10)] as const,
g: '' as string,
p: {} as Record<string, string>,
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<string, boolean>()
async function compareDecrypt(content: string, hash: string, separator = ':'): Promise<boolean> {
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
}
}

View File

@ -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')

View File

@ -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<ThemePageData>,
): void {
const password = toArray(page.frontmatter.password)
): Promise<void> {
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
}

View File

@ -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<ThemePageData>,
): void {
const options = getThemeConfig()
): Promise<void> {
cleanPageData(page)
encryptPage(page)
autoCategory(page)
enableBulletin(page, options)
enableBulletin(page)
await encryptPage(page)
}
function cleanPageData(page: Page<ThemePageData>) {

View File

@ -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<ThemePageData>,
options: ThemeOptions,
): void {
const options = getThemeConfig()
if (isPlainObject(options.bulletin)) {
const enablePage = options.bulletin.enablePage
page.data.bulletin = (isFunction(enablePage) ? enablePage(page) : enablePage) ?? true

View File

@ -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<void> {
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<void> {
perf.log('prepare:encrypt')
}
function resolveEncrypt(encrypt?: EncryptOptions): EncryptConfig {
async function resolveEncrypt(encrypt?: EncryptOptions): Promise<EncryptConfig> {
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]

View File

@ -72,7 +72,7 @@ export function plumeTheme(options: ThemeOptions = {}): Theme {
templateBuildRenderer,
extendsPage: page => extendsPageData(page as Page<ThemePageData>),
extendsPage: async page => await extendsPageData(page as Page<ThemePageData>),
onInitialized: async app => await createPages(app),

View File

@ -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<string> {
const salt = new Uint8Array(16)
crypto.getRandomValues(salt)
return bcrypt({
password,
salt,
costFactor: 11,
outputType: 'encoded',
})
}