forked from jackyzha0/quartz
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: allow to set password-protected notes
- Loading branch information
Showing
40 changed files
with
589 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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` |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" | ||
import { i18n } from "../../i18n" | ||
|
||
const EncryptedContent: QuartzComponent = ({ encryptedContent, cfg }: QuartzComponentProps) => { | ||
return ( | ||
<> | ||
<div id="lock"> | ||
<div | ||
id="msg" | ||
data-wrong={i18n(cfg.locale).pages.encryptedContent.wrongPassword} | ||
data-modern={i18n(cfg.locale).pages.encryptedContent.modernBrowser} | ||
data-empty={i18n(cfg.locale).pages.encryptedContent.noPayload} | ||
> | ||
{i18n(cfg.locale).pages.encryptedContent.enterPassword} | ||
</div> | ||
<div id="load"> | ||
<p class="spinner"></p> | ||
<p id="load-text" data-decrypt={i18n(cfg.locale).pages.encryptedContent.decrypting}> | ||
{i18n(cfg.locale).pages.encryptedContent.loading} | ||
</p> | ||
</div> | ||
<form class="hidden"> | ||
<input | ||
type="password" | ||
class="pwd" | ||
name="pwd" | ||
aria-label={i18n(cfg.locale).pages.encryptedContent.password} | ||
autofocus | ||
/> | ||
<input type="submit" value={i18n(cfg.locale).pages.encryptedContent.submit} /> | ||
</form> | ||
<pre class="hidden" data-i={cfg.passProtected?.iteration}> | ||
{encryptedContent} | ||
</pre> | ||
</div> | ||
<article id="content"></article> | ||
</> | ||
) | ||
} | ||
|
||
export default (() => EncryptedContent) satisfies QuartzComponentConstructor |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
import { base64 } from "rfc4648" | ||
|
||
// @ts-ignore:next-line | ||
function find<T>(selector: string): T { | ||
const element = document.querySelector(selector) as T | ||
if (element) return element | ||
} | ||
|
||
let salt: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array, iterations: number | ||
const subtle = | ||
window.crypto?.subtle || | ||
(window.crypto as unknown as { webkitSubtle: Crypto["subtle"] })?.webkitSubtle | ||
|
||
let pl: HTMLPreElement, | ||
form: HTMLFormElement, | ||
pwd: HTMLInputElement, | ||
load: HTMLDivElement, | ||
loadText: HTMLElement, | ||
lock: HTMLDivElement, | ||
msg: HTMLParagraphElement, | ||
article: HTMLElement | ||
|
||
async function decryptHTML() { | ||
pl = find<HTMLPreElement>("pre[data-i]") | ||
form = find<HTMLFormElement>("form") | ||
pwd = find<HTMLInputElement>(".pwd") | ||
load = find<HTMLDivElement>("#load") | ||
loadText = find<HTMLElement>("#load-text") | ||
lock = find<HTMLDivElement>("#lock") | ||
msg = find<HTMLParagraphElement>("#msg") | ||
article = find<HTMLElement>("#content") | ||
|
||
if (!pl || !form || !pwd) { | ||
return | ||
} | ||
pwd.value = "" | ||
if (!subtle) { | ||
pwd.disabled = true | ||
error("modern") | ||
return | ||
} | ||
|
||
show(lock) | ||
if (!pl.innerHTML) { | ||
pwd.disabled = true | ||
error("empty") | ||
return | ||
} | ||
|
||
form.addEventListener("submit", async (event) => { | ||
event.preventDefault() | ||
await decrypt() | ||
}) | ||
|
||
iterations = Number(pl.dataset.i) | ||
const bytes = base64.parse(pl.innerHTML) | ||
salt = bytes.slice(0, 32) | ||
iv = bytes.slice(32, 32 + 16) | ||
ciphertext = bytes.slice(32 + 16) | ||
|
||
if (location.hash) { | ||
const parts = location.href.split("#") | ||
pwd.value = parts[1] | ||
history.replaceState(null, "", parts[0]) | ||
} | ||
|
||
if (sessionStorage[document.body.dataset.slug!] || pwd.value) { | ||
await decrypt() | ||
} else { | ||
hide(load) | ||
show(form) | ||
pwd.focus() | ||
} | ||
} | ||
|
||
document.addEventListener("nav", decryptHTML) | ||
|
||
function show(element: Element) { | ||
element.classList.remove("hidden") | ||
} | ||
|
||
function hide(element: Element) { | ||
element.classList.add("hidden") | ||
} | ||
|
||
function error(code: string) { | ||
msg.innerText = msg.getAttribute("data-" + code) || "" | ||
} | ||
|
||
async function sleep(milliseconds: number): Promise<void> { | ||
return new Promise((resolve) => setTimeout(resolve, milliseconds)) | ||
} | ||
|
||
async function decrypt() { | ||
loadText.innerText = loadText.getAttribute("data-decrypt") || "" | ||
show(load) | ||
hide(form) | ||
await sleep(60) | ||
|
||
try { | ||
const decrypted = await decryptFile({ salt, iv, ciphertext, iterations }, pwd.value) | ||
|
||
article.innerHTML = decrypted | ||
hide(lock) | ||
} catch (e) { | ||
hide(load) | ||
show(form) | ||
|
||
if (sessionStorage[document.body.dataset.slug!]) { | ||
sessionStorage.removeItem(document.body.dataset.slug!) | ||
} else { | ||
error("wrong") | ||
} | ||
|
||
pwd.value = "" | ||
pwd.focus() | ||
} | ||
} | ||
|
||
async function deriveKey( | ||
salt: Uint8Array, | ||
password: string, | ||
iterations: number, | ||
): Promise<CryptoKey> { | ||
const encoder = new TextEncoder() | ||
const baseKey = await subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, [ | ||
"deriveKey", | ||
]) | ||
return await subtle.deriveKey( | ||
{ name: "PBKDF2", salt, iterations, hash: "SHA-256" }, | ||
baseKey, | ||
{ name: "AES-GCM", length: 256 }, | ||
true, | ||
["decrypt"], | ||
) | ||
} | ||
|
||
async function importKey(key: JsonWebKey) { | ||
return subtle.importKey("jwk", key, "AES-GCM", true, ["decrypt"]) | ||
} | ||
|
||
async function decryptFile( | ||
{ | ||
salt, | ||
iv, | ||
ciphertext, | ||
iterations, | ||
}: { | ||
salt: Uint8Array | ||
iv: Uint8Array | ||
ciphertext: Uint8Array | ||
iterations: number | ||
}, | ||
password: string, | ||
) { | ||
const decoder = new TextDecoder() | ||
|
||
const key = sessionStorage[document.body.dataset.slug!] | ||
? await importKey(JSON.parse(sessionStorage[document.body.dataset.slug!])) | ||
: await deriveKey(salt, password, iterations) | ||
|
||
const data = new Uint8Array(await subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext)) | ||
if (!data) throw "Malformed data" | ||
|
||
sessionStorage[document.body.dataset.slug!] = JSON.stringify(await subtle.exportKey("jwk", key)) | ||
|
||
return decoder.decode(data) | ||
} |
Oops, something went wrong.