diff --git a/src/components/shortcodes/ProtectedContent.astro b/src/components/shortcodes/ProtectedContent.astro new file mode 100644 index 0000000..1ab737d --- /dev/null +++ b/src/components/shortcodes/ProtectedContent.astro @@ -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); +--- + +
+
+

This content is protected. Enter the password to view it:

+
+ + +
+
+ +
+ + + + \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index c45b1c5..40171d0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -28,6 +28,10 @@ export const siteConfig = { searchEngine: 'bing', // 'google', 'duckduckgo', 'bing'(broken until M1cr0$0ft get support for it), defaults to 'google' // content 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 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 // yes you can write html safely here customFooter: 'I have no mouth, and I must SCREAM', diff --git a/src/content/posts/markdown-example/index.mdx b/src/content/posts/markdown-example/index.mdx index 99438d2..de1689f 100644 --- a/src/content/posts/markdown-example/index.mdx +++ b/src/content/posts/markdown-example/index.mdx @@ -8,6 +8,7 @@ import Callout from '/src/components/shortcodes/Callout.astro'; import LinkCard from '/src/components/shortcodes/LinkCard.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. ## Markdown in Astro @@ -100,8 +101,34 @@ 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: +### 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. + +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. + + + Yes, what you input is the ultimate answer to *life*, *the universe*, and **everything**. + + +```mdx + + Yes, what you input is the ultimate answer to *life*, *the universe*, and **everything**. + +``` + + + 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) + + +```mdx + + 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) + +``` ## Headings diff --git a/src/plugins/encrypt.ts b/src/plugins/encrypt.ts new file mode 100644 index 0000000..505a273 --- /dev/null +++ b/src/plugins/encrypt.ts @@ -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') + }; +} \ No newline at end of file