mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-21 20:04:06 -06:00
Clean up the plugin configuration types and make fields nullable
This commit is contained in:
parent
78d4fb5faa
commit
127e5c3c03
@ -17,132 +17,90 @@ This plugin enables content encryption for sensitive pages in your Quartz site.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
Plugin.Encrypt({
|
Plugin.Encrypt({
|
||||||
algorithm: "aes-256-cbc", // Encryption algorithm (key length auto-inferred)
|
algorithm: "aes-256-cbc", // Encryption algorithm
|
||||||
|
ttl: 3600 * 24 * 7, // Password cache TTL in seconds (7 days)
|
||||||
|
message: "This content is encrypted.", // Default message shown
|
||||||
encryptedFolders: {
|
encryptedFolders: {
|
||||||
// Folder-level encryption with simple passwords
|
// Simple password for a folder
|
||||||
"private/": "folder-password",
|
"private/": "folder-password",
|
||||||
"work/confidential/": "work-password",
|
|
||||||
// Advanced per-folder configuration
|
// Advanced configuration for a folder
|
||||||
"secure/": {
|
"secure/": {
|
||||||
password: "advanced-password",
|
password: "advanced-password",
|
||||||
algorithm: "aes-256-gcm",
|
algorithm: "aes-256-gcm",
|
||||||
ttl: 3600 * 24 * 30, // 30 days
|
ttl: 3600 * 24 * 30, // 30 days
|
||||||
|
message: "Authorized access only",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ttl: 3600 * 24 * 7, // Password cache TTL in seconds (7 days)
|
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!warning]
|
> [!warning]
|
||||||
> It is very important to note that:
|
> Important security notes:
|
||||||
>
|
>
|
||||||
> - All non-markdown files will be left unencrypted in the final build.
|
> - All non-markdown files remain unencrypted in the final build
|
||||||
> - Marking something as encrypted will only encrypt the page in the final build. The file's content can still be viewed if your repository is public.
|
> - Encrypted content is still visible in your source repository if it's public
|
||||||
|
> - Use this for access control, not for storing highly sensitive secrets
|
||||||
|
|
||||||
### Configuration Options
|
### Configuration Options
|
||||||
|
|
||||||
- `algorithm`: Encryption algorithm to use. Supported values:
|
- `algorithm`: Encryption algorithm to use
|
||||||
- `"aes-256-cbc"` (default): AES-256 in CBC mode
|
- `"aes-256-cbc"` (default): AES-256 in CBC mode
|
||||||
- `"aes-256-gcm"`: AES-256 in GCM mode (authenticated encryption)
|
- `"aes-256-gcm"`: AES-256 in GCM mode (authenticated encryption)
|
||||||
- `"aes-256-ecb"`: AES-256 in ECB mode (not recommended for security)
|
- `"aes-256-ecb"`: AES-256 in ECB mode (not recommended)
|
||||||
- Key length is automatically inferred from the algorithm (e.g., 256-bit = 32 bytes)
|
- `ttl`: Time-to-live for cached passwords in seconds (default: 7 days)
|
||||||
- `encryptedFolders`: Object mapping folder paths to passwords or configuration objects for folder-level encryption
|
- `message`: Default message shown on encrypted pages
|
||||||
- `ttl`: Time-to-live for cached passwords in seconds (default: 604800 = 7 days, set to 0 for session-only)
|
- `encryptedFolders`: Configure folder-level encryption
|
||||||
- `message`: Message to be displayed in the decryption page
|
|
||||||
|
|
||||||
### Advanced Folder Configuration
|
## How Configuration Works
|
||||||
|
|
||||||
You can provide detailed configuration for each encrypted folder:
|
### Configuration Inheritance
|
||||||
|
|
||||||
|
Settings cascade down through your folder structure:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
encryptedFolders: {
|
encryptedFolders: {
|
||||||
"basic/": "simple-password", // Simple string password
|
"docs/": {
|
||||||
"advanced/": {
|
password: "docs-password",
|
||||||
password: "complex-password",
|
algorithm: "aes-256-gcm"
|
||||||
algorithm: "aes-256-gcm", // Override global algorithm
|
},
|
||||||
ttl: 3600 * 24 * 30, // Override global TTL (30 days)
|
"docs/internal/": {
|
||||||
message: "This content is encrypted", // Message to be displayed
|
password: "internal-password"
|
||||||
|
// Inherits algorithm from parent folder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
In this example:
|
||||||
|
|
||||||
### Folder-level Encryption
|
- `docs/page.md` uses `"docs-password"` with `"aes-256-gcm"`
|
||||||
|
- `docs/internal/report.md` uses `"internal-password"` but still uses `"aes-256-gcm"` (inherited)
|
||||||
|
|
||||||
Configure folders to be encrypted by adding them to the `encryptedFolders` option:
|
### Configuration Priority
|
||||||
|
|
||||||
```typescript
|
When multiple configurations apply, the priority is:
|
||||||
Plugin.Encrypt({
|
|
||||||
encryptedFolders: {
|
|
||||||
"private/": "my-secret-password",
|
|
||||||
"work/": "work-password",
|
|
||||||
"personal/diary/": "diary-password",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
All pages within these folders will be encrypted with the specified password. Nested folders inherit passwords from parent folders, with deeper paths taking precedence.
|
1. **Page frontmatter** (highest priority)
|
||||||
|
2. **Deepest matching folder**
|
||||||
### Page-level Encryption
|
3. **Parent folders** (inherited settings)
|
||||||
|
4. **Global defaults** (lowest priority)
|
||||||
Use frontmatter to encrypt individual pages or override folder passwords.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
title: "My Secret Page"
|
|
||||||
password: "page-specific-password"
|
|
||||||
encryptConfig:
|
|
||||||
password: "password-also-allowed-here"
|
|
||||||
message: "Sorry, this one is only for my eyes"
|
|
||||||
algorithm: "aes-256-gcm" # Optional: override algorithm
|
|
||||||
ttl: 86400 # Optional: override TTL (1 day)
|
|
||||||
---
|
|
||||||
This content will be encrypted and require a password to view.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontmatter Fields
|
|
||||||
|
|
||||||
#### encryptConfig object fields:
|
|
||||||
|
|
||||||
- `password`: (required) The password required to decrypt this page
|
|
||||||
- `message`: (optional) Custom message to show on the unlock page
|
|
||||||
- `algorithm`: (optional) Override the encryption algorithm for this page
|
|
||||||
- `ttl`: (optional) Override password cache TTL for this page
|
|
||||||
|
|
||||||
#### Legacy fields (still supported):
|
|
||||||
|
|
||||||
- `encrypt`: Set to `true` to enable encryption for this page
|
|
||||||
- `password`: The password required to decrypt this page
|
|
||||||
|
|
||||||
If a page is in an encrypted folder but has its own password configuration, the page-specific settings will be used instead of the folder settings.
|
|
||||||
|
|
||||||
### Security Considerations
|
|
||||||
|
|
||||||
- Use strong passwords for sensitive content
|
|
||||||
- Consider using AES-256-GCM mode for authenticated encryption
|
|
||||||
- ECB mode is provided for compatibility but is not recommended for security-critical applications
|
|
||||||
- Key lengths are automatically determined by the algorithm (no manual configuration needed)
|
|
||||||
|
|
||||||
## Security Features
|
## Security Features
|
||||||
|
|
||||||
### Password Cache
|
### Password Caching
|
||||||
|
|
||||||
The plugin implements intelligent password caching:
|
- Passwords are stored in browser localStorage
|
||||||
|
- Automatic expiration based on TTL settings
|
||||||
|
- Cached passwords are tried automatically when navigating
|
||||||
|
|
||||||
- Passwords are cached in browser localStorage
|
### Protection Levels
|
||||||
- Configurable TTL (time-to-live) for automatic cache expiration
|
|
||||||
- Passwords are automatically tried when navigating to encrypted pages
|
|
||||||
|
|
||||||
### Content Protection
|
- **Content**: Entire page HTML is encrypted
|
||||||
|
- **Search/RSS**: Only generic descriptions are exposed
|
||||||
- **Full Content Encryption**: The entire HTML content is encrypted, not just hidden
|
- **Navigation**: Encrypted pages appear in navigation but require passwords to view
|
||||||
- **SEO Protection**: Search engines and RSS feeds see generic placeholder descriptions
|
|
||||||
- **Client-side Decryption**: Content is never transmitted in plain text
|
|
||||||
- **Password Verification**: Fast password hash verification before attempting decryption
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
- Category: Transformer
|
- Category: Transformer
|
||||||
- Function name: `Plugin.Encrypt()`.
|
- Function name: `Plugin.Encrypt()`
|
||||||
- Source: [`quartz/plugins/transformers/encrypt.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/encrypt.ts).
|
- Source: [`quartz/plugins/transformers/encrypt.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/encrypt.ts)
|
||||||
|
|||||||
1
index.d.ts
vendored
1
index.d.ts
vendored
@ -8,6 +8,7 @@ interface CustomEventMap {
|
|||||||
prenav: CustomEvent<{}>
|
prenav: CustomEvent<{}>
|
||||||
nav: CustomEvent<{ url: FullSlug }>
|
nav: CustomEvent<{ url: FullSlug }>
|
||||||
render: CustomEvent<{ htmlElement: HTMLElement }>
|
render: CustomEvent<{ htmlElement: HTMLElement }>
|
||||||
|
decrypt: CustomEvent<{ filePath: FullSlug; password: string }>
|
||||||
themechange: CustomEvent<{ theme: "light" | "dark" }>
|
themechange: CustomEvent<{ theme: "light" | "dark" }>
|
||||||
readermodechange: CustomEvent<{ mode: "on" | "off" }>
|
readermodechange: CustomEvent<{ mode: "on" | "off" }>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -121,7 +121,6 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</div>
|
</div>
|
||||||
<template id="template-file">
|
<template id="template-file">
|
||||||
<li>
|
<li>
|
||||||
<span class="file-title"></span>
|
|
||||||
<a href="#"></a>
|
<a href="#"></a>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@ -144,7 +143,6 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<button class="folder-button">
|
<button class="folder-button">
|
||||||
<span class="folder-title folder-title-icon"></span>
|
|
||||||
<span class="folder-title folder-title-text"></span>
|
<span class="folder-title folder-title-text"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -54,14 +54,13 @@ export default ((opts?: Partial<Options>) => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<OverflowList class={fileData.collapseToc ? "collapsed toc-content" : "toc-content"}>
|
<OverflowList class={fileData.collapseToc ? "collapsed toc-content" : "toc-content"}>
|
||||||
{!fileData.encryptionResult &&
|
{fileData.toc.map((tocEntry) => (
|
||||||
fileData.toc.map((tocEntry) => (
|
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
||||||
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
|
||||||
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
|
{tocEntry.text}
|
||||||
{tocEntry.text}
|
</a>
|
||||||
</a>
|
</li>
|
||||||
</li>
|
))}
|
||||||
))}
|
|
||||||
</OverflowList>
|
</OverflowList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -71,7 +70,7 @@ export default ((opts?: Partial<Options>) => {
|
|||||||
TableOfContents.afterDOMLoaded = concatenateResources(script, overflowListAfterDOMLoaded)
|
TableOfContents.afterDOMLoaded = concatenateResources(script, overflowListAfterDOMLoaded)
|
||||||
|
|
||||||
const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => {
|
const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => {
|
||||||
if (!fileData.toc) {
|
if (!fileData.toc || fileData.encryptionResult) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import {
|
|||||||
verifyPasswordHash,
|
verifyPasswordHash,
|
||||||
addPasswordToCache,
|
addPasswordToCache,
|
||||||
Hash,
|
Hash,
|
||||||
EncryptionConfig,
|
CompleteCryptoConfig,
|
||||||
searchForValidPassword,
|
searchForValidPassword,
|
||||||
EncryptionResult,
|
EncryptionResult,
|
||||||
} from "../../util/encryption"
|
} from "../../util/encryption"
|
||||||
@ -25,6 +25,16 @@ const showLoading = (container: Element, show: boolean) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dispatchDecryptEvent(filePath: FullSlug, password: string) {
|
||||||
|
const event = new CustomEvent("decrypt", {
|
||||||
|
detail: {
|
||||||
|
filePath,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
document.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
const decryptWithPassword = async (
|
const decryptWithPassword = async (
|
||||||
container: Element,
|
container: Element,
|
||||||
password: string,
|
password: string,
|
||||||
@ -33,7 +43,7 @@ const decryptWithPassword = async (
|
|||||||
const errorDivs = container.querySelectorAll(".decrypt-error") as NodeListOf<HTMLElement>
|
const errorDivs = container.querySelectorAll(".decrypt-error") as NodeListOf<HTMLElement>
|
||||||
const containerElement = container as HTMLElement
|
const containerElement = container as HTMLElement
|
||||||
|
|
||||||
const config = JSON.parse(containerElement.dataset.config!) as EncryptionConfig
|
const config = JSON.parse(containerElement.dataset.config!) as CompleteCryptoConfig
|
||||||
const encrypted = JSON.parse(containerElement.dataset.encrypted!) as EncryptionResult
|
const encrypted = JSON.parse(containerElement.dataset.encrypted!) as EncryptionResult
|
||||||
const hash = JSON.parse(containerElement.dataset.hash!) as Hash
|
const hash = JSON.parse(containerElement.dataset.hash!) as Hash
|
||||||
|
|
||||||
@ -137,13 +147,14 @@ function updateTitle(container: HTMLElement | null) {
|
|||||||
|
|
||||||
const tryAutoDecrypt = async (parent: HTMLElement, container: HTMLElement): Promise<boolean> => {
|
const tryAutoDecrypt = async (parent: HTMLElement, container: HTMLElement): Promise<boolean> => {
|
||||||
const fullSlug = container.dataset.slug as FullSlug
|
const fullSlug = container.dataset.slug as FullSlug
|
||||||
const config = JSON.parse(container.dataset.config!) as EncryptionConfig
|
const config = JSON.parse(container.dataset.config!) as CompleteCryptoConfig
|
||||||
const hash = JSON.parse(container.dataset.hash!) as Hash
|
const hash = JSON.parse(container.dataset.hash!) as Hash
|
||||||
|
|
||||||
const password = await searchForValidPassword(fullSlug, hash, config)
|
const password = await searchForValidPassword(fullSlug, hash, config)
|
||||||
|
|
||||||
if (password && (await decryptWithPassword(container, password, false))) {
|
if (password && (await decryptWithPassword(container, password, false))) {
|
||||||
dispatchRenderEvent(parent)
|
dispatchRenderEvent(parent)
|
||||||
|
dispatchDecryptEvent(fullSlug, password)
|
||||||
updateTitle(parent)
|
updateTitle(parent)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -151,6 +162,7 @@ const tryAutoDecrypt = async (parent: HTMLElement, container: HTMLElement): Prom
|
|||||||
}
|
}
|
||||||
|
|
||||||
const manualDecrypt = async (parent: HTMLElement, container: HTMLElement) => {
|
const manualDecrypt = async (parent: HTMLElement, container: HTMLElement) => {
|
||||||
|
const fullSlug = container.dataset.slug as FullSlug
|
||||||
const passwordInput = container.querySelector(".decrypt-password") as HTMLInputElement
|
const passwordInput = container.querySelector(".decrypt-password") as HTMLInputElement
|
||||||
const password = passwordInput.value
|
const password = passwordInput.value
|
||||||
|
|
||||||
@ -161,6 +173,7 @@ const manualDecrypt = async (parent: HTMLElement, container: HTMLElement) => {
|
|||||||
|
|
||||||
if (await decryptWithPassword(container, password, true)) {
|
if (await decryptWithPassword(container, password, true)) {
|
||||||
dispatchRenderEvent(parent)
|
dispatchRenderEvent(parent)
|
||||||
|
dispatchDecryptEvent(fullSlug, password)
|
||||||
updateTitle(parent)
|
updateTitle(parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,18 +87,15 @@ function createFileNode(currentSlug: FullSlug, node: FileTrieNode): HTMLLIElemen
|
|||||||
const a = li.querySelector("a") as HTMLAnchorElement
|
const a = li.querySelector("a") as HTMLAnchorElement
|
||||||
a.href = resolveRelative(currentSlug, node.slug)
|
a.href = resolveRelative(currentSlug, node.slug)
|
||||||
a.dataset.for = node.slug
|
a.dataset.for = node.slug
|
||||||
a.textContent = node.displayName
|
|
||||||
|
|
||||||
const span = li.querySelector("span") as HTMLSpanElement
|
if (node.data?.encryptionResult) {
|
||||||
|
a.textContent = "🔒 " + node.displayName
|
||||||
if (span && node.data?.encryptionResult) {
|
|
||||||
span.textContent = "🔒 "
|
|
||||||
|
|
||||||
contentDecryptedEventListener(node.slug, node.data.hash!, node.data.encryptionConfig!, () => {
|
contentDecryptedEventListener(node.slug, node.data.hash!, node.data.encryptionConfig!, () => {
|
||||||
span.textContent = "🔓 "
|
a.textContent = "🔓 " + node.displayName
|
||||||
})
|
})
|
||||||
} else if (span) {
|
} else {
|
||||||
span.remove()
|
a.textContent = node.displayName
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSlug === node.slug) {
|
if (currentSlug === node.slug) {
|
||||||
@ -124,14 +121,7 @@ function createFolderNode(
|
|||||||
const folderPath = node.slug
|
const folderPath = node.slug
|
||||||
folderContainer.dataset.folderpath = folderPath
|
folderContainer.dataset.folderpath = folderPath
|
||||||
|
|
||||||
const span = titleContainer.querySelector("span.folder-title-icon") as HTMLElement
|
let titleElement: HTMLElement
|
||||||
|
|
||||||
if (span && node.data?.encryptionResult) {
|
|
||||||
span.textContent = "🔒 "
|
|
||||||
contentDecryptedEventListener(folderPath, node.data.hash!, node.data.encryptionConfig!, () => {
|
|
||||||
span.textContent = "🔓 "
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.folderClickBehavior === "link") {
|
if (opts.folderClickBehavior === "link") {
|
||||||
// Replace button with link for link behavior
|
// Replace button with link for link behavior
|
||||||
@ -140,12 +130,20 @@ function createFolderNode(
|
|||||||
a.href = resolveRelative(currentSlug, folderPath)
|
a.href = resolveRelative(currentSlug, folderPath)
|
||||||
a.dataset.for = folderPath
|
a.dataset.for = folderPath
|
||||||
a.className = "folder-title"
|
a.className = "folder-title"
|
||||||
a.textContent = node.displayName
|
titleElement = a
|
||||||
button.replaceWith(a)
|
button.replaceWith(a)
|
||||||
titleContainer.insertBefore(span, a)
|
|
||||||
} else {
|
} else {
|
||||||
const span = titleContainer.querySelector(".folder-title-text") as HTMLElement
|
const span = titleContainer.querySelector(".folder-title-text") as HTMLElement
|
||||||
span.textContent = node.displayName
|
titleElement = span
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.data?.encryptionResult) {
|
||||||
|
titleElement.textContent = "🔒 " + node.displayName
|
||||||
|
contentDecryptedEventListener(folderPath, node.data.hash!, node.data.encryptionConfig!, () => {
|
||||||
|
titleElement.textContent = "🔓 " + node.displayName
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
titleElement.textContent = node.displayName
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the saved state is collapsed or the default state is collapsed
|
// if the saved state is collapsed or the default state is collapsed
|
||||||
|
|||||||
@ -49,9 +49,9 @@
|
|||||||
font-family: var(--bodyFont);
|
font-family: var(--bodyFont);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
border: 1px solid var(--lightgray);
|
border: 1px solid var(--lightgray);
|
||||||
background: var(--light);
|
|
||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
transition: border-color 0.2s ease;
|
transition: border-color 0.2s ease;
|
||||||
|
// background: var(--backgroundPrimary);
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { QuartzEmitterPlugin } from "../types"
|
|||||||
import { toHtml } from "hast-util-to-html"
|
import { toHtml } from "hast-util-to-html"
|
||||||
import { write } from "./helpers"
|
import { write } from "./helpers"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
import { Hash, EncryptionResult, EncryptionConfig } from "../../util/encryption"
|
import { Hash, EncryptionResult, CompleteCryptoConfig } from "../../util/encryption"
|
||||||
|
|
||||||
export type ContentIndexMap = Map<FullSlug, ContentDetails>
|
export type ContentIndexMap = Map<FullSlug, ContentDetails>
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ export type ContentDetails = {
|
|||||||
richContent?: string
|
richContent?: string
|
||||||
date?: Date
|
date?: Date
|
||||||
description?: string
|
description?: string
|
||||||
encryptionConfig?: EncryptionConfig
|
encryptionConfig?: CompleteCryptoConfig
|
||||||
hash?: Hash
|
hash?: Hash
|
||||||
encryptionResult?: EncryptionResult
|
encryptionResult?: EncryptionResult
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,78 +2,238 @@ import { QuartzTransformerPlugin } from "../types"
|
|||||||
import { Root } from "hast"
|
import { Root } from "hast"
|
||||||
import { toHtml } from "hast-util-to-html"
|
import { toHtml } from "hast-util-to-html"
|
||||||
import { fromHtml } from "hast-util-from-html"
|
import { fromHtml } from "hast-util-from-html"
|
||||||
import { toString } from "hast-util-to-string"
|
|
||||||
import { VFile } from "vfile"
|
import { VFile } from "vfile"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
import {
|
import {
|
||||||
EncryptionOptions,
|
|
||||||
DirectoryConfig,
|
|
||||||
defaultEncryptionConfig,
|
|
||||||
SUPPORTED_ALGORITHMS,
|
SUPPORTED_ALGORITHMS,
|
||||||
SupportedEncryptionAlgorithm,
|
|
||||||
encryptContent,
|
encryptContent,
|
||||||
getEncryptionConfigForPath,
|
|
||||||
Hash,
|
Hash,
|
||||||
hashString,
|
hashString,
|
||||||
EncryptionResult,
|
EncryptionResult,
|
||||||
|
CompleteCryptoConfig,
|
||||||
|
BaseCryptoConfig,
|
||||||
EncryptionConfig,
|
EncryptionConfig,
|
||||||
|
DirectoryConfig,
|
||||||
} from "../../util/encryption"
|
} from "../../util/encryption"
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import encryptScript from "../../components/scripts/encrypt.inline.ts"
|
import encryptScript from "../../components/scripts/encrypt.inline.ts"
|
||||||
import encryptStyle from "../../components/styles/encrypt.scss"
|
import encryptStyle from "../../components/styles/encrypt.scss"
|
||||||
|
|
||||||
export interface Options extends EncryptionOptions {}
|
// =============================================================================
|
||||||
|
// TYPE DEFINITIONS
|
||||||
const defaultOptions: Options = {
|
// =============================================================================
|
||||||
...defaultEncryptionConfig,
|
// Plugin configuration
|
||||||
encryptedFolders: {},
|
export interface PluginConfig extends CompleteCryptoConfig {
|
||||||
|
encryptedFolders: Record<string, DirectoryConfig>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Encrypt: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
// User-provided options (all optional)
|
||||||
const opts = { ...defaultOptions, ...userOpts }
|
export type PluginOptions = Partial<PluginConfig>
|
||||||
|
|
||||||
// Validate algorithm at build time
|
// Internal normalized folder configuration
|
||||||
if (
|
interface NormalizedFolderConfig extends CompleteCryptoConfig {
|
||||||
opts.algorithm &&
|
password: string
|
||||||
!SUPPORTED_ALGORITHMS.includes(opts.algorithm as SupportedEncryptionAlgorithm)
|
path: string
|
||||||
) {
|
depth: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONFIGURATION MANAGEMENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: CompleteCryptoConfig = {
|
||||||
|
algorithm: "aes-256-cbc",
|
||||||
|
ttl: 3600 * 24 * 7, // 1 week
|
||||||
|
message: "This content is encrypted.",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a directory configuration into a complete configuration object
|
||||||
|
*/
|
||||||
|
function normalizeDirectoryConfig(
|
||||||
|
dirConfig: DirectoryConfig,
|
||||||
|
defaults: CompleteCryptoConfig,
|
||||||
|
): EncryptionConfig {
|
||||||
|
if (typeof dirConfig === "string") {
|
||||||
|
return {
|
||||||
|
...defaults,
|
||||||
|
password: dirConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
algorithm: dirConfig.algorithm ?? defaults.algorithm,
|
||||||
|
ttl: dirConfig.ttl ?? defaults.ttl,
|
||||||
|
message: dirConfig.message ?? defaults.message,
|
||||||
|
password: dirConfig.password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a sorted list of folder configurations by path depth
|
||||||
|
* This ensures parent configurations are processed before children
|
||||||
|
*/
|
||||||
|
function createFolderConfigHierarchy(
|
||||||
|
folders: Record<string, DirectoryConfig>,
|
||||||
|
globalDefaults: CompleteCryptoConfig,
|
||||||
|
): NormalizedFolderConfig[] {
|
||||||
|
const configs: NormalizedFolderConfig[] = []
|
||||||
|
|
||||||
|
for (const [path, config] of Object.entries(folders)) {
|
||||||
|
const normalized = normalizeDirectoryConfig(config, globalDefaults)
|
||||||
|
configs.push({
|
||||||
|
...normalized,
|
||||||
|
path: path.endsWith("/") ? path : path + "/",
|
||||||
|
depth: path.split("/").filter(Boolean).length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by depth (shallow to deep) to ensure proper inheritance
|
||||||
|
return configs.sort((a, b) => a.depth - b.depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges configurations following the inheritance chain
|
||||||
|
* Deeper paths override shallower ones
|
||||||
|
*/
|
||||||
|
function mergeConfigurations(
|
||||||
|
base: CompleteCryptoConfig,
|
||||||
|
override: Partial<BaseCryptoConfig>,
|
||||||
|
): CompleteCryptoConfig {
|
||||||
|
return {
|
||||||
|
algorithm: override.algorithm ?? base.algorithm,
|
||||||
|
ttl: override.ttl ?? base.ttl,
|
||||||
|
message: override.message ?? base.message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the encryption configuration for a specific file path
|
||||||
|
* Respects the directory hierarchy and inheritance
|
||||||
|
*/
|
||||||
|
function getConfigurationForPath(
|
||||||
|
filePath: string,
|
||||||
|
folderConfigs: NormalizedFolderConfig[],
|
||||||
|
globalDefaults: CompleteCryptoConfig,
|
||||||
|
): EncryptionConfig | undefined {
|
||||||
|
let currentConfig: CompleteCryptoConfig = globalDefaults
|
||||||
|
let password: string | undefined
|
||||||
|
|
||||||
|
// Apply configurations in order (shallow to deep)
|
||||||
|
for (const folderConfig of folderConfigs) {
|
||||||
|
if (filePath.startsWith(folderConfig.path)) {
|
||||||
|
// Merge the configuration, inheriting from parent
|
||||||
|
currentConfig = mergeConfigurations(currentConfig, folderConfig)
|
||||||
|
password = folderConfig.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no password was found in any matching folder, return undefined
|
||||||
|
if (!password) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentConfig,
|
||||||
|
password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the plugin configuration
|
||||||
|
*/
|
||||||
|
function validateConfig(config: PluginConfig): void {
|
||||||
|
if (!SUPPORTED_ALGORITHMS.includes(config.algorithm)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[EncryptPlugin] Unsupported encryption algorithm: ${opts.algorithm}. Supported algorithms: ${SUPPORTED_ALGORITHMS.join(", ")}`,
|
`[EncryptPlugin] Unsupported encryption algorithm: ${config.algorithm}. ` +
|
||||||
|
`Supported algorithms: ${SUPPORTED_ALGORITHMS.join(", ")}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEncryptionConfig = (file: VFile): DirectoryConfig | undefined => {
|
if (config.ttl <= 0) {
|
||||||
|
throw new Error(`[EncryptPlugin] TTL must be a positive number`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PLUGIN IMPLEMENTATION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const Encrypt: QuartzTransformerPlugin<PluginOptions> = (userOpts) => {
|
||||||
|
// Merge user options with defaults
|
||||||
|
const pluginConfig: PluginConfig = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
...userOpts,
|
||||||
|
encryptedFolders: userOpts?.encryptedFolders ?? {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate configuration
|
||||||
|
validateConfig(pluginConfig)
|
||||||
|
|
||||||
|
// Pre-process folder configurations for efficient lookup
|
||||||
|
const folderConfigs = createFolderConfigHierarchy(pluginConfig.encryptedFolders, pluginConfig)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the final encryption configuration for a file
|
||||||
|
* Priority: frontmatter > deepest matching folder > global defaults
|
||||||
|
*/
|
||||||
|
const getEncryptionConfig = (file: VFile): EncryptionConfig | undefined => {
|
||||||
const frontmatter = file.data?.frontmatter
|
const frontmatter = file.data?.frontmatter
|
||||||
const frontmatterConfig = (frontmatter?.encryptConfig ?? {}) as DirectoryConfig
|
|
||||||
const relativePath = file.data?.relativePath
|
const relativePath = file.data?.relativePath
|
||||||
|
|
||||||
const folderConfig = relativePath ? getEncryptionConfigForPath(relativePath, opts) : null
|
// Check if file should be encrypted via frontmatter
|
||||||
|
const shouldEncryptViaFrontmatter = frontmatter?.encrypt === true
|
||||||
|
const frontmatterConfig = frontmatter?.encryptConfig as
|
||||||
|
| (BaseCryptoConfig & { password?: string })
|
||||||
|
| undefined
|
||||||
|
|
||||||
if (!folderConfig && !frontmatterConfig.password) {
|
// Get folder-based configuration
|
||||||
return undefined
|
const folderConfig = relativePath
|
||||||
} else if (!folderConfig && !frontmatter?.encrypt) {
|
? getConfigurationForPath(relativePath, folderConfigs, pluginConfig)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
// If neither folder config nor frontmatter indicates encryption, skip
|
||||||
|
if (!folderConfig && !shouldEncryptViaFrontmatter) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
// Build final configuration with proper precedence
|
||||||
algorithm: frontmatterConfig.algorithm || folderConfig?.algorithm || opts.algorithm,
|
let finalConfig: EncryptionConfig | undefined
|
||||||
password: frontmatterConfig.password || folderConfig?.password || "",
|
|
||||||
message: frontmatterConfig.message || folderConfig?.message || opts.message,
|
if (folderConfig && frontmatterConfig) {
|
||||||
ttl: frontmatterConfig.ttl || folderConfig?.ttl || opts.ttl,
|
// Merge folder and frontmatter configs
|
||||||
|
finalConfig = {
|
||||||
|
algorithm: frontmatterConfig.algorithm ?? folderConfig.algorithm,
|
||||||
|
ttl: frontmatterConfig.ttl ?? folderConfig.ttl,
|
||||||
|
message: frontmatterConfig.message ?? folderConfig.message,
|
||||||
|
password: frontmatterConfig.password ?? folderConfig.password,
|
||||||
|
}
|
||||||
|
} else if (folderConfig) {
|
||||||
|
// Use folder config only
|
||||||
|
finalConfig = folderConfig
|
||||||
|
} else if (frontmatterConfig?.password) {
|
||||||
|
// Use frontmatter config with global defaults
|
||||||
|
finalConfig = {
|
||||||
|
algorithm: frontmatterConfig.algorithm ?? pluginConfig.algorithm,
|
||||||
|
ttl: frontmatterConfig.ttl ?? pluginConfig.ttl,
|
||||||
|
message: frontmatterConfig.message ?? pluginConfig.message,
|
||||||
|
password: frontmatterConfig.password,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.password) {
|
// Validate final configuration
|
||||||
|
if (!finalConfig?.password) {
|
||||||
|
console.warn(`[EncryptPlugin] No password configured for ${relativePath}`)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return finalConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "Encrypt",
|
name: "Encrypt",
|
||||||
markdownPlugins() {
|
markdownPlugins() {
|
||||||
// If encrypted, prepend lock emoji before the title
|
|
||||||
return [
|
return [
|
||||||
() => {
|
() => {
|
||||||
return async (_, file) => {
|
return async (_, file) => {
|
||||||
@ -93,34 +253,33 @@ export const Encrypt: QuartzTransformerPlugin<Partial<Options>> = (userOpts) =>
|
|||||||
() => {
|
() => {
|
||||||
return async (tree: Root, file) => {
|
return async (tree: Root, file) => {
|
||||||
const config = getEncryptionConfig(file)
|
const config = getEncryptionConfig(file)
|
||||||
|
if (!config || !file.data.hash) {
|
||||||
if (!file.data.hash || !config) {
|
|
||||||
return tree
|
return tree
|
||||||
}
|
}
|
||||||
|
|
||||||
const locale = ctx.cfg.configuration.locale
|
const locale = ctx.cfg.configuration.locale
|
||||||
const t = i18n(locale).components.encryption
|
const t = i18n(locale).components.encryption
|
||||||
|
|
||||||
// Convert html to plaintext and encrypt it
|
// Encrypt the content
|
||||||
file.data.encryptionResult = await encryptContent(
|
|
||||||
toString(tree),
|
|
||||||
config.password,
|
|
||||||
config,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Encrypt the content and generate verification hash
|
|
||||||
const encryptionResult = await encryptContent(toHtml(tree), config.password, config)
|
const encryptionResult = await encryptContent(toHtml(tree), config.password, config)
|
||||||
|
|
||||||
// Create individual attributes for each field instead of JSON
|
// Store for later use
|
||||||
|
file.data.encryptionResult = encryptionResult
|
||||||
|
|
||||||
|
// Create attributes for client-side decryption
|
||||||
const attributes = [
|
const attributes = [
|
||||||
`data-config='${JSON.stringify(config)}'`,
|
`data-config='${JSON.stringify({
|
||||||
|
algorithm: config.algorithm,
|
||||||
|
ttl: config.ttl,
|
||||||
|
message: config.message,
|
||||||
|
})}'`,
|
||||||
`data-encrypted='${JSON.stringify(encryptionResult)}'`,
|
`data-encrypted='${JSON.stringify(encryptionResult)}'`,
|
||||||
`data-hash='${JSON.stringify(file.data.hash)}'`,
|
`data-hash='${JSON.stringify(file.data.hash)}'`,
|
||||||
`data-slug='${file.data.slug}'`,
|
`data-slug='${file.data.slug}'`,
|
||||||
`data-decrypted='false'`,
|
`data-decrypted='false'`,
|
||||||
].join(" ")
|
].join(" ")
|
||||||
|
|
||||||
// Create a new tree with encrypted content placeholder
|
// Create encrypted content placeholder
|
||||||
const encryptedTree = fromHtml(
|
const encryptedTree = fromHtml(
|
||||||
`
|
`
|
||||||
<div class="encrypted-content" ${attributes}>
|
<div class="encrypted-content" ${attributes}>
|
||||||
@ -149,7 +308,6 @@ export const Encrypt: QuartzTransformerPlugin<Partial<Options>> = (userOpts) =>
|
|||||||
|
|
||||||
// Replace the original tree
|
// Replace the original tree
|
||||||
tree.children = encryptedTree.children
|
tree.children = encryptedTree.children
|
||||||
|
|
||||||
return tree
|
return tree
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -170,12 +328,15 @@ export const Encrypt: QuartzTransformerPlugin<Partial<Options>> = (userOpts) =>
|
|||||||
inline: true,
|
inline: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
additionalHead: [],
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// MODULE AUGMENTATION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
declare module "vfile" {
|
declare module "vfile" {
|
||||||
interface DataMap {
|
interface DataMap {
|
||||||
encryptionConfig: EncryptionConfig
|
encryptionConfig: EncryptionConfig
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import toml from "toml"
|
|||||||
import { FilePath, FullSlug, getFileExtension, slugifyFilePath, slugTag } from "../../util/path"
|
import { FilePath, FullSlug, getFileExtension, slugifyFilePath, slugTag } from "../../util/path"
|
||||||
import { QuartzPluginData } from "../vfile"
|
import { QuartzPluginData } from "../vfile"
|
||||||
import { i18n } from "../../i18n"
|
import { i18n } from "../../i18n"
|
||||||
import { DirectoryConfig } from "../../util/encryption"
|
import { EncryptionConfig } from "../../util/encryption"
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
delimiters: string | [string, string]
|
delimiters: string | [string, string]
|
||||||
@ -169,7 +169,7 @@ declare module "vfile" {
|
|||||||
socialImage: string
|
socialImage: string
|
||||||
comments: boolean | string
|
comments: boolean | string
|
||||||
encrypt: boolean
|
encrypt: boolean
|
||||||
encryptConfig: DirectoryConfig
|
encryptConfig: EncryptionConfig
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,17 @@
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// TYPES AND INTERFACES
|
// TYPES AND INTERFACES
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export const SUPPORTED_ALGORITHMS = ["aes-256-cbc", "aes-256-gcm", "aes-256-ecb"] as const
|
export const SUPPORTED_ALGORITHMS = ["aes-256-cbc", "aes-256-gcm", "aes-256-ecb"] as const
|
||||||
|
|
||||||
export type SupportedEncryptionAlgorithm = (typeof SUPPORTED_ALGORITHMS)[number]
|
export type SupportedEncryptionAlgorithm = (typeof SUPPORTED_ALGORITHMS)[number]
|
||||||
|
|
||||||
|
// Result of hash operation
|
||||||
export interface Hash {
|
export interface Hash {
|
||||||
hash: string
|
hash: string
|
||||||
salt: string
|
salt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Result of encryption operation
|
||||||
export interface EncryptionResult {
|
export interface EncryptionResult {
|
||||||
encryptedContent: string
|
encryptedContent: string
|
||||||
encryptionSalt: string
|
encryptionSalt: string
|
||||||
@ -18,30 +19,28 @@ export interface EncryptionResult {
|
|||||||
authTag?: string
|
authTag?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EncryptionConfig {
|
// Base crypto configuration without password
|
||||||
algorithm: string
|
export interface BaseCryptoConfig {
|
||||||
|
algorithm?: SupportedEncryptionAlgorithm
|
||||||
|
ttl?: number
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directory configuration can be partial or just a password string
|
||||||
|
export type DirectoryConfig = (BaseCryptoConfig & { password: string }) | string
|
||||||
|
|
||||||
|
// Complete crypto configuration with all required fields
|
||||||
|
export interface CompleteCryptoConfig {
|
||||||
|
algorithm: SupportedEncryptionAlgorithm
|
||||||
ttl: number
|
ttl: number
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DirectoryConfig extends EncryptionConfig {
|
// Encryption configuration includes password
|
||||||
|
export interface EncryptionConfig extends CompleteCryptoConfig {
|
||||||
password: string
|
password: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EncryptionOptions extends EncryptionConfig {
|
|
||||||
encryptedFolders: { [folderPath: string]: string | DirectoryConfig }
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// CONSTANTS AND CONFIGURATION
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export const defaultEncryptionConfig: EncryptionConfig = {
|
|
||||||
algorithm: "aes-256-cbc",
|
|
||||||
ttl: 3600 * 24 * 7,
|
|
||||||
message: "This content is encrypted.",
|
|
||||||
}
|
|
||||||
|
|
||||||
const ENCRYPTION_CACHE_KEY = "quartz-encrypt-passwords"
|
const ENCRYPTION_CACHE_KEY = "quartz-encrypt-passwords"
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@ -197,7 +196,7 @@ export async function verifyPasswordHash(
|
|||||||
export async function encryptContent(
|
export async function encryptContent(
|
||||||
content: string,
|
content: string,
|
||||||
password: string,
|
password: string,
|
||||||
config: EncryptionConfig,
|
config: CompleteCryptoConfig,
|
||||||
): Promise<EncryptionResult> {
|
): Promise<EncryptionResult> {
|
||||||
checkCryptoSupport()
|
checkCryptoSupport()
|
||||||
|
|
||||||
@ -290,7 +289,7 @@ export async function encryptContent(
|
|||||||
|
|
||||||
export async function decryptContent(
|
export async function decryptContent(
|
||||||
encrypted: EncryptionResult,
|
encrypted: EncryptionResult,
|
||||||
config: EncryptionConfig,
|
config: CompleteCryptoConfig,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
checkCryptoSupport()
|
checkCryptoSupport()
|
||||||
@ -364,72 +363,27 @@ export async function decryptContent(
|
|||||||
return arrayBufferToString(decryptedBuffer)
|
return arrayBufferToString(decryptedBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// CONFIGURATION MANAGEMENT
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export function normalizeDirectoryConfig(
|
|
||||||
folderConfig: string | DirectoryConfig,
|
|
||||||
globalConfig: EncryptionConfig,
|
|
||||||
): DirectoryConfig {
|
|
||||||
if (typeof folderConfig === "string") {
|
|
||||||
return {
|
|
||||||
...globalConfig,
|
|
||||||
password: folderConfig,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
algorithm: folderConfig.algorithm || globalConfig.algorithm,
|
|
||||||
ttl: folderConfig.ttl || globalConfig.ttl,
|
|
||||||
password: folderConfig.password,
|
|
||||||
message: folderConfig.message || globalConfig.message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEncryptionConfigForPath(
|
|
||||||
filePath: string,
|
|
||||||
options: EncryptionOptions,
|
|
||||||
): DirectoryConfig | undefined {
|
|
||||||
if (!options.encryptedFolders) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
let deepestFolder = ""
|
|
||||||
let deepestConfig: string | DirectoryConfig | undefined
|
|
||||||
|
|
||||||
for (const [folder, config] of Object.entries(options.encryptedFolders)) {
|
|
||||||
if (filePath.startsWith(folder) && deepestFolder.length < folder.length) {
|
|
||||||
deepestFolder = folder
|
|
||||||
deepestConfig = config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deepestConfig) {
|
|
||||||
const globalConfig = {
|
|
||||||
algorithm: options.algorithm || defaultEncryptionConfig.algorithm,
|
|
||||||
ttl: options.ttl || defaultEncryptionConfig.ttl,
|
|
||||||
message: options.message || defaultEncryptionConfig.message,
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizeDirectoryConfig(deepestConfig, globalConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function searchForValidPassword(
|
export async function searchForValidPassword(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
hash: Hash,
|
hash: Hash,
|
||||||
config: EncryptionConfig,
|
config: CompleteCryptoConfig,
|
||||||
|
blacklist: Set<string> | null = null,
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
const passwords = getRelevantPasswords(filePath)
|
const passwords = getRelevantPasswords(filePath)
|
||||||
|
|
||||||
for (const password of passwords) {
|
for (const password of passwords) {
|
||||||
|
if (blacklist && blacklist.has(password)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (await verifyPasswordHash(password, hash)) {
|
if (await verifyPasswordHash(password, hash)) {
|
||||||
addPasswordToCache(password, filePath, config.ttl)
|
addPasswordToCache(password, filePath, config.ttl)
|
||||||
return password
|
return password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (blacklist) {
|
||||||
|
blacklist.add(password)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
@ -566,27 +520,36 @@ export function getSharedDirectoryDepth(path1: string, path2: string): number {
|
|||||||
export async function contentDecryptedEventListener(
|
export async function contentDecryptedEventListener(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
hash: Hash,
|
hash: Hash,
|
||||||
config: EncryptionConfig,
|
config: CompleteCryptoConfig,
|
||||||
callback: (password: string) => void,
|
callback: (password: string) => void,
|
||||||
once: boolean = true,
|
|
||||||
) {
|
) {
|
||||||
const checkForValidPassword: () => Promise<boolean> = async () => {
|
const blacklist = new Set<string>()
|
||||||
const password = await searchForValidPassword(filePath, hash, config)
|
|
||||||
if (password) {
|
async function decryptionSuccessful(password: string) {
|
||||||
callback(password)
|
addPasswordToCache(password, filePath, config.ttl)
|
||||||
return true
|
callback(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listener(e: CustomEventMap["decrypt"]) {
|
||||||
|
const password = e.detail.password
|
||||||
|
|
||||||
|
if (blacklist.has(password)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await verifyPasswordHash(password, hash)) {
|
||||||
|
document.removeEventListener("decrypt", listener)
|
||||||
|
await decryptionSuccessful(password)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
blacklist.add(password)
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await checkForValidPassword()
|
const password = await searchForValidPassword(filePath, hash, config, blacklist)
|
||||||
|
if (password) {
|
||||||
if (!result || !once) {
|
callback(password)
|
||||||
document.addEventListener("nav", async function listener() {
|
|
||||||
const result = await checkForValidPassword()
|
|
||||||
if (result && once) {
|
|
||||||
document.removeEventListener("nav", listener)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener("decrypt", listener)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user