@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.
Files changed (40) hide show
  1. package/dist/collab/chat.js +1 -1
  2. package/dist/collab/chat.js.map +1 -1
  3. package/dist/collab/doc.js +8 -8
  4. package/dist/collab/doc.js.map +1 -1
  5. package/dist/dialogs/figure.js +1 -1
  6. package/dist/dialogs/figure.js.map +1 -1
  7. package/dist/document_template/exporter.js +1 -1
  8. package/dist/document_template/exporter.js.map +1 -1
  9. package/dist/document_template/index.js +2 -2
  10. package/dist/document_template/index.js.map +1 -1
  11. package/dist/e2ee/snapshot-manager.js +5 -5
  12. package/dist/e2ee/snapshot-manager.js.map +1 -1
  13. package/dist/exporter/native/copy.js +117 -4
  14. package/dist/exporter/native/copy.js.map +1 -1
  15. package/dist/footnotes/editor.js +1 -1
  16. package/dist/footnotes/editor.js.map +1 -1
  17. package/dist/images/edit_dialog/edit_dialog/index.js +1 -1
  18. package/dist/images/edit_dialog/edit_dialog/index.js.map +1 -1
  19. package/dist/index.js +7 -7
  20. package/dist/index.js.map +1 -1
  21. package/dist/menus/headerbar/model.js +4 -4
  22. package/dist/menus/headerbar/model.js.map +1 -1
  23. package/package.json +3 -3
  24. package/src/collab/chat.js +1 -1
  25. package/src/collab/doc.js +8 -8
  26. package/src/dialogs/figure.js +1 -1
  27. package/src/document_template/exporter.js +1 -1
  28. package/src/document_template/index.js +2 -2
  29. package/src/e2ee/snapshot-manager.js +5 -5
  30. package/src/exporter/native/copy.js +144 -7
  31. package/src/footnotes/editor.js +1 -1
  32. package/src/images/edit_dialog/edit_dialog/index.js +1 -1
  33. package/src/index.js +7 -7
  34. package/src/menus/headerbar/model.js +4 -4
  35. package/src/e2ee/encryptor.js +0 -228
  36. package/src/e2ee/key-manager.js +0 -202
  37. package/src/e2ee/passphrase-crypto.js +0 -606
  38. package/src/e2ee/passphrase-dialog.js +0 -655
  39. package/src/e2ee/passphrase-manager.js +0 -502
  40. package/src/e2ee/password-dialog.js +0 -652
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiduswriter/editor",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Fidus Writer browser editor",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@fiduswriter/common": "^0.1.3",
35
- "@fiduswriter/document": "^0.2.17",
35
+ "@fiduswriter/document": "^0.2.19",
36
36
  "bibliojson": "^4.0.0",
37
37
  "citeproc-plus": "^1.0.2",
38
38
  "cropperjs": "^1.6.2",
@@ -42,7 +42,7 @@
42
42
  "fast-deep-equal": "^3.1.3",
43
43
  "fast-xml-parser": "^4.5.0",
44
44
  "fix-utf8": "^2.0.1",
45
- "fwtoolkit": "^0.1.4",
45
+ "fwtoolkit": "^0.1.9",
46
46
  "jszip": "^3.10.1",
47
47
  "mathml-to-latex": "^1.4.3",
48
48
  "mathml2omml": "^0.5.0",
@@ -1,5 +1,5 @@
1
1
  import {localizeDate, whenReady} from "fwtoolkit"
2
- import {E2EEEncryptor} from "../e2ee/encryptor.js"
2
+ import {E2EEEncryptor} from "fwtoolkit/e2ee/encryptor"
3
3
  import {messageTemplate} from "./templates.js"
4
4
 
5
5
  /*
package/src/collab/doc.js CHANGED
@@ -3,11 +3,11 @@ import {Dialog, activateWait, deactivateWait, makeWorker} from "fwtoolkit"
3
3
  import {receiveTransaction, sendableSteps} from "prosemirror-collab"
4
4
  import {EditorState} from "prosemirror-state"
5
5
  import {Step} from "prosemirror-transform"
6
- import {E2EEEncryptor} from "../e2ee/encryptor.js"
7
- import {E2EEKeyManager} from "../e2ee/key-manager.js"
8
- import {enterPassphraseDialog} from "../e2ee/passphrase-dialog.js"
9
- import {PassphraseManager} from "../e2ee/passphrase-manager.js"
10
- import {enterPasswordDialog} from "../e2ee/password-dialog.js"
6
+ import {E2EEEncryptor} from "fwtoolkit/e2ee/encryptor"
7
+ import {E2EEKeyManager} from "fwtoolkit/e2ee/key-manager"
8
+ import {enterPassphraseDialog} from "fwtoolkit/e2ee/passphrase-dialog"
9
+ import {PassphraseManager} from "fwtoolkit/e2ee/passphrase-manager"
10
+ import {enterPasswordDialog} from "fwtoolkit/e2ee/password-dialog"
11
11
  import {
12
12
  getSelectionUpdate,
13
13
  removeCollaboratorSelection,
@@ -312,7 +312,7 @@ export class ModCollabDoc {
312
312
  } else if (result?.action === "recover") {
313
313
  // Recovery flow
314
314
  const {recoverWithKeyDialog} = await import(
315
- "../e2ee/passphrase-dialog.js"
315
+ "fwtoolkit/e2ee/passphrase-dialog"
316
316
  )
317
317
  const recoverResult = await new Promise(resolve => {
318
318
  recoverWithKeyDialog(resolve)
@@ -325,7 +325,7 @@ export class ModCollabDoc {
325
325
  recoverResult.newPassphrase
326
326
  )
327
327
  const {showRecoveryKeyDialog} = await import(
328
- "../e2ee/passphrase-dialog.js"
328
+ "fwtoolkit/e2ee/passphrase-dialog"
329
329
  )
330
330
  await new Promise(resolve => {
331
331
  showRecoveryKeyDialog(
@@ -685,7 +685,7 @@ export class ModCollabDoc {
685
685
  let decryptedImages = doc.images
686
686
  if (doc.images) {
687
687
  decryptedImages = {}
688
- const {E2EEEncryptor} = await import("../e2ee/encryptor.js")
688
+ const {E2EEEncryptor} = await import("fwtoolkit/e2ee/encryptor")
689
689
  for (const [id, image] of Object.entries(doc.images)) {
690
690
  decryptedImages[id] = image
691
691
  if (typeof image.copyright === "string") {
@@ -3,7 +3,7 @@ import deepEqual from "fast-deep-equal"
3
3
  import {randomFigureId} from "@fiduswriter/document/schema/common/index"
4
4
  import {ContentMenu, Dialog, addAlert, dropdownSelect} from "fwtoolkit"
5
5
  import {ImageSelectionDialog} from "../images/selection_dialog/index.js"
6
- import {E2EEEncryptor} from "../e2ee/encryptor.js"
6
+ import {E2EEEncryptor} from "fwtoolkit/e2ee/encryptor"
7
7
  import {configureFigureTemplate} from "./templates.js"
8
8
 
9
9
  export class FigureDialog {
@@ -1,7 +1,7 @@
1
1
  import download from "downloadjs"
2
2
 
3
3
  import {createSlug} from "@fiduswriter/document/exporter/tools/file"
4
- import {ZipFileCreator} from "@fiduswriter/document/exporter/tools/zip"
4
+ import {ZipFileCreator} from "fwtoolkit/file/zip"
5
5
  import {postJson} from "fwtoolkit"
6
6
 
7
7
  export class DocumentTemplateExporter {
@@ -7,11 +7,11 @@ import {
7
7
  shortFileTitle
8
8
  } from "fwtoolkit"
9
9
  import {SaveCopy} from "../exporter/native/index.js"
10
- import {E2EEKeyManager} from "../e2ee/key-manager.js"
10
+ import {E2EEKeyManager} from "fwtoolkit/e2ee/key-manager"
11
11
  import {
12
12
  createPasswordDialog,
13
13
  enterPasswordDialog
14
- } from "../e2ee/password-dialog.js"
14
+ } from "fwtoolkit/e2ee/password-dialog"
15
15
 
16
16
  export {serializeHelp} from "./schema.js"
17
17
 
@@ -68,7 +68,7 @@ export class E2EESnapshotManager {
68
68
  const bibliography = this.editor.docInfo.bibliography || {}
69
69
  const {title} = this.editor.getDoc()
70
70
 
71
- const {E2EEEncryptor} = await import("./encryptor.js")
71
+ const {E2EEEncryptor} = await import("fwtoolkit/e2ee/encryptor")
72
72
 
73
73
  const encryptedContent = await E2EEEncryptor.encryptObject(
74
74
  content,
@@ -129,7 +129,7 @@ export class E2EESnapshotManager {
129
129
  const {title} = this.editor.getDoc()
130
130
 
131
131
  // Dynamically import to avoid circular dependencies
132
- const {E2EEEncryptor} = await import("./encryptor.js")
132
+ const {E2EEEncryptor} = await import("fwtoolkit/e2ee/encryptor")
133
133
 
134
134
  // Encrypt all document data
135
135
  const encryptedContent = await E2EEEncryptor.encryptObject(
@@ -183,7 +183,7 @@ export class E2EESnapshotManager {
183
183
  this.clearKey()
184
184
  // Clear cached password and key from sessionStorage so the user
185
185
  // is prompted for the new password on next access.
186
- const {E2EEKeyManager} = await import("./key-manager.js")
186
+ const {E2EEKeyManager} = await import("fwtoolkit/e2ee/key-manager")
187
187
  E2EEKeyManager.clearKeyFromSession(this.editor.docInfo.id)
188
188
  E2EEKeyManager.clearPasswordFromSession(this.editor.docInfo.id)
189
189
  // Notify the user that the password has changed.
@@ -213,7 +213,7 @@ export class E2EESnapshotManager {
213
213
  return
214
214
  }
215
215
 
216
- const {E2EEEncryptor} = await import("./encryptor.js")
216
+ const {E2EEEncryptor} = await import("fwtoolkit/e2ee/encryptor")
217
217
 
218
218
  const encryptedContent = await E2EEEncryptor.encryptObject(
219
219
  content,
@@ -269,7 +269,7 @@ export class E2EESnapshotManager {
269
269
  // Extract the current title from the editor
270
270
  const {title} = this.editor.getDoc()
271
271
 
272
- const {E2EEEncryptor} = await import("./encryptor.js")
272
+ const {E2EEEncryptor} = await import("fwtoolkit/e2ee/encryptor")
273
273
 
274
274
  // Encrypt with the new key
275
275
  const encryptedContent = await E2EEEncryptor.encryptObject(
@@ -1,9 +1,144 @@
1
1
  import {SaveCopy as GenericSaveCopy} from "@fiduswriter/document/exporter/native"
2
- import {addProgress, gettext, shortFileTitle} from "fwtoolkit"
3
- import {E2EEEncryptor} from "../../e2ee/encryptor.js"
4
- import {E2EEKeyManager} from "../../e2ee/key-manager.js"
2
+ import {addAlert, addProgress, gettext, postJson, shortFileTitle} from "fwtoolkit"
3
+ import {E2EEEncryptor} from "fwtoolkit/e2ee/encryptor"
4
+ import {E2EEKeyManager} from "fwtoolkit/e2ee/key-manager"
5
5
  import {NativeImporter} from "@fiduswriter/document/importer/native"
6
6
 
7
+ async function maybeDecryptImage(imageEntry, sourceKey) {
8
+ if (!sourceKey || !imageEntry.file) {
9
+ return
10
+ }
11
+ if (imageEntry.file_type !== "application/octet-stream") {
12
+ return
13
+ }
14
+ const fileBuffer = await imageEntry.file.arrayBuffer()
15
+ const bytes = new Uint8Array(fileBuffer)
16
+ let binary = ""
17
+ const chunkSize = 65536
18
+ for (let i = 0; i < bytes.length; i += chunkSize) {
19
+ const chunk = bytes.subarray(i, i + chunkSize)
20
+ binary += String.fromCharCode.apply(null, chunk)
21
+ }
22
+ const base64 = btoa(binary)
23
+ const decrypted = await E2EEEncryptor.decryptBufferToBase64(base64, sourceKey)
24
+ const mime = imageEntry.original_file_type || "image/png"
25
+ const byteCharacters = atob(decrypted)
26
+ const byteNumbers = new Array(byteCharacters.length)
27
+ for (let i = 0; i < byteCharacters.length; i++) {
28
+ byteNumbers[i] = byteCharacters.charCodeAt(i)
29
+ }
30
+ const byteArray = new Uint8Array(byteNumbers)
31
+ imageEntry.file = new Blob([byteArray], {type: mime})
32
+ imageEntry.file_type = mime
33
+ }
34
+
35
+ function createNativeImporterBackend(user, e2eeOptions) {
36
+ return {
37
+ createDoc: (template, importId, path, e2ee, files) => {
38
+ const jsonData = {
39
+ template: template.content,
40
+ export_templates: template.exportTemplates,
41
+ document_styles: template.documentStyles,
42
+ import_id: importId
43
+ ? importId
44
+ : template.content.attrs.import_id,
45
+ template_title: template.content.attrs.template,
46
+ path
47
+ }
48
+ if (e2ee?.enabled) {
49
+ jsonData.e2ee = true
50
+ if (e2ee.salt) {
51
+ jsonData.e2ee_salt = e2ee.salt
52
+ }
53
+ if (e2ee.iterations) {
54
+ jsonData.e2ee_iterations = e2ee.iterations
55
+ }
56
+ }
57
+ return postJson("/api/document/import/create/", jsonData, files)
58
+ .then(({json}) => ({
59
+ id: json.id,
60
+ path: json.path,
61
+ e2ee: json.e2ee,
62
+ template: json.template
63
+ }))
64
+ .catch(error => {
65
+ addAlert("error", gettext("Could not create document"))
66
+ throw error
67
+ })
68
+ },
69
+ saveImages: async (images, docId, e2ee) => {
70
+ const isE2EE = e2ee?.enabled
71
+ const endpoint = isE2EE
72
+ ? "/api/document/e2ee_image/"
73
+ : "/api/document/import/image/"
74
+ const imageTranslationTable = {}
75
+ await Promise.all(
76
+ Object.values(images.db).map(async imageEntry => {
77
+ await maybeDecryptImage(imageEntry, e2ee?.sourceKey)
78
+ const encryptedFile =
79
+ e2ee?.enabled && e2ee.key
80
+ ? await E2EEEncryptor.encryptImage(
81
+ imageEntry.file,
82
+ e2ee.key
83
+ )
84
+ : imageEntry.file
85
+ const jsonData = {
86
+ doc_id: docId,
87
+ title: imageEntry.title,
88
+ copyright: imageEntry.copyright,
89
+ checksum: imageEntry.checksum
90
+ }
91
+ const files = {
92
+ image: {
93
+ file: encryptedFile,
94
+ filename: imageEntry.image.split("/").pop()
95
+ }
96
+ }
97
+ const {json} = await postJson(endpoint, jsonData, files)
98
+ imageTranslationTable[imageEntry.id] = json.id
99
+ })
100
+ )
101
+ return imageTranslationTable
102
+ },
103
+ saveDocument: async (saveData, e2ee) => {
104
+ if (e2ee?.enabled && e2ee.key) {
105
+ saveData.content = await E2EEEncryptor.encryptObject(
106
+ saveData.content,
107
+ e2ee.key
108
+ )
109
+ saveData.comments = await E2EEEncryptor.encryptObject(
110
+ saveData.comments || {},
111
+ e2ee.key
112
+ )
113
+ saveData.bibliography = await E2EEEncryptor.encryptObject(
114
+ saveData.bibliography,
115
+ e2ee.key
116
+ )
117
+ saveData.title = await E2EEEncryptor.encrypt(
118
+ saveData.title,
119
+ e2ee.key
120
+ )
121
+ }
122
+ return postJson("/api/document/import/", saveData)
123
+ .then(({json}) => ({added: json.added, updated: json.updated}))
124
+ .catch(error => {
125
+ addAlert(
126
+ "error",
127
+ `${gettext("Could not save ")} ${shortFileTitle(
128
+ saveData.title,
129
+ ""
130
+ )}`
131
+ )
132
+ throw error
133
+ })
134
+ },
135
+ encryptImage: E2EEEncryptor.encryptImage.bind(E2EEEncryptor),
136
+ encryptObject: E2EEEncryptor.encryptObject.bind(E2EEEncryptor),
137
+ encrypt: E2EEEncryptor.encrypt.bind(E2EEEncryptor),
138
+ storeKeyInSession: E2EEKeyManager.storeKeyInSession.bind(E2EEKeyManager)
139
+ }
140
+ }
141
+
7
142
  export class SaveCopy extends GenericSaveCopy {
8
143
  constructor(
9
144
  doc,
@@ -37,10 +172,12 @@ export class SaveCopy extends GenericSaveCopy {
37
172
  imageDB,
38
173
  httpIncludes,
39
174
  options.user,
40
- options.importId,
41
- options.requestedPath,
42
- null,
43
- options.e2eeOptions
175
+ createNativeImporterBackend(options.user, options.e2eeOptions),
176
+ {
177
+ importId: options.importId,
178
+ requestedPath: options.requestedPath,
179
+ e2eeOptions: options.e2eeOptions
180
+ }
44
181
  ).init()
45
182
 
46
183
  super(doc, bibDB, imageDB, newUser, {
@@ -11,7 +11,7 @@ import {EditorView} from "prosemirror-view"
11
11
 
12
12
  import {fnSchema} from "@fiduswriter/document/schema/footnotes"
13
13
  import {fnNodeToPmNode} from "@fiduswriter/document/schema/footnotes_convert"
14
- import {E2EEEncryptor} from "../e2ee/encryptor.js"
14
+ import {E2EEEncryptor} from "fwtoolkit/e2ee/encryptor"
15
15
  import {
16
16
  citationRenderPlugin,
17
17
  clipboardPlugin,
@@ -1,5 +1,5 @@
1
1
  import {CheckableList, ContentMenu, Dialog, addAlert} from "fwtoolkit"
2
- import {E2EEEncryptor} from "../../../e2ee/encryptor.js"
2
+ import {E2EEEncryptor} from "fwtoolkit/e2ee/encryptor"
3
3
  import {imageEditModel} from "./model.js"
4
4
  import {imageEditTemplate} from "./templates.js"
5
5
  export class ImageEditDialog {
package/src/index.js CHANGED
@@ -20,18 +20,18 @@ import {EditorState, TextSelection} from "prosemirror-state"
20
20
  import {tableEditing} from "prosemirror-tables"
21
21
  import {EditorView} from "prosemirror-view"
22
22
  import {FeedbackTab} from "@fiduswriter/common"
23
- import {E2EEEncryptor} from "./e2ee/encryptor.js"
24
- import {E2EEKeyManager} from "./e2ee/key-manager.js"
23
+ import {E2EEEncryptor} from "fwtoolkit/e2ee/encryptor"
24
+ import {E2EEKeyManager} from "fwtoolkit/e2ee/key-manager"
25
25
  import {
26
26
  enterPassphraseDialog,
27
27
  setupPassphraseDialog,
28
28
  showRecoveryKeyDialog
29
- } from "./e2ee/passphrase-dialog.js"
30
- import {PassphraseManager} from "./e2ee/passphrase-manager.js"
29
+ } from "fwtoolkit/e2ee/passphrase-dialog"
30
+ import {PassphraseManager} from "fwtoolkit/e2ee/passphrase-manager"
31
31
  import {
32
32
  changePasswordDialog,
33
33
  createPasswordDialog
34
- } from "./e2ee/password-dialog.js"
34
+ } from "fwtoolkit/e2ee/password-dialog"
35
35
  import {E2EESnapshotManager} from "./e2ee/snapshot-manager.js"
36
36
 
37
37
  import {getSettings} from "@fiduswriter/document/schema/convert"
@@ -794,7 +794,7 @@ export class Editor {
794
794
  } else if (result.action === "recover") {
795
795
  // Recovery flow
796
796
  const {recoverWithKeyDialog} = await import(
797
- "./e2ee/passphrase-dialog.js"
797
+ "fwtoolkit/e2ee/passphrase-dialog"
798
798
  )
799
799
  const recoverResult = await new Promise(resolve => {
800
800
  recoverWithKeyDialog(resolve)
@@ -807,7 +807,7 @@ export class Editor {
807
807
  recoverResult.newPassphrase
808
808
  )
809
809
  const {showRecoveryKeyDialog} = await import(
810
- "./e2ee/passphrase-dialog.js"
810
+ "fwtoolkit/e2ee/passphrase-dialog"
811
811
  )
812
812
  await new Promise(resolve => {
813
813
  showRecoveryKeyDialog(newRecoveryKey, resolve)
@@ -10,9 +10,9 @@ import {DocumentAccessRightsDialog} from "../../documents/access_rights/index.js
10
10
  import {SaveCopy, SaveRevision} from "../../exporter/native/index.js"
11
11
  import {ExportFidusFile} from "../../exporter/native/file.js"
12
12
  import {LanguageDialog, RevisionDialog} from "../../dialogs/index.js"
13
- import {E2EEKeyManager} from "../../e2ee/key-manager.js"
14
- import {PassphraseManager} from "../../e2ee/passphrase-manager.js"
15
- import {changePasswordDialog} from "../../e2ee/password-dialog.js"
13
+ import {E2EEKeyManager} from "fwtoolkit/e2ee/key-manager"
14
+ import {PassphraseManager} from "fwtoolkit/e2ee/passphrase-manager"
15
+ import {changePasswordDialog} from "fwtoolkit/e2ee/password-dialog"
16
16
  import {
17
17
  KeyBindingsDialog,
18
18
  SearchReplaceDialog,
@@ -435,7 +435,7 @@ export const headerbarModel = () => ({
435
435
  editor.e2ee.encryptionIterations
436
436
  )
437
437
  const {E2EEEncryptor} = await import(
438
- "../../e2ee/encryptor.js"
438
+ "fwtoolkit/e2ee/encryptor"
439
439
  )
440
440
  const testValue = "test"
441
441
  const encryptedTest =
@@ -1,228 +0,0 @@
1
- /**
2
- * E2EE Encryptor - Handles encryption and decryption of document content
3
- * for end-to-end encrypted documents.
4
- *
5
- * Uses AES-GCM (256-bit) for all encryption operations. Each encryption
6
- * operation generates a random 12-byte IV (initialization vector) to
7
- * ensure that encrypting the same plaintext twice produces different
8
- * ciphertext.
9
- *
10
- * Encrypted data format (Appendix A of the E2EE plan):
11
- * [IV (12 bytes)][Ciphertext (variable length)][Auth Tag (16 bytes, implicit in AES-GCM)]
12
- *
13
- * When stored as a string (e.g., in JSON fields), the entire structure
14
- * is Base64-encoded.
15
- */
16
- export class E2EEEncryptor {
17
- /**
18
- * Encrypt a string with AES-GCM.
19
- *
20
- * @param {string} plaintext - The plaintext string to encrypt
21
- * @param {CryptoKey} key - An AES-GCM key (from E2EEKeyManager.deriveKey)
22
- * @returns {Promise<string>} Base64-encoded string (iv + ciphertext + auth tag)
23
- */
24
- static async encrypt(plaintext, key) {
25
- const iv = crypto.getRandomValues(new Uint8Array(12))
26
- const encoded = new TextEncoder().encode(plaintext)
27
- const ciphertext = await crypto.subtle.encrypt(
28
- {name: "AES-GCM", iv: iv},
29
- key,
30
- encoded
31
- )
32
- // Prepend IV to ciphertext for storage
33
- const combined = new Uint8Array(iv.length + ciphertext.byteLength)
34
- combined.set(iv, 0)
35
- combined.set(new Uint8Array(ciphertext), iv.length)
36
- return E2EEEncryptor._uint8ArrayToBase64(combined)
37
- }
38
-
39
- /**
40
- * Decrypt a Base64-encoded AES-GCM ciphertext.
41
- *
42
- * @param {string} ciphertextBase64 - Base64-encoded (iv + ciphertext + auth tag)
43
- * @param {CryptoKey} key - An AES-GCM key (from E2EEKeyManager.deriveKey)
44
- * @returns {Promise<string>} The decrypted plaintext string
45
- */
46
- static async decrypt(ciphertextBase64, key) {
47
- const combined = E2EEEncryptor._base64ToUint8Array(ciphertextBase64)
48
- const iv = combined.slice(0, 12)
49
- const ciphertext = combined.slice(12)
50
- const decrypted = await crypto.subtle.decrypt(
51
- {name: "AES-GCM", iv: iv},
52
- key,
53
- ciphertext
54
- )
55
- return new TextDecoder().decode(decrypted)
56
- }
57
-
58
- /**
59
- * Encrypt a JSON-serializable object.
60
- *
61
- * Serializes the object to JSON, then encrypts the JSON string.
62
- *
63
- * @param {Object} obj - A JSON-serializable object
64
- * @param {CryptoKey} key - An AES-GCM key
65
- * @returns {Promise<string>} Base64-encoded encrypted data
66
- */
67
- static encryptObject(obj, key) {
68
- return E2EEEncryptor.encrypt(JSON.stringify(obj), key)
69
- }
70
-
71
- /**
72
- * Decrypt to a JSON-serializable object.
73
- *
74
- * Decrypts the Base64-encoded ciphertext, then parses the result as JSON.
75
- *
76
- * @param {string} ciphertextBase64 - Base64-encoded encrypted data
77
- * @param {CryptoKey} key - An AES-GCM key
78
- * @returns {Promise<Object>} The decrypted and parsed object
79
- */
80
- static async decryptObject(ciphertextBase64, key) {
81
- const plaintext = await E2EEEncryptor.decrypt(ciphertextBase64, key)
82
- return JSON.parse(plaintext)
83
- }
84
-
85
- /**
86
- * Encrypt an ArrayBuffer (for images and other binary data).
87
- *
88
- * @param {ArrayBuffer} buffer - The binary data to encrypt
89
- * @param {CryptoKey} key - An AES-GCM key
90
- * @returns {Promise<string>} Base64-encoded encrypted data (iv + ciphertext)
91
- */
92
- static async encryptBuffer(buffer, key) {
93
- const iv = crypto.getRandomValues(new Uint8Array(12))
94
- const ciphertext = await crypto.subtle.encrypt(
95
- {name: "AES-GCM", iv: iv},
96
- key,
97
- buffer
98
- )
99
- // Prepend IV to ciphertext
100
- const combined = new Uint8Array(iv.length + ciphertext.byteLength)
101
- combined.set(iv, 0)
102
- combined.set(new Uint8Array(ciphertext), iv.length)
103
- return E2EEEncryptor._uint8ArrayToBase64(combined)
104
- }
105
-
106
- /**
107
- * Decrypt to an ArrayBuffer.
108
- *
109
- * @param {string} ciphertextBase64 - Base64-encoded encrypted data
110
- * @param {CryptoKey} key - An AES-GCM key
111
- * @returns {Promise<ArrayBuffer>} The decrypted binary data
112
- */
113
- static decryptBuffer(ciphertextBase64, key) {
114
- const combined = E2EEEncryptor._base64ToUint8Array(ciphertextBase64)
115
- const iv = combined.slice(0, 12)
116
- const ciphertext = combined.slice(12)
117
- return crypto.subtle.decrypt({name: "AES-GCM", iv: iv}, key, ciphertext)
118
- }
119
-
120
- /**
121
- * Encrypt an image File/Blob for upload.
122
- *
123
- * Reads the file as an ArrayBuffer, encrypts it, and returns
124
- * a Blob with application/octet-stream type (since the encrypted
125
- * data is opaque binary).
126
- *
127
- * @param {File|Blob} file - The image file to encrypt
128
- * @param {CryptoKey} key - An AES-GCM key
129
- * @returns {Promise<Blob>} An encrypted Blob with type application/octet-stream
130
- */
131
- static async encryptImage(file, key) {
132
- const buffer = await file.arrayBuffer()
133
- const iv = crypto.getRandomValues(new Uint8Array(12))
134
- const ciphertext = await crypto.subtle.encrypt(
135
- {name: "AES-GCM", iv: iv},
136
- key,
137
- buffer
138
- )
139
- // Prepend IV to ciphertext
140
- const combined = new Uint8Array(iv.length + ciphertext.byteLength)
141
- combined.set(iv, 0)
142
- combined.set(new Uint8Array(ciphertext), iv.length)
143
- return new Blob([combined], {type: "application/octet-stream"})
144
- }
145
-
146
- /**
147
- * Decrypt an encrypted image back to an ArrayBuffer.
148
- *
149
- * @param {string} ciphertextBase64 - Base64-encoded encrypted image data
150
- * @param {CryptoKey} key - An AES-GCM key
151
- * @returns {Promise<ArrayBuffer>} The decrypted image data
152
- */
153
- static decryptImage(ciphertextBase64, key) {
154
- return E2EEEncryptor.decryptBuffer(ciphertextBase64, key)
155
- }
156
-
157
- /**
158
- * Decrypt a Base64-encoded ciphertext and return as a Base64 string.
159
- *
160
- * Useful for decrypting encrypted images that need to be stored
161
- * as Base64 data URLs or re-exported.
162
- *
163
- * @param {string} ciphertextBase64 - Base64-encoded encrypted data
164
- * @param {CryptoKey} key - An AES-GCM key
165
- * @returns {Promise<string>} Base64-encoded decrypted data
166
- */
167
- static async decryptBufferToBase64(ciphertextBase64, key) {
168
- const buffer = await E2EEEncryptor.decryptBuffer(ciphertextBase64, key)
169
- const bytes = new Uint8Array(buffer)
170
- let binary = ""
171
- for (let i = 0; i < bytes.length; i++) {
172
- binary += String.fromCharCode(bytes[i])
173
- }
174
- return btoa(binary)
175
- }
176
-
177
- /**
178
- * Decrypt an encrypted image and return a blob URL for display.
179
- *
180
- * Fetches the encrypted image file from the given URL, decrypts it,
181
- * and creates a temporary object URL that can be used as an img src.
182
- *
183
- * @param {string} imageUrl - The URL of the encrypted image file
184
- * @param {CryptoKey} key - An AES-GCM key
185
- * @param {string} mimeType - The MIME type of the decrypted image
186
- * @returns {Promise<string>} A blob URL for the decrypted image
187
- */
188
- static async decryptImageToUrl(imageUrl, key, mimeType = "image/png") {
189
- const response = await fetch(imageUrl)
190
- const arrayBuffer = await response.arrayBuffer()
191
- const bytes = new Uint8Array(arrayBuffer)
192
- let binary = ""
193
- for (let i = 0; i < bytes.length; i++) {
194
- binary += String.fromCharCode(bytes[i])
195
- }
196
- const base64 = btoa(binary)
197
- const decrypted = await E2EEEncryptor.decryptBuffer(base64, key)
198
- const blob = new Blob([decrypted], {type: mimeType})
199
- return URL.createObjectURL(blob)
200
- }
201
-
202
- // --- Private helper methods ---
203
-
204
- /**
205
- * Convert a Uint8Array to a Base64-encoded string.
206
- * @private
207
- */
208
- static _uint8ArrayToBase64(bytes) {
209
- let binary = ""
210
- for (let i = 0; i < bytes.length; i++) {
211
- binary += String.fromCharCode(bytes[i])
212
- }
213
- return btoa(binary)
214
- }
215
-
216
- /**
217
- * Convert a Base64-encoded string to a Uint8Array.
218
- * @private
219
- */
220
- static _base64ToUint8Array(base64) {
221
- const binary = atob(base64)
222
- const bytes = new Uint8Array(binary.length)
223
- for (let i = 0; i < binary.length; i++) {
224
- bytes[i] = binary.charCodeAt(i)
225
- }
226
- return bytes
227
- }
228
- }