From cceac1a3fcf25be42ffd85e15a147c58f42b1936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20=C4=90=E1=BB=A9c=20Nam?= Date: Fri, 27 Dec 2024 14:51:14 +0700 Subject: [PATCH] feat: allow to set password-protected notes --- docs/configuration.md | 3 + docs/features/Password Protected.md | 32 ++++ package-lock.json | 7 + package.json | 1 + quartz.config.ts | 4 + quartz/cfg.ts | 9 + quartz/cli/handlers.js | 2 +- quartz/components/pages/EncryptedContent.tsx | 41 +++++ quartz/components/renderPage.tsx | 19 +- quartz/components/scripts/decrypt.inline.ts | 168 ++++++++++++++++++ quartz/components/styles/passProtected.scss | 30 ++++ quartz/components/types.ts | 1 + quartz/i18n/locales/ar-SA.ts | 10 ++ quartz/i18n/locales/ca-ES.ts | 11 ++ quartz/i18n/locales/cs-CZ.ts | 10 ++ quartz/i18n/locales/de-DE.ts | 12 ++ quartz/i18n/locales/definition.ts | 10 ++ quartz/i18n/locales/en-GB.ts | 10 ++ quartz/i18n/locales/en-US.ts | 10 ++ quartz/i18n/locales/es-ES.ts | 11 ++ quartz/i18n/locales/fa-IR.ts | 11 ++ quartz/i18n/locales/fr-FR.ts | 12 ++ quartz/i18n/locales/hu-HU.ts | 11 ++ quartz/i18n/locales/it-IT.ts | 11 ++ quartz/i18n/locales/ja-JP.ts | 12 ++ quartz/i18n/locales/ko-KR.ts | 10 ++ quartz/i18n/locales/nl-NL.ts | 12 ++ quartz/i18n/locales/pl-PL.ts | 10 ++ quartz/i18n/locales/pt-BR.ts | 10 ++ quartz/i18n/locales/ro-RO.ts | 11 ++ quartz/i18n/locales/ru-RU.ts | 11 ++ quartz/i18n/locales/tr-TR.ts | 11 ++ quartz/i18n/locales/uk-UA.ts | 10 ++ quartz/i18n/locales/vi-VN.ts | 10 ++ quartz/i18n/locales/zh-CN.ts | 10 ++ quartz/i18n/locales/zh-TW.ts | 10 ++ quartz/plugins/emitters/404.tsx | 2 +- quartz/plugins/emitters/componentResources.ts | 8 + quartz/plugins/emitters/contentPage.tsx | 2 +- quartz/plugins/emitters/folderPage.tsx | 2 +- quartz/plugins/emitters/tagPage.tsx | 2 +- quartz/util/encrypt.ts | 33 ++++ 42 files changed, 614 insertions(+), 8 deletions(-) create mode 100644 docs/features/Password Protected.md create mode 100644 quartz/components/pages/EncryptedContent.tsx create mode 100644 quartz/components/scripts/decrypt.inline.ts create mode 100644 quartz/components/styles/passProtected.scss create mode 100644 quartz/util/encrypt.ts diff --git a/docs/configuration.md b/docs/configuration.md index 1622da6fa414a..37ef040b52abc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -24,6 +24,9 @@ This part of the configuration concerns anything that can affect the whole site. - `pageTitleSuffix`: a string added to the end of the page title. This only applies to the browser tab title, not the title shown at the top of the page. - `enableSPA`: whether to enable [[SPA Routing]] on your site. - `enablePopovers`: whether to enable [[popover previews]] on your site. +- `passProtected`: what to use [[Password Protected]] on your site. + - `enabled`: whether to enable password protected + - `iteration`: iteration of key derivation, default is `2e6` - `analytics`: what to use for analytics on your site. Values can be - `null`: don't use analytics; - `{ provider: 'google', tagId: '' }`: use Google Analytics; diff --git a/docs/features/Password Protected.md b/docs/features/Password Protected.md new file mode 100644 index 0000000000000..c98ef3848224c --- /dev/null +++ b/docs/features/Password Protected.md @@ -0,0 +1,32 @@ +--- +title: Password Protected +--- + +Some notes may be sensitive, i.e. non-public personal projects, contacts, meeting notes and such. It would be really useful to be able to protect some pages or group of pages so they don't appear to everyone, while still allowing them to be published. + +By adding a password to your note's frontmatter, you can create an extra layer of security, ensuring that only authorized individuals can access your content. Whether you're safeguarding personal journals, project plans, this feature provides the peace of mind you need. + +## How it works + +Simply add a password field to your note's frontmatter and set your desired password. When you try to view the note, you'll be prompted to enter the password. If the password is correct, the note will be unlocked. Once unlocked, you can access the note until you clear your browser cookies. + +### Security techniques + +- Key Derivation: Utilizes PBKDF2 for generating secure encryption keys. +- Unique Salt for Each Encryption: A unique salt is generated every time the encrypt method is used, enhancing security. +- Encryption/Decryption: Implements AES-GCM for robust data encryption and decryption. +- Encoding/Decoding: Use base64 to convert non-textual encrypted data in HTML + +### Disclaimer + +- Use it at your own risk +- You need to choose a strong password and share it only to trusted users +- You need to secure your notes and Quartz repository in private mode on Github/Gitlab/Bitbucket... or use your own Git server +- You can use other WAF tools to enhance security, based on URL of notes that Quartz build for you, e.g. Cloudflare WAF, AWS WAF, Google Cloud Armor... + +## Configuration + +- Enable password protected notes: set the `passwordProtected.enabled` field in `quartz.config.ts` to be `true`. +- Change iteration count of key derivation: set the `passwordProtected.iteration` filed in `quartz.config.ts` to any bigger than 2e6. +- Style: `quartz/components/styles/passwordProtected.scss` +- Script: `quartz/components/scripts/decrypt.inline.ts` diff --git a/package-lock.json b/package-lock.json index 03f757d6622da..b19637d30df44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", + "rfc4648": "^1.5.3", "rfdc": "^1.4.1", "rimraf": "^6.0.1", "satori": "^0.12.0", @@ -6502,6 +6503,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfc4648": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.3.tgz", + "integrity": "sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==", + "license": "MIT" + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", diff --git a/package.json b/package.json index 42714c1fa3212..c9f3cdc098db6 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", + "rfc4648": "^1.5.3", "rfdc": "^1.4.1", "rimraf": "^6.0.1", "satori": "^0.12.0", diff --git a/quartz.config.ts b/quartz.config.ts index dc339d987f1c2..f9ea2f545eaac 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -20,6 +20,10 @@ const config: QuartzConfig = { ignorePatterns: ["private", "templates", ".obsidian"], defaultDateType: "created", generateSocialImages: false, + passProtected: { + enabled: false, + iteration: 2e6, + }, theme: { fontOrigin: "googleFonts", cdnCaching: true, diff --git a/quartz/cfg.ts b/quartz/cfg.ts index 135f584994a6d..08d66ca689683 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -44,6 +44,13 @@ export type Analytics = projectId?: string } +export type PassProtected = { + /** Whether to enable password protected page rendering */ + enabled: boolean + /** Iteration of derived key to encrypt page */ + iteration: number +} + export interface GlobalConfiguration { pageTitle: string pageTitleSuffix?: string @@ -57,6 +64,8 @@ export interface GlobalConfiguration { ignorePatterns: string[] /** Whether to use created, modified, or published as the default type of date */ defaultDateType: ValidDateType + /** Password protected page rendering */ + passProtected: PassProtected /** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL. * Quartz will avoid using this as much as possible and use relative URLs most of the time */ diff --git a/quartz/cli/handlers.js b/quartz/cli/handlers.js index 6b23d8010b281..c8f509b9b3c3b 100644 --- a/quartz/cli/handlers.js +++ b/quartz/cli/handlers.js @@ -250,7 +250,7 @@ export async function handleBuild(argv) { // remove default exports that we manually inserted text = text.replace("export default", "") - text = text.replace("export", "") + text = text.replace("export ", "") const sourcefile = path.relative(path.resolve("."), args.path) const resolveDir = path.dirname(sourcefile) diff --git a/quartz/components/pages/EncryptedContent.tsx b/quartz/components/pages/EncryptedContent.tsx new file mode 100644 index 0000000000000..5d98447dd0d66 --- /dev/null +++ b/quartz/components/pages/EncryptedContent.tsx @@ -0,0 +1,41 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" +import { i18n } from "../../i18n" + +const EncryptedContent: QuartzComponent = ({ encryptedContent, cfg }: QuartzComponentProps) => { + return ( + <> +
+
+ {i18n(cfg.locale).pages.encryptedContent.enterPassword} +
+
+

+

+ {i18n(cfg.locale).pages.encryptedContent.loading} +

+
+ + +
+
+ + ) +} + +export default (() => EncryptedContent) satisfies QuartzComponentConstructor diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index 9c530967bc2d1..fa033d64e7c1f 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -2,7 +2,9 @@ import { render } from "preact-render-to-string" import { QuartzComponent, QuartzComponentProps } from "./types" import HeaderConstructor from "./Header" import BodyConstructor from "./Body" +import EncryptedContent from "./pages/EncryptedContent" import { JSResourceToScriptElement, StaticResources } from "../util/resources" +import { getEncryptedPayload } from "../util/encrypt" import { clone, FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path" import { visit } from "unist-util-visit" import { Root, Element, ElementContent } from "hast" @@ -58,13 +60,13 @@ export function pageResources( } } -export function renderPage( +export async function renderPage( cfg: GlobalConfiguration, slug: FullSlug, componentData: QuartzComponentProps, components: RenderComponents, pageResources: StaticResources, -): string { +): Promise { // make a deep copy of the tree so we don't remove the transclusion references // for the file cached in contentMap in build.ts const root = clone(componentData.tree) as Root @@ -200,6 +202,7 @@ export function renderPage( } = components const Header = HeaderConstructor() const Body = BodyConstructor() + const Encrypted = EncryptedContent() const LeftComponent = ( - + {content}