diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 9e2b4391..5bcdacc7 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -9,6 +9,10 @@ import type { Options as ElementTransformOptions } from '@nolebase/markdown-it-e import { ElementTransform } from '@nolebase/markdown-it-element-transform' import { buildEndGenerateOpenGraphImages } from '@nolebase/vitepress-plugin-og-image' +import { rehype } from 'rehype' +import RehypeStringgify from 'rehype-stringify' +import RehypeRewrite from 'rehype-rewrite' + export const sidebars: Record = { 'en': { '/': [ @@ -60,6 +64,7 @@ export const sidebars: Record = { { text: 'Changelog & File history', link: '/pages/en/integrations/vitepress-plugin-git-changelog/' }, { text: 'Page properties', link: '/pages/en/integrations/vitepress-plugin-page-properties/' }, { text: 'Previewing image (social media card) generation', link: '/pages/en/integrations/vitepress-plugin-og-image/' }, + { text: 'Encrypt', link: '/pages/en/integrations/vitepress-plugin-encrypt/' }, ], }, ], @@ -134,6 +139,7 @@ export const sidebars: Record = { { text: '变更日志 及 文件历史', link: '/pages/zh-CN/integrations/vitepress-plugin-git-changelog/' }, { text: '页面属性', link: '/pages/zh-CN/integrations/vitepress-plugin-page-properties/' }, { text: '预览图片(社交媒体卡片)生成', link: '/pages/zh-CN/integrations/vitepress-plugin-og-image/' }, + { text: '保密', link: '/pages/zh-CN/integrations/vitepress-plugin-encrypt/' }, ], }, ], @@ -239,6 +245,46 @@ export default defineConfig({ }, }, }, + transformHtml: async (code, id) => { + if (id.includes('vitepress-plugin-encrypt')) { + const rawHTML = '' + + const processed = await rehype() + .data('settings', { fragment: true }) + .use(RehypeRewrite, { + rewrite: (node) => { + if (node.type === 'element' && node.properties.id === 'vp-nolebase-encrypt-protected-content') { + node.children = [ + { + type: 'element', + tagName: 'div', + properties: { + id: 'vp-nolebase-encrypt-protected-content-placeholder', + }, + children: [ + { + type: 'text', + value: 'This content is protected. Please input the password to view it.', + }, + ], + }, + ] + } + }, + }) + .use(RehypeStringgify) + .use(() => { + return (tree) => { + const scriptNode = { + + } + } + }) + .process(code) + + return processed.toString() + } + }, markdown: { config(md) { md.use(MarkdownItFootnote) diff --git a/docs/.vitepress/theme/components/Protected.vue b/docs/.vitepress/theme/components/Protected.vue new file mode 100644 index 00000000..d75c938b --- /dev/null +++ b/docs/.vitepress/theme/components/Protected.vue @@ -0,0 +1,126 @@ + + + diff --git a/docs/.vitepress/theme/composables/crypto.ts b/docs/.vitepress/theme/composables/crypto.ts new file mode 100644 index 00000000..b938ba56 --- /dev/null +++ b/docs/.vitepress/theme/composables/crypto.ts @@ -0,0 +1,105 @@ +function useCrypto() { + if (crypto) + return crypto + + if (window.crypto) + return window.crypto + + throw new Error('Crypto not supported') +} + +export async function encryptText(plainText: string, plainTextKey: string) { + const crypto = useCrypto() + + // Encode the key and hash it using SHA-256 + const keyMaterial = await getKeyMaterial(plainTextKey) + const key = await crypto.subtle.importKey( + 'raw', + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt'], + ) + + // Encode the text to be encrypted + const encoder = new TextEncoder() + const encodedText = encoder.encode(plainText) + + // Generate an IV + const iv = window.crypto.getRandomValues(new Uint8Array(12)) + + // Encrypt the text + const encryptedData = await window.crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv, + }, + key, + encodedText, + ) + + // Convert the encrypted data to Base64 + const encryptedText = bufferToBase64(encryptedData) + + // Convert the IV to Base64 + const ivBase64 = bufferToBase64(iv) + + return { encryptedText, ivBase64 } +} + +export async function decryptText(encryptedTextBase64: string, plainTextKey: string, ivBase64: string) { + // Decode the Base64 encrypted text and IV + const encryptedData = base64ToBuffer(encryptedTextBase64) + const iv = base64ToBuffer(ivBase64) + + // Encode the key and hash it using SHA-256 + const keyMaterial = await getKeyMaterial(plainTextKey) + + const key = await window.crypto.subtle.importKey( + 'raw', + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['decrypt'], + ) + + // Decrypt the text + const decryptedData = await window.crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv, + }, + key, + encryptedData, + ) + + // Decode the decrypted data + const decoder = new TextDecoder() + return decoder.decode(decryptedData) +} + +// Helper function to convert a Base64 string to an ArrayBuffer +function base64ToBuffer(base64: string): ArrayBuffer { + const binaryString = atob(base64) + const bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) + bytes[i] = binaryString.charCodeAt(i) + + return bytes.buffer +} + +// Helper function to convert a plaintext string to a SHA-256 hash +async function getKeyMaterial(plainTextKey: string): Promise { + const crypto = useCrypto() + + const encoder = new TextEncoder() + const keyData = encoder.encode(plainTextKey) + return crypto.subtle.digest('SHA-256', keyData) +} + +// Helper function to convert an ArrayBuffer to a Base64 string +function bufferToBase64(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer) + const binary = bytes.reduce((acc, byte) => acc + String.fromCharCode(byte), '') + return btoa(binary) +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index b537cfc1..70936c51 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -4,6 +4,7 @@ import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client' import { NuLazyTeleportRiveCanvas } from '@nolebase/ui' import { NolebasePluginSet, defineThemeUnconfig } from '@nolebase/unconfig-vitepress' +import Protected from './components/Protected.vue' import IntegrationCard from './components/IntegrationCard.vue' import HomeContent from './components/HomeContent.vue' @@ -28,6 +29,7 @@ export default defineThemeUnconfig({ app.component('IntegrationCard', IntegrationCard) app.component('HomeContent', HomeContent) app.use(TwoslashFloatingVue as Plugin) + app.component('Protected', Protected) }, pluginSets: [ NolebasePluginSet({ diff --git a/docs/pages/en/index.md b/docs/pages/en/index.md index 8855d084..bb198db4 100644 --- a/docs/pages/en/index.md +++ b/docs/pages/en/index.md @@ -85,6 +85,12 @@ Nólëbase Integrations project provides a variety of integrations, plugins, com + + + +