Skip to content

Commit 64f6560

Browse files
committed
feat: added encrypt plugin
Signed-off-by: Neko Ayaka <neko@ayaka.moe>
1 parent ad3eeb4 commit 64f6560

12 files changed

Lines changed: 713 additions & 2 deletions

File tree

docs/.vitepress/config.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { defineConfig } from 'vitepress'
33
import { BiDirectionalLinks } from '@nolebase/markdown-it-bi-directional-links'
44
import type { Options as ElementTransformOptions } from '@nolebase/markdown-it-element-transform'
55
import { ElementTransform } from '@nolebase/markdown-it-element-transform'
6+
import { rehype } from 'rehype'
7+
import RehypeStringgify from 'rehype-stringify'
8+
import RehypeRewrite from 'rehype-rewrite'
69

710
// https://vitepress.dev/reference/site-config
811
export default defineConfig({
@@ -78,6 +81,7 @@ export default defineConfig({
7881
{
7982
text: 'VitePress Plugins',
8083
items: [
84+
{ text: 'Encrypt', link: '/pages/en/integrations/vitepress-plugin-encrypt/' },
8185
{ text: 'Enhanced Readabilities', link: '/pages/en/integrations/vitepress-plugin-enhanced-readabilities/' },
8286
{ text: 'Inline Links Previewing', link: '/pages/en/integrations/vitepress-plugin-inline-link-preview/' },
8387
{ text: 'Blinking highlight targeted heading', link: '/pages/en/integrations/vitepress-plugin-highlight-targeted-heading/' },
@@ -129,6 +133,7 @@ export default defineConfig({
129133
{
130134
text: 'VitePress 插件',
131135
items: [
136+
{ text: '保密', link: '/pages/zh-CN/integrations/vitepress-plugin-encrypt/' },
132137
{ text: '阅读增强', link: '/pages/zh-CN/integrations/vitepress-plugin-enhanced-readabilities/' },
133138
{ text: '行内链接预览', link: '/pages/zh-CN/integrations/vitepress-plugin-inline-link-preview/' },
134139
{ text: '闪烁高亮当前的目标标题', link: '/pages/zh-CN/integrations/vitepress-plugin-highlight-targeted-heading/' },
@@ -140,6 +145,46 @@ export default defineConfig({
140145
},
141146
},
142147
},
148+
transformHtml: async (code, id) => {
149+
if (id.includes('vitepress-plugin-encrypt')) {
150+
const rawHTML = ''
151+
152+
const processed = await rehype()
153+
.data('settings', { fragment: true })
154+
.use(RehypeRewrite, {
155+
rewrite: (node) => {
156+
if (node.type === 'element' && node.properties.id === 'vp-nolebase-encrypt-protected-content') {
157+
node.children = [
158+
{
159+
type: 'element',
160+
tagName: 'div',
161+
properties: {
162+
id: 'vp-nolebase-encrypt-protected-content-placeholder',
163+
},
164+
children: [
165+
{
166+
type: 'text',
167+
value: 'This content is protected. Please input the password to view it.',
168+
},
169+
],
170+
},
171+
]
172+
}
173+
},
174+
})
175+
.use(RehypeStringgify)
176+
.use(() => {
177+
return (tree) => {
178+
const scriptNode = {
179+
180+
}
181+
}
182+
})
183+
.process(code)
184+
185+
return processed.toString()
186+
}
187+
},
143188
markdown: {
144189
config(md) {
145190
md.use(BiDirectionalLinks({
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<script setup lang="ts">
2+
import { onMounted, ref } from 'vue'
3+
import { LazyHydrationWrapper } from 'vue3-lazy-hydration'
4+
import { decryptText, encryptText } from '../composables/crypto'
5+
6+
const password = ref('')
7+
const iv = ref('')
8+
const errored = ref(false)
9+
10+
const rawContent = '<div>a</div>'
11+
const encryptedHtml = ref('')
12+
const decryptedHtml = ref('')
13+
const trigger = ref(false)
14+
15+
onMounted(async () => {
16+
try {
17+
const key = 'password'
18+
const encrypted = await encryptText(rawContent, key)
19+
encryptedHtml.value = encrypted.encryptedText
20+
iv.value = encrypted.ivBase64
21+
}
22+
catch (err) {
23+
console.error('Failed to encrypt the content.')
24+
}
25+
})
26+
27+
async function onPasswordInput() {
28+
try {
29+
decryptedHtml.value = await decryptText(encryptedHtml.value, password.value, iv.value)
30+
trigger.value = true
31+
}
32+
catch (err) {
33+
console.error('Invalid password.')
34+
errored.value = true
35+
return
36+
}
37+
38+
errored.value = false
39+
}
40+
</script>
41+
42+
<template>
43+
<div id="vp-nolebase-protected">
44+
<div>
45+
<div relative>
46+
<div flex="~ col" absolute left-0 top-0 z-10 h-full w-full items-center justify-center>
47+
<div mb-4>
48+
<p mt="0!" mb="4!" text-center>
49+
<span font-semibold>You don't have permissions to access this page.</span>
50+
</p>
51+
<p my="0!" text-left>
52+
<span>You could either</span>
53+
<ul my="0!">
54+
<li my="0!">
55+
Request an access permission from the owner.
56+
</li>
57+
<li my="0!">
58+
Prompt a valid password for it.
59+
</li>
60+
</ul>
61+
</p>
62+
</div>
63+
<div w-full flex="~ col" items-center justify-center max-w="80">
64+
<button
65+
min-w-35 w-full rounded-lg
66+
px-3 py-2
67+
bg="zinc-700 hover:zinc-600 active:zinc-700 dark:zinc-200 dark:hover:zinc-300 dark:active:zinc-400"
68+
text="zinc-100 dark:zinc-900 " font-semibold
69+
transition="all ease" duration-750
70+
>
71+
Request Access
72+
</button>
73+
<span>or</span>
74+
<form min-w-35 w-full flex="~ row" @submit.prevent="() => {}">
75+
<input
76+
v-model="password"
77+
type="password"
78+
mr-2
79+
w-full rounded-lg
80+
px-3 py-2
81+
bg="$vp-c-bg"
82+
text="$vp-c-text-1" font-semibold
83+
transition="all ease" duration-750
84+
:class="[
85+
errored ? 'outline-offset-1 outline-2 outline-red-400' : 'outline-offset-1 outline-2 outline-zinc-100',
86+
]"
87+
placeholder="Enter the valid password..."
88+
>
89+
<button
90+
rounded-lg
91+
px-3 py-2
92+
font-semibold
93+
transition="all ease" duration-750
94+
:class="[
95+
password !== '' ? 'bg-zinc-700 hover:bg-zinc-600 active:bg-zinc-700 dark:bg-zinc-200 dark:hover:bg-zinc-300 dark:active:bg-zinc-400 text-zinc-100 dark:text-zinc-900' : 'bg-$vp-c-bg cursor-not-allowed! text-$vp-c-text-1',
96+
]"
97+
@click="onPasswordInput"
98+
>
99+
Unlock
100+
</button>
101+
</form>
102+
</div>
103+
</div>
104+
<div blur-md space-y-5>
105+
<div h="[1lh]" bg="[var(--vp-c-text-1)]" w-full rounded-lg opacity-5 />
106+
<div h="[1lh]" bg="[var(--vp-c-text-1)]" w-full rounded-lg opacity-5 />
107+
<div h="[1lh]" bg="[var(--vp-c-text-1)]" w-full rounded-lg opacity-5 />
108+
<div h="[1lh]" bg="[var(--vp-c-text-1)]" w-full rounded-lg opacity-5 />
109+
<div h="[1lh]" bg="[var(--vp-c-text-1)]" w-full rounded-lg opacity-5 />
110+
<div h="[1lh]" bg="[var(--vp-c-text-1)]" w-full rounded-lg opacity-5 />
111+
<div h="[1lh]" bg="[var(--vp-c-text-1)]" w-full rounded-lg opacity-5 />
112+
<div h="[1lh]" bg="[var(--vp-c-text-1)]" w-full rounded-lg opacity-5 />
113+
<div h="[1lh]" bg="[var(--vp-c-text-1)]" w-full rounded-lg opacity-5 />
114+
<div h="[1lh]" bg="[var(--vp-c-text-1)]" w-full rounded-lg opacity-5 />
115+
<div h="[1lh]" bg="[var(--vp-c-text-1)]" w-full rounded-lg opacity-5 />
116+
</div>
117+
</div>
118+
<LazyHydrationWrapper :when-triggered="trigger">
119+
<div id="vp-nolebase-protected-content">
120+
<slot />
121+
</div>
122+
<div v-html="decryptedHtml" />
123+
</LazyHydrationWrapper>
124+
</div>
125+
</div>
126+
</template>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
function useCrypto() {
2+
if (crypto)
3+
return crypto
4+
5+
if (window.crypto)
6+
return window.crypto
7+
8+
throw new Error('Crypto not supported')
9+
}
10+
11+
export async function encryptText(plainText: string, plainTextKey: string) {
12+
const crypto = useCrypto()
13+
14+
// Encode the key and hash it using SHA-256
15+
const keyMaterial = await getKeyMaterial(plainTextKey)
16+
const key = await crypto.subtle.importKey(
17+
'raw',
18+
keyMaterial,
19+
{ name: 'AES-GCM', length: 256 },
20+
false,
21+
['encrypt'],
22+
)
23+
24+
// Encode the text to be encrypted
25+
const encoder = new TextEncoder()
26+
const encodedText = encoder.encode(plainText)
27+
28+
// Generate an IV
29+
const iv = window.crypto.getRandomValues(new Uint8Array(12))
30+
31+
// Encrypt the text
32+
const encryptedData = await window.crypto.subtle.encrypt(
33+
{
34+
name: 'AES-GCM',
35+
iv,
36+
},
37+
key,
38+
encodedText,
39+
)
40+
41+
// Convert the encrypted data to Base64
42+
const encryptedText = bufferToBase64(encryptedData)
43+
44+
// Convert the IV to Base64
45+
const ivBase64 = bufferToBase64(iv)
46+
47+
return { encryptedText, ivBase64 }
48+
}
49+
50+
export async function decryptText(encryptedTextBase64: string, plainTextKey: string, ivBase64: string) {
51+
// Decode the Base64 encrypted text and IV
52+
const encryptedData = base64ToBuffer(encryptedTextBase64)
53+
const iv = base64ToBuffer(ivBase64)
54+
55+
// Encode the key and hash it using SHA-256
56+
const keyMaterial = await getKeyMaterial(plainTextKey)
57+
58+
const key = await window.crypto.subtle.importKey(
59+
'raw',
60+
keyMaterial,
61+
{ name: 'AES-GCM', length: 256 },
62+
false,
63+
['decrypt'],
64+
)
65+
66+
// Decrypt the text
67+
const decryptedData = await window.crypto.subtle.decrypt(
68+
{
69+
name: 'AES-GCM',
70+
iv,
71+
},
72+
key,
73+
encryptedData,
74+
)
75+
76+
// Decode the decrypted data
77+
const decoder = new TextDecoder()
78+
return decoder.decode(decryptedData)
79+
}
80+
81+
// Helper function to convert a Base64 string to an ArrayBuffer
82+
function base64ToBuffer(base64: string): ArrayBuffer {
83+
const binaryString = atob(base64)
84+
const bytes = new Uint8Array(binaryString.length)
85+
for (let i = 0; i < binaryString.length; i++)
86+
bytes[i] = binaryString.charCodeAt(i)
87+
88+
return bytes.buffer
89+
}
90+
91+
// Helper function to convert a plaintext string to a SHA-256 hash
92+
async function getKeyMaterial(plainTextKey: string): Promise<ArrayBuffer> {
93+
const crypto = useCrypto()
94+
95+
const encoder = new TextEncoder()
96+
const keyData = encoder.encode(plainTextKey)
97+
return crypto.subtle.digest('SHA-256', keyData)
98+
}
99+
100+
// Helper function to convert an ArrayBuffer to a Base64 string
101+
function bufferToBase64(buffer: ArrayBuffer): string {
102+
const bytes = new Uint8Array(buffer)
103+
const binary = bytes.reduce((acc, byte) => acc + String.fromCharCode(byte), '')
104+
return btoa(binary)
105+
}

docs/.vitepress/theme/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import '@nolebase/vitepress-plugin-page-properties/client/style.css'
3737
import './styles/vars.css'
3838
import './styles/main.css'
3939

40+
import Protected from './components/Protected.vue'
4041
import IntegrationCard from './components/IntegrationCard.vue'
4142
import HomeContent from './components/HomeContent.vue'
4243

@@ -56,6 +57,7 @@ export const Theme: ThemeConfig = {
5657
})
5758
},
5859
enhanceApp({ app }) {
60+
app.component('Protected', Protected)
5961
app.component('IntegrationCard', IntegrationCard)
6062
app.component('HomeContent', HomeContent)
6163

docs/pages/en/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Nólëbase Integrations project provides a variety of integrations, plugins, com
3838

3939
<IntegrationCard type="markdown-it" title="Elements Transformation" package="markdown-it-element-transform" />
4040

41+
<IntegrationCard type="vitepress" title="Encrypt" package="vitepress-plugin-encrypt" />
42+
4143
<IntegrationCard type="vitepress" title="Enhanced Readabilities" package="vitepress-plugin-enhanced-readabilities" />
4244

4345
<IntegrationCard type="vitepress" title="Inline Link Previewing" package="vitepress-plugin-inline-link-preview" />
@@ -53,4 +55,3 @@ Nólëbase Integrations project provides a variety of integrations, plugins, com
5355
</div>
5456

5557
</HomeContent>
56-

docs/pages/en/integrations/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Nólëbase Integrations project provides a variety of integrations, plugins, com
1414

1515
<br />
1616

17+
<IntegrationCard type="vitepress" title="Encrypt" package="vitepress-plugin-encrypt" />
18+
19+
<br />
20+
1721
<IntegrationCard type="vitepress" title="Enhanced Readabilities" package="vitepress-plugin-enhanced-readabilities" />
1822

1923
<br />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<Protected>
2+
3+
# Share
4+
5+
<IntegrationCard type="markdown-it" title="Elements Transformation" package="markdown-it-element-transform" />
6+
7+
</Protected>

docs/pages/zh-CN/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Nólëbase 集成项目提供多种不同的集成、插件、组件和库来方
3838

3939
<IntegrationCard type="markdown-it" title="元素转换" package="markdown-it-element-transform" />
4040

41+
<IntegrationCard type="vitepress" title="保密" package="vitepress-plugin-encrypt" />
42+
4143
<IntegrationCard type="vitepress" title="阅读增强" package="vitepress-plugin-enhanced-readabilities" />
4244

4345
<IntegrationCard type="vitepress" title="行内链接预览" package="vitepress-plugin-inline-link-preview" />

docs/pages/zh-CN/integrations/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Nólëbase 集成项目提供多种不同的集成、插件、组件和库来方
1414

1515
<br />
1616

17+
<IntegrationCard type="vitepress" title="保密" package="vitepress-plugin-encrypt" />
18+
19+
<br />
20+
1721
<IntegrationCard type="vitepress" title="阅读增强" package="vitepress-plugin-enhanced-readabilities" />
1822

1923
<br />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<Protected>
2+
3+
# 分享
4+
5+
<IntegrationCard type="markdown-it" title="元素变换" package="markdown-it-element-transform" />
6+
7+
</Protected>

0 commit comments

Comments
 (0)