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:
eezd committed 2026-10-17 14:51:08 +08:00
1 parent 35d3138c7d
commit 5cf5a45682
20 files changed
+899 -39

No files matched your search

+12 -2
View File
@@ -3,7 +3,8 @@ import { EditorState, type StateEffect, type Text } from '@codemirror/state'
import type { EditorView } from '@codemirror/view'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { createEditorView } from '../editor'
import { activeTab, attachView, editorStateOf, noteRefOf, revealPending, storeViewState, ws } from './workspace'
import { keyState } from './encryption'
import { activeTab, attachView, editorStateOf, noteRefOf, revealPending, storeViewState, unlockNotes, ws } from './workspace'
const host = ref<HTMLElement>()
const tab = computed(activeTab)
@@ -28,6 +29,8 @@ function show() {
scrollPositions.set(shown, { doc: view.state.doc, snapshot: view.scrollSnapshot(), top: view.scrollDOM.scrollTop })
}
if (!ref || !state) {
// 锁定后视图中不能留下已解密的内容
if (shown) view.setState(EditorState.create())
shown = undefined
return
}
@@ -60,10 +63,17 @@ watch(() => [ws.active, tab.value?.status], show, { flush: 'post' })
<template>
<div class="rm-editor-pane">
<div v-show="tab && tab.status !== 'loading' && tab.status !== 'error'" ref="host" class="rm-editor-host" />
<div v-show="tab && !['loading', 'error', 'locked'].includes(tab.status)" ref="host" class="rm-editor-host" />
<div v-if="!tab" class="rm-empty">
<p>选择或新建一篇笔记</p>
</div>
<div v-else-if="tab.status === 'error'" class="rm-empty"><p>笔记打开失败</p></div>
<div v-else-if="tab.status === 'locked'" class="rm-empty">
<template v-if="keyState.supported">
<p>这是一篇加密笔记</p>
<button class="rm-btn rm-btn-primary" @click="unlockNotes()">解锁</button>
</template>
<p v-else class="rm-locked-hint">这是一篇加密笔记。当前连接不是 HTTPS,浏览器不允许网页使用加密功能,无法查看。请通过 HTTPS 访问。</p>
</div>
</div>
</template>