Compare commits
2 commits
a2d594b393
...
93fb60724c
Author | SHA1 | Date | |
---|---|---|---|
93fb60724c | |||
d5d025e011 |
5 changed files with 211 additions and 0 deletions
131
src/components/shortcodes/ProtectedContent.astro
Normal file
131
src/components/shortcodes/ProtectedContent.astro
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
---
|
||||||
|
import { encrypt } from '../../plugins/encrypt';
|
||||||
|
import {siteConfig} from "../../config";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
password?: string;
|
||||||
|
pwEnv?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { password: propPassword, pwEnv } = Astro.props;
|
||||||
|
// Get password from props, environment variable, or site config
|
||||||
|
const password = (pwEnv ? import.meta.env[pwEnv] : propPassword) || siteConfig.contentPassword || import.meta.env.CONTENT_PASSWORD || Math.random().toString();
|
||||||
|
|
||||||
|
// Get the slot content
|
||||||
|
const content = await Astro.slots.render('default');
|
||||||
|
|
||||||
|
// Encrypt content at build time
|
||||||
|
const { encryptedData, iv } = encrypt(content, password);
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="encrypted-content" data-encrypted={encryptedData} data-iv={iv}>
|
||||||
|
<div class="password-form">
|
||||||
|
<p>This content is protected. Enter the password to view it:</p>
|
||||||
|
<form>
|
||||||
|
<input type="password" class="decrypt-password" title="password" />
|
||||||
|
<button class="decrypt-button">Decrypt</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="content-container hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.encrypted-content {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
div.password-form {
|
||||||
|
margin: 0.5rem auto;
|
||||||
|
}
|
||||||
|
input[type="password"] {
|
||||||
|
background: var(--accent-color);
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
div.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Client-side decryption logic
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const containers = document.querySelectorAll('.encrypted-content');
|
||||||
|
|
||||||
|
containers.forEach(container => {
|
||||||
|
const encryptedData = container.getAttribute('data-encrypted');
|
||||||
|
const iv = container.getAttribute('data-iv');
|
||||||
|
const passwordInput = container.querySelector('.decrypt-password');
|
||||||
|
const decryptButton = container.querySelector('.decrypt-button');
|
||||||
|
const contentContainer = container.querySelector('.content-container');
|
||||||
|
const passwordForm = container.querySelector('.password-form');
|
||||||
|
|
||||||
|
decryptButton.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const password = passwordInput.value;
|
||||||
|
if (!password) return;
|
||||||
|
|
||||||
|
const content = await decrypt(encryptedData, iv, password);
|
||||||
|
contentContainer.innerHTML = content;
|
||||||
|
contentContainer.classList.remove('hidden');
|
||||||
|
passwordForm.classList.add('hidden');
|
||||||
|
} catch (error) {
|
||||||
|
alert('Incorrect password');
|
||||||
|
console.error('Decryption failed:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow pressing Enter to decrypt
|
||||||
|
passwordInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
decryptButton.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function decrypt(encryptedData, iv, password) {
|
||||||
|
// Convert base64 to array buffer
|
||||||
|
const encryptedBytes = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));
|
||||||
|
const ivBytes = Uint8Array.from(atob(iv), c => c.charCodeAt(0));
|
||||||
|
|
||||||
|
// Derive key from password
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const passwordBytes = encoder.encode(password);
|
||||||
|
const keyMaterial = await window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
passwordBytes,
|
||||||
|
{ name: 'PBKDF2' },
|
||||||
|
false,
|
||||||
|
['deriveBits', 'deriveKey']
|
||||||
|
);
|
||||||
|
|
||||||
|
const key = await window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: ivBytes,
|
||||||
|
iterations: 100000,
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
keyMaterial,
|
||||||
|
{ name: 'AES-GCM', length: 256 },
|
||||||
|
false,
|
||||||
|
['decrypt']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Decrypt
|
||||||
|
const decryptedBytes = await window.crypto.subtle.decrypt(
|
||||||
|
{ name: 'AES-GCM', iv: ivBytes },
|
||||||
|
key,
|
||||||
|
encryptedBytes
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert array buffer to text
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
return decoder.decode(decryptedBytes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
6
src/components/shortcodes/Ruby.astro
Normal file
6
src/components/shortcodes/Ruby.astro
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
const { text, ruby } = Astro.props;
|
||||||
|
---
|
||||||
|
<ruby>
|
||||||
|
{text} <rp>(</rp><rt>{ruby}</rt><rp>)</rp>
|
||||||
|
</ruby>
|
|
@ -28,6 +28,10 @@ export const siteConfig = {
|
||||||
searchEngine: 'bing', // 'google', 'duckduckgo', 'bing'(broken until M1cr0$0ft get support for it), defaults to 'google'
|
searchEngine: 'bing', // 'google', 'duckduckgo', 'bing'(broken until M1cr0$0ft get support for it), defaults to 'google'
|
||||||
// content
|
// content
|
||||||
displayAvatar: true, // display author avatar in the article list and info line of article page
|
displayAvatar: true, // display author avatar in the article list and info line of article page
|
||||||
|
// encryption
|
||||||
|
// the global password to encrypt/decrypt the content, if set, all <ProtectedContent/> without specifying a password will be encrypted with this password
|
||||||
|
// you can use a different environment variable to set the password.
|
||||||
|
contentPassword: import.meta.env.CONTENT_PASSWORD,
|
||||||
// footer
|
// footer
|
||||||
// yes you can write html safely here
|
// yes you can write html safely here
|
||||||
customFooter: '<i>I have no mouth, and I must SCREAM</i>',
|
customFooter: '<i>I have no mouth, and I must SCREAM</i>',
|
||||||
|
|
|
@ -7,6 +7,8 @@ tags: ["markdown", "css", "html", "sample"]
|
||||||
import Callout from '/src/components/shortcodes/Callout.astro';
|
import Callout from '/src/components/shortcodes/Callout.astro';
|
||||||
import LinkCard from '/src/components/shortcodes/LinkCard.astro';
|
import LinkCard from '/src/components/shortcodes/LinkCard.astro';
|
||||||
import Spoiler from '/src/components/shortcodes/Spoiler.astro';
|
import Spoiler from '/src/components/shortcodes/Spoiler.astro';
|
||||||
|
import Ruby from '/src/components/shortcodes/Ruby.astro';
|
||||||
|
import ProtectedContent from '/src/components/shortcodes/ProtectedContent.astro';
|
||||||
|
|
||||||
This article offers a sample of basic and extended Markdown formatting that can be used, also it shows how some basic HTML elements are decorated.
|
This article offers a sample of basic and extended Markdown formatting that can be used, also it shows how some basic HTML elements are decorated.
|
||||||
## Markdown in Astro
|
## Markdown in Astro
|
||||||
|
@ -52,6 +54,17 @@ You can also display a 'tip' for the reader when hovered:
|
||||||
</Spoiler>
|
</Spoiler>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Ruby
|
||||||
|
The `Ruby` component allows you to quickly add ruby annotations to your text, which is useful for providing pronunciation or additional information about certain words, especially in East Asian languages.
|
||||||
|
|
||||||
|
<Ruby text="汉" ruby="han" />
|
||||||
|
<Ruby text="字" ruby="zi" />
|
||||||
|
|
||||||
|
```mdx
|
||||||
|
<Ruby text="汉" ruby="han" />
|
||||||
|
<Ruby text="字" ruby="zi" />
|
||||||
|
```
|
||||||
|
|
||||||
### Callouts
|
### Callouts
|
||||||
You can use callouts to highlight important information or warnings in your content. Callouts are styled boxes that draw attention to specific parts of the text.
|
You can use callouts to highlight important information or warnings in your content. Callouts are styled boxes that draw attention to specific parts of the text.
|
||||||
<Callout icon='💁' type="info">
|
<Callout icon='💁' type="info">
|
||||||
|
@ -88,6 +101,35 @@ You can use the `LinkCard` component to create cards that link to external resou
|
||||||
Or to customize the card further with a title and description:
|
Or to customize the card further with a title and description:
|
||||||
<LinkCard url="https://www.bilibili.com/video/BV1PC4y1L7mq/" title="Don't check the description" description="Don't click on the link" />
|
<LinkCard url="https://www.bilibili.com/video/BV1PC4y1L7mq/" title="Don't check the description" description="Don't click on the link" />
|
||||||
|
|
||||||
|
### Protected Content
|
||||||
|
You can use the `ProtectedContent` component to protect certain parts of your content with a password. This is useful for sharing exclusive content or information that should only be accessible to certain users.
|
||||||
|
<Callout type="warning">
|
||||||
|
About security:
|
||||||
|
|
||||||
|
Although the encrypt process is happened at build and encrypted content is not visible in the rendered HTML. But it's still not 100% secure, as the password can be stored in the source code of a document (which could be public). Use it only for not really sensitive content.
|
||||||
|
Using a environment variable to store the password is recommended.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<ProtectedContent password="42">
|
||||||
|
Yes, what you input is the ultimate answer to *life*, *the universe*, and **everything**.
|
||||||
|
</ProtectedContent>
|
||||||
|
|
||||||
|
```mdx
|
||||||
|
<ProtectedContent password="42">
|
||||||
|
Yes, what you input is the ultimate answer to *life*, *the universe*, and **everything**.
|
||||||
|
</ProtectedContent>
|
||||||
|
```
|
||||||
|
|
||||||
|
<ProtectedContent>
|
||||||
|
And this will be encrypted with a global password, which is set in the environment variable `CONTENT_PASSWORD` or defined by `config.ts`. (config.ts takes precedence over the environment variable)
|
||||||
|
</ProtectedContent>
|
||||||
|
|
||||||
|
```mdx
|
||||||
|
<ProtectedContent>
|
||||||
|
And this will be encrypted with a global password, which is set in the environment variable `CONTENT_PASSWORD` or defined by `config.ts`. (config.ts takes precedence over the environment variable)
|
||||||
|
</ProtectedContent>
|
||||||
|
```
|
||||||
|
|
||||||
## Headings
|
## Headings
|
||||||
|
|
||||||
The following HTML `<h1>`—`<h6>` elements represent six levels of section headings. `<h1>` is the highest section level while `<h6>` is the lowest.
|
The following HTML `<h1>`—`<h6>` elements represent six levels of section headings. `<h1>` is the highest section level while `<h6>` is the lowest.
|
||||||
|
|
28
src/plugins/encrypt.ts
Normal file
28
src/plugins/encrypt.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
export function encrypt(content: string, password: string) {
|
||||||
|
// Generate a random initialization vector
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
|
||||||
|
// Derive key from password using PBKDF2
|
||||||
|
const key = crypto.pbkdf2Sync(password, iv, 100000, 32, 'sha256');
|
||||||
|
|
||||||
|
// Create cipher
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
||||||
|
|
||||||
|
// Encrypt content
|
||||||
|
let encryptedData = cipher.update(content, 'utf8', 'base64');
|
||||||
|
encryptedData += cipher.final('base64');
|
||||||
|
|
||||||
|
// Get auth tag and append to encrypted data
|
||||||
|
const authTag = cipher.getAuthTag();
|
||||||
|
const encryptedBuffer = Buffer.concat([
|
||||||
|
Buffer.from(encryptedData, 'base64'),
|
||||||
|
authTag
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
encryptedData: encryptedBuffer.toString('base64'),
|
||||||
|
iv: iv.toString('base64')
|
||||||
|
};
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue