219 lines
5.8 KiB
TypeScript
219 lines
5.8 KiB
TypeScript
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 { computed, inject, provide } from 'vue'
|
|
import { useRoute } from 'vuepress/client'
|
|
import { removeLeadingSlash } from 'vuepress/shared'
|
|
import { useData } from './data.js'
|
|
import { useEncryptData } from './encrypt-data.js'
|
|
|
|
export interface Encrypt {
|
|
hasPageEncrypt: Ref<boolean>
|
|
isGlobalDecrypted: Ref<boolean>
|
|
isPageDecrypted: Ref<boolean>
|
|
hashList: Ref<EncryptDataRule[]>
|
|
}
|
|
|
|
export const EncryptSymbol: InjectionKey<Encrypt> = Symbol(
|
|
__VUEPRESS_DEV__ ? 'Encrypt' : '',
|
|
)
|
|
|
|
const storage = useSessionStorage('2a0a3d6afb2fdf1f', () => {
|
|
if (__VUEPRESS_SSR__) {
|
|
return { s: ['', ''] as const, g: '', p: {} as Record<string, string> }
|
|
}
|
|
return {
|
|
s: [genSaltSync(10), genSaltSync(10)] as const,
|
|
g: '' as string,
|
|
p: {} as Record<string, 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)
|
|
if (compareCache.has(key))
|
|
return compareCache.get(key)!
|
|
|
|
try {
|
|
const result = await compare(content, hash)
|
|
compareCache.set(key, result)
|
|
return result
|
|
}
|
|
catch {
|
|
compareCache.set(key, false)
|
|
return false
|
|
}
|
|
}
|
|
|
|
const matchCache = new Map<string, RegExp>()
|
|
function createMatchRegex(match: string) {
|
|
if (matchCache.has(match))
|
|
return matchCache.get(match)!
|
|
|
|
const regex = new RegExp(match)
|
|
matchCache.set(match, regex)
|
|
return regex
|
|
}
|
|
|
|
function toMatch(match: string, pagePath: string, filePathRelative: string | null) {
|
|
const relativePath = filePathRelative || ''
|
|
if (match[0] === '^') {
|
|
const regex = createMatchRegex(match)
|
|
return regex.test(pagePath) || regex.test(relativePath)
|
|
}
|
|
if (match.endsWith('.md'))
|
|
return relativePath && relativePath.endsWith(match)
|
|
|
|
return pagePath.startsWith(match) || relativePath.startsWith(removeLeadingSlash(match))
|
|
}
|
|
|
|
export function setupEncrypt(): void {
|
|
const { page } = useData()
|
|
const route = useRoute()
|
|
const encrypt = useEncryptData()
|
|
|
|
const hasPageEncrypt = computed(() => {
|
|
const pagePath = route.path
|
|
const filePathRelative = page.value.filePathRelative
|
|
if (page.value._e)
|
|
return true
|
|
|
|
return encrypt.value.ruleList.length
|
|
? encrypt.value.matches.some(match => toMatch(match, pagePath, filePathRelative))
|
|
: false
|
|
})
|
|
|
|
const isGlobalDecrypted = computed(() => {
|
|
if (!encrypt.value.global)
|
|
return true
|
|
|
|
const hash = splitHash(storage.value.g)
|
|
|
|
return !!hash && encrypt.value.admins.includes(hash)
|
|
})
|
|
|
|
const hashList = computed(() => {
|
|
const pagePath = route.path
|
|
const filePathRelative = page.value.filePathRelative
|
|
const passwords = typeof page.value._e === 'string' ? page.value._e.split(':') : []
|
|
const pageRule: EncryptDataRule | undefined = passwords.length
|
|
? { key: pagePath.replace(/\//g, '').replace(/\.html$/, ''), match: pagePath, rules: passwords }
|
|
: undefined
|
|
const rules = encrypt.value.ruleList.length
|
|
? encrypt.value.ruleList
|
|
.filter(item => toMatch(item.match, pagePath, filePathRelative))
|
|
: []
|
|
|
|
return [pageRule, ...rules].filter(Boolean) as EncryptDataRule[]
|
|
})
|
|
|
|
const isPageDecrypted = computed(() => {
|
|
if (!hasPageEncrypt.value)
|
|
return true
|
|
|
|
const hash = splitHash(storage.value.g || '')
|
|
if (hash && encrypt.value.admins.includes(hash))
|
|
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))
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
})
|
|
|
|
provide(EncryptSymbol, {
|
|
hasPageEncrypt,
|
|
isGlobalDecrypted,
|
|
isPageDecrypted,
|
|
hashList,
|
|
})
|
|
}
|
|
|
|
export function useEncrypt(): Encrypt {
|
|
const result = inject(EncryptSymbol)
|
|
|
|
if (!result)
|
|
throw new Error('useEncrypt() is called without setup')
|
|
|
|
return result
|
|
}
|
|
|
|
export function useEncryptCompare(): {
|
|
compareGlobal: (password: string) => Promise<boolean>
|
|
comparePage: (password: string) => Promise<boolean>
|
|
} {
|
|
const encrypt = useEncryptData()
|
|
const { page } = useData()
|
|
const route = useRoute()
|
|
const { hashList } = useEncrypt()
|
|
|
|
async function compareGlobal(password: string): Promise<boolean> {
|
|
if (!password)
|
|
return false
|
|
|
|
for (const admin of encrypt.value.admins) {
|
|
if (await compareDecrypt(password, admin, encrypt.value.separator)) {
|
|
storage.value.g = mergeHash(admin)
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
async function comparePage(password: string): Promise<boolean> {
|
|
if (!password)
|
|
return false
|
|
|
|
const pagePath = route.path
|
|
const filePathRelative = page.value.filePathRelative
|
|
|
|
let decrypted = false
|
|
|
|
for (const { match, key, rules } of hashList.value) {
|
|
if (toMatch(match, pagePath, filePathRelative)) {
|
|
for (const rule of rules) {
|
|
if (await compareDecrypt(password, rule, encrypt.value.separator)) {
|
|
decrypted = true
|
|
storage.value.p = {
|
|
...storage.value.p,
|
|
[key]: mergeHash(rule),
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if (decrypted)
|
|
break
|
|
}
|
|
}
|
|
|
|
if (!decrypted) {
|
|
decrypted = await compareGlobal(password)
|
|
}
|
|
|
|
return decrypted
|
|
}
|
|
|
|
return { compareGlobal, comparePage }
|
|
}
|