@fiduswriter/editor 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/collab/chat.js +1 -1
- package/dist/collab/chat.js.map +1 -1
- package/dist/collab/doc.js +8 -8
- package/dist/collab/doc.js.map +1 -1
- package/dist/dialogs/figure.js +1 -1
- package/dist/dialogs/figure.js.map +1 -1
- package/dist/document_template/exporter.js +1 -1
- package/dist/document_template/exporter.js.map +1 -1
- package/dist/document_template/index.js +2 -2
- package/dist/document_template/index.js.map +1 -1
- package/dist/e2ee/snapshot-manager.js +5 -5
- package/dist/e2ee/snapshot-manager.js.map +1 -1
- package/dist/exporter/native/copy.js +117 -4
- package/dist/exporter/native/copy.js.map +1 -1
- package/dist/footnotes/editor.js +1 -1
- package/dist/footnotes/editor.js.map +1 -1
- package/dist/images/edit_dialog/edit_dialog/index.js +1 -1
- package/dist/images/edit_dialog/edit_dialog/index.js.map +1 -1
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -1
- package/dist/menus/headerbar/model.js +4 -4
- package/dist/menus/headerbar/model.js.map +1 -1
- package/package.json +3 -3
- package/src/collab/chat.js +1 -1
- package/src/collab/doc.js +8 -8
- package/src/dialogs/figure.js +1 -1
- package/src/document_template/exporter.js +1 -1
- package/src/document_template/index.js +2 -2
- package/src/e2ee/snapshot-manager.js +5 -5
- package/src/exporter/native/copy.js +144 -7
- package/src/footnotes/editor.js +1 -1
- package/src/images/edit_dialog/edit_dialog/index.js +1 -1
- package/src/index.js +7 -7
- package/src/menus/headerbar/model.js +4 -4
- package/src/e2ee/encryptor.js +0 -228
- package/src/e2ee/key-manager.js +0 -202
- package/src/e2ee/passphrase-crypto.js +0 -606
- package/src/e2ee/passphrase-dialog.js +0 -655
- package/src/e2ee/passphrase-manager.js +0 -502
- package/src/e2ee/password-dialog.js +0 -652
package/src/e2ee/key-manager.js
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* E2EE Key Manager - Handles key derivation and salt generation for
|
|
3
|
-
* end-to-end encrypted documents.
|
|
4
|
-
*
|
|
5
|
-
* Uses PBKDF2 with SHA-256 to derive a 256-bit AES-GCM key from a
|
|
6
|
-
* user-supplied password and a server-stored salt. The salt and
|
|
7
|
-
* iteration count are stored on the server as part of the Document
|
|
8
|
-
* model (Document.e2ee_salt and Document.e2ee_iterations),
|
|
9
|
-
* so the user only needs the password to decrypt from any device.
|
|
10
|
-
*
|
|
11
|
-
* The key is marked as non-extractable for security — it cannot be
|
|
12
|
-
* read back from the CryptoKey object once created.
|
|
13
|
-
*/
|
|
14
|
-
export class E2EEKeyManager {
|
|
15
|
-
/**
|
|
16
|
-
* Derive an AES-GCM key from a password and salt using PBKDF2.
|
|
17
|
-
*
|
|
18
|
-
* @param {string} password - The user-supplied password
|
|
19
|
-
* @param {Uint8Array} salt - The salt (16 bytes), fetched from the server
|
|
20
|
-
* as part of the document data (get_doc_data or subscribe)
|
|
21
|
-
* @param {number} [iterations=600000] - PBKDF2 iteration count
|
|
22
|
-
* (OWASP 2023 recommendation for PBKDF2-SHA256)
|
|
23
|
-
* @returns {Promise<CryptoKey>} A non-extractable AES-GCM 256-bit key
|
|
24
|
-
*/
|
|
25
|
-
static async deriveKey(password, salt, iterations = 600000) {
|
|
26
|
-
const encoder = new TextEncoder()
|
|
27
|
-
const keyMaterial = await crypto.subtle.importKey(
|
|
28
|
-
"raw",
|
|
29
|
-
encoder.encode(password),
|
|
30
|
-
"PBKDF2",
|
|
31
|
-
false,
|
|
32
|
-
["deriveKey"]
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
const key = await crypto.subtle.deriveKey(
|
|
36
|
-
{
|
|
37
|
-
name: "PBKDF2",
|
|
38
|
-
salt: salt,
|
|
39
|
-
iterations: iterations,
|
|
40
|
-
hash: "SHA-256"
|
|
41
|
-
},
|
|
42
|
-
keyMaterial,
|
|
43
|
-
{name: "AES-GCM", length: 256},
|
|
44
|
-
true, // extractable — required for sessionStorage caching
|
|
45
|
-
["encrypt", "decrypt"]
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
return key
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Store an AES-GCM key in sessionStorage for the current browser session.
|
|
53
|
-
* The key is exported as raw bytes and Base64-encoded before storage.
|
|
54
|
-
*
|
|
55
|
-
* @param {number} documentId - The document ID
|
|
56
|
-
* @param {CryptoKey} key - The AES-GCM key to store
|
|
57
|
-
*/
|
|
58
|
-
static async storeKeyInSession(documentId, key) {
|
|
59
|
-
const raw = await crypto.subtle.exportKey("raw", key)
|
|
60
|
-
const base64 = btoa(String.fromCharCode(...new Uint8Array(raw)))
|
|
61
|
-
sessionStorage.setItem(`e2ee_key_${documentId}`, base64)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Retrieve an AES-GCM key from sessionStorage.
|
|
66
|
-
*
|
|
67
|
-
* @param {number} documentId - The document ID
|
|
68
|
-
* @returns {Promise<CryptoKey|null>} The imported key, or null if not found
|
|
69
|
-
*/
|
|
70
|
-
static getKeyFromSession(documentId) {
|
|
71
|
-
const base64 = sessionStorage.getItem(`e2ee_key_${documentId}`)
|
|
72
|
-
if (!base64) {
|
|
73
|
-
return null
|
|
74
|
-
}
|
|
75
|
-
const binary = atob(base64)
|
|
76
|
-
const raw = new Uint8Array(binary.length)
|
|
77
|
-
for (let i = 0; i < binary.length; i++) {
|
|
78
|
-
raw[i] = binary.charCodeAt(i)
|
|
79
|
-
}
|
|
80
|
-
return crypto.subtle.importKey(
|
|
81
|
-
"raw",
|
|
82
|
-
raw,
|
|
83
|
-
{name: "AES-GCM", length: 256},
|
|
84
|
-
true,
|
|
85
|
-
["encrypt", "decrypt"]
|
|
86
|
-
)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Remove a cached key from sessionStorage.
|
|
91
|
-
*
|
|
92
|
-
* @param {number} documentId - The document ID
|
|
93
|
-
*/
|
|
94
|
-
static clearKeyFromSession(documentId) {
|
|
95
|
-
sessionStorage.removeItem(`e2ee_key_${documentId}`)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Clear all cached E2EE keys from sessionStorage.
|
|
100
|
-
* Should be called on sign-out or session expiration.
|
|
101
|
-
*/
|
|
102
|
-
static clearAllKeysFromSession() {
|
|
103
|
-
for (let i = sessionStorage.length - 1; i >= 0; i--) {
|
|
104
|
-
const key = sessionStorage.key(i)
|
|
105
|
-
if (key && key.startsWith("e2ee_key_")) {
|
|
106
|
-
sessionStorage.removeItem(key)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Generate a new random salt (16 bytes).
|
|
113
|
-
*
|
|
114
|
-
* Used when creating a new E2EE document or changing the password.
|
|
115
|
-
* The generated salt is sent to the server and stored in
|
|
116
|
-
* Document.e2ee_salt. The salt is not a secret — its purpose
|
|
117
|
-
* is to ensure that two documents with the same password produce
|
|
118
|
-
* different derived keys (preventing rainbow table attacks).
|
|
119
|
-
*
|
|
120
|
-
* @returns {Uint8Array} A 16-byte random salt
|
|
121
|
-
*/
|
|
122
|
-
static generateSalt() {
|
|
123
|
-
return crypto.getRandomValues(new Uint8Array(16))
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Resolve a document password to an AES-GCM key.
|
|
128
|
-
*
|
|
129
|
-
* If the password is a valid base64/base64url-encoded 32-byte string
|
|
130
|
-
* (43 or 44 characters), it is treated as a raw DEK and imported
|
|
131
|
-
* directly without PBKDF2. Otherwise, the key is derived via PBKDF2.
|
|
132
|
-
*
|
|
133
|
-
* @param {string} password - The document password
|
|
134
|
-
* @param {Uint8Array} salt - The salt (16 bytes)
|
|
135
|
-
* @param {number} [iterations=600000] - PBKDF2 iteration count
|
|
136
|
-
* @returns {Promise<CryptoKey>} The AES-GCM key
|
|
137
|
-
*/
|
|
138
|
-
static resolvePasswordToKey(password, salt, iterations = 600000) {
|
|
139
|
-
// Try to interpret as raw base64/base64url DEK first
|
|
140
|
-
if (password.length === 44 || password.length === 43) {
|
|
141
|
-
let decoded = null
|
|
142
|
-
try {
|
|
143
|
-
decoded = atob(password)
|
|
144
|
-
} catch (_e) {
|
|
145
|
-
// Try base64url with padding conversion
|
|
146
|
-
try {
|
|
147
|
-
let base64 = password.replace(/-/g, "+").replace(/_/g, "/")
|
|
148
|
-
while (base64.length % 4) {
|
|
149
|
-
base64 += "="
|
|
150
|
-
}
|
|
151
|
-
decoded = atob(base64)
|
|
152
|
-
} catch (_e2) {
|
|
153
|
-
// Not valid base64url either
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (decoded && decoded.length === 32) {
|
|
157
|
-
const raw = new Uint8Array(decoded.length)
|
|
158
|
-
for (let i = 0; i < decoded.length; i++) {
|
|
159
|
-
raw[i] = decoded.charCodeAt(i)
|
|
160
|
-
}
|
|
161
|
-
return crypto.subtle.importKey(
|
|
162
|
-
"raw",
|
|
163
|
-
raw,
|
|
164
|
-
{name: "AES-GCM", length: 256},
|
|
165
|
-
true,
|
|
166
|
-
["encrypt", "decrypt"]
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
// Fall back to PBKDF2 derivation
|
|
171
|
-
return E2EEKeyManager.deriveKey(password, salt, iterations)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Store the document password in sessionStorage.
|
|
176
|
-
*
|
|
177
|
-
* @param {number} documentId - The document ID
|
|
178
|
-
* @param {string} password - The document password
|
|
179
|
-
*/
|
|
180
|
-
static storePasswordInSession(documentId, password) {
|
|
181
|
-
sessionStorage.setItem(`e2ee_password_${documentId}`, password)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Retrieve the document password from sessionStorage.
|
|
186
|
-
*
|
|
187
|
-
* @param {number} documentId - The document ID
|
|
188
|
-
* @returns {string|null} The password, or null if not found
|
|
189
|
-
*/
|
|
190
|
-
static getPasswordFromSession(documentId) {
|
|
191
|
-
return sessionStorage.getItem(`e2ee_password_${documentId}`)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Remove a cached password from sessionStorage.
|
|
196
|
-
*
|
|
197
|
-
* @param {number} documentId - The document ID
|
|
198
|
-
*/
|
|
199
|
-
static clearPasswordFromSession(documentId) {
|
|
200
|
-
sessionStorage.removeItem(`e2ee_password_${documentId}`)
|
|
201
|
-
}
|
|
202
|
-
}
|