Add client-side note encryption
- WebCrypto keyring (PBKDF2 600k + AES-GCM master key) and per-note/per-image keys - Encrypt/decrypt commands convert the note and its own images, deleting the originals - Unlock, setup and change-password dialogs; manual lock; lock on page close - Encrypted images are decrypted to blob URLs; pasted images are encrypted before upload - Rewrite image links in the browser when renaming an encrypted note - Skip local drafts for encrypted notes and confirm before closing with unsaved changes - Show encrypted notes as locked with an explanation outside secure contexts
This commit is contained in:
1 parent
35d3138c7d
commit
5cf5a45682
20 files changed
+899
-39
No files matched your search
@@ -8,7 +8,7 @@ import { typoraExtensions } from '../markdown/extensions'
|
||||
import { renderedBlocks } from './blocks'
|
||||
import { codeBlockHighlight } from './codeblocks'
|
||||
import { editorFocus } from './focus'
|
||||
import { livePreview, resolveImageUrl } from './livepreview'
|
||||
import { imageLoader, livePreview, resolveImageUrl } from './livepreview'
|
||||
import { tableEditing } from './table'
|
||||
import { markdownTheme } from './theme'
|
||||
|
||||
@@ -26,6 +26,8 @@ const rawmarkLanguage = new Language(
|
||||
export interface EditorStateOptions {
|
||||
doc: string
|
||||
resolveImage?: (src: string) => string
|
||||
// 见 imageLoader
|
||||
loadImage?: (src: string) => Promise<string> | null
|
||||
// 文档变化回调,仅对挂载在视图上的状态触发;未激活标签页的状态由调用方直接更新
|
||||
onChange?: (update: ViewUpdate) => void
|
||||
extensions?: Extension[]
|
||||
@@ -53,6 +55,7 @@ export function createEditorState(opts: EditorStateOptions): EditorState {
|
||||
EditorView.contentAttributes.of({ spellcheck: 'false', autocapitalize: 'sentences' }),
|
||||
]
|
||||
if (opts.resolveImage) extensions.push(resolveImageUrl.of(opts.resolveImage))
|
||||
if (opts.loadImage) extensions.push(imageLoader.of(opts.loadImage))
|
||||
if (opts.onChange) {
|
||||
const onChange = opts.onChange
|
||||
extensions.push(EditorView.updateListener.of((u) => u.docChanged && onChange(u)))
|
||||
|
||||
@@ -21,6 +21,11 @@ export const resolveImageUrl = Facet.define<(src: string) => string, (src: strin
|
||||
combine: (values) => values[0] ?? ((src) => src),
|
||||
})
|
||||
|
||||
// 需要先取回处理的图片(加密笔记的图片要下载后在浏览器解密):返回可显示的地址;返回 null 表示直接加载
|
||||
export const imageLoader = Facet.define<(src: string) => Promise<string> | null, (src: string) => Promise<string> | null>({
|
||||
combine: (values) => values[0] ?? (() => null),
|
||||
})
|
||||
|
||||
const hidden = Decoration.replace({})
|
||||
const inlineCode = Decoration.mark({ class: 'cm-rm-inline-code' })
|
||||
const linkText = Decoration.mark({ class: 'cm-rm-link' })
|
||||
@@ -247,7 +252,7 @@ function buildDecorations(view: EditorView): DecorationSet {
|
||||
const marks = node.getChildren('LinkMark')
|
||||
if (!url || marks.length < 2) return
|
||||
const src = resolveUrl(state.sliceDoc(url.from, url.to).replace(/^<|>$/g, ''))
|
||||
const widget = new ImageWidget(src, state.sliceDoc(marks[0].to, marks[1].from))
|
||||
const widget = new ImageWidget(src, state.sliceDoc(marks[0].to, marks[1].from), state.facet(imageLoader))
|
||||
if (touches(node)) out.push(Decoration.widget({ widget, side: 1 }).range(node.to))
|
||||
else if (lineOf(node.from) === lineOf(node.to)) out.push(Decoration.replace({ widget }).range(node.from, node.to))
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ export class ImageWidget extends WidgetType {
|
||||
constructor(
|
||||
readonly src: string,
|
||||
readonly alt: string,
|
||||
readonly load: (src: string) => Promise<string> | null = () => null,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -133,7 +134,9 @@ export class ImageWidget extends WidgetType {
|
||||
const wrap = document.createElement('span')
|
||||
wrap.className = 'cm-rm-image'
|
||||
const img = document.createElement('img')
|
||||
img.src = this.src
|
||||
const loading = this.load(this.src)
|
||||
if (loading) loading.then((url) => (img.src = url), () => img.dispatchEvent(new Event('error')))
|
||||
else img.src = this.src
|
||||
img.alt = this.alt
|
||||
img.loading = 'lazy'
|
||||
img.decoding = 'async'
|
||||
|
||||
Reference in new issue
Block a user