From 18245ba3807d024763fcc7b6ad202312a688daeb Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Mon, 10 Aug 2026 00:18:18 +0100 Subject: [PATCH 1/4] Fix note screen is not properly refreshed on sync when E2EE is enabled --- packages/lib/components/shared/note-screen-shared.ts | 10 +++++++++- packages/lib/models/BaseItem.ts | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/lib/components/shared/note-screen-shared.ts b/packages/lib/components/shared/note-screen-shared.ts index 68eb39e98db..452197d6bdf 100644 --- a/packages/lib/components/shared/note-screen-shared.ts +++ b/packages/lib/components/shared/note-screen-shared.ts @@ -298,7 +298,15 @@ shared.isModified = function(comp: BaseNoteScreenComponent) { shared.reloadNote = async (comp: BaseNoteScreenComponent, useDefaultEditorState = false) => { const isProvisionalNote = comp.props.provisionalNoteIds.includes(comp.props.noteId); - const note = await Note.load(comp.props.noteId); + let note = await Note.load(comp.props.noteId); + if (note.encryption_cipher_text) { + try { + note = await Note.decrypt(note); + } catch (error) { + reg.logger().info(`Could not decrypt note ${note.id}, note could not be refreshed:`, error.message); + note = null; // fall into the non existent note handling + } + } let mode = comp.state.mode; if (useDefaultEditorState) { diff --git a/packages/lib/models/BaseItem.ts b/packages/lib/models/BaseItem.ts index e97c82052ec..88328d7d752 100644 --- a/packages/lib/models/BaseItem.ts +++ b/packages/lib/models/BaseItem.ts @@ -20,6 +20,7 @@ import { checkObjectHasProperties } from '@joplin/utils/object'; const { sprintf } = require('sprintf-js'); import moment = require('moment'); +import Note from './Note'; export interface ItemsThatNeedDecryptionResult { hasMore: boolean; @@ -571,6 +572,13 @@ export default class BaseItem extends BaseModel { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- item is a BaseItemEntity subclass with encryption_cipher_text/encryption_applied; saved back via ItemClass.save which is per-subclass public static async decrypt(item: any) { + if (item.type_ === ModelType.Note) { + // Validate if still eligible to decrypt using the latest encryption_applied value, as notes may be decrypted on demand while the decryption worker is running. + // If it has been decrypted already, avoid decrypting it again to avoid potentially overwriting it with an outdated version + const encryptionApplied = (await Note.load(item.id, { fields: ['encryption_applied'] }))?.encryption_applied; + if (!encryptionApplied) throw new Error(`Note has already been decrypted: ${item.id}`); + } + if (!item.encryption_cipher_text) throw new Error(`Item is not encrypted: ${item.id}`); const ItemClass = this.itemClass(item); From 63e1542765832e2b0313e9dbc10190869f40926a Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Mon, 10 Aug 2026 01:14:41 +0100 Subject: [PATCH 2/4] Move bypass logic to DecryptionWorker --- packages/lib/models/BaseItem.ts | 8 -------- packages/lib/services/DecryptionWorker.ts | 25 ++++++++++++++++------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/lib/models/BaseItem.ts b/packages/lib/models/BaseItem.ts index 88328d7d752..e97c82052ec 100644 --- a/packages/lib/models/BaseItem.ts +++ b/packages/lib/models/BaseItem.ts @@ -20,7 +20,6 @@ import { checkObjectHasProperties } from '@joplin/utils/object'; const { sprintf } = require('sprintf-js'); import moment = require('moment'); -import Note from './Note'; export interface ItemsThatNeedDecryptionResult { hasMore: boolean; @@ -572,13 +571,6 @@ export default class BaseItem extends BaseModel { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- item is a BaseItemEntity subclass with encryption_cipher_text/encryption_applied; saved back via ItemClass.save which is per-subclass public static async decrypt(item: any) { - if (item.type_ === ModelType.Note) { - // Validate if still eligible to decrypt using the latest encryption_applied value, as notes may be decrypted on demand while the decryption worker is running. - // If it has been decrypted already, avoid decrypting it again to avoid potentially overwriting it with an outdated version - const encryptionApplied = (await Note.load(item.id, { fields: ['encryption_applied'] }))?.encryption_applied; - if (!encryptionApplied) throw new Error(`Note has already been decrypted: ${item.id}`); - } - if (!item.encryption_cipher_text) throw new Error(`Item is not encrypted: ${item.id}`); const ItemClass = this.itemClass(item); diff --git a/packages/lib/services/DecryptionWorker.ts b/packages/lib/services/DecryptionWorker.ts index 76606d02d08..f72ffbc2f5f 100644 --- a/packages/lib/services/DecryptionWorker.ts +++ b/packages/lib/services/DecryptionWorker.ts @@ -1,5 +1,5 @@ import BaseItem, { ItemsThatNeedDecryptionResult } from '../models/BaseItem'; -import BaseModel from '../BaseModel'; +import BaseModel, { ModelType } from '../BaseModel'; import MasterKey from '../models/MasterKey'; import Resource from '../models/Resource'; import ResourceService from './ResourceService'; @@ -219,6 +219,12 @@ export default class DecryptionWorker { await this.kvStore().deleteValue(errorKey); }; + const markSuccessfulDecryption = async (decryptedItemType: number) => { + await clearDecryptionCounter(); + if (!decryptedItemCounts[decryptedItemType]) decryptedItemCounts[decryptedItemType] = 0; + decryptedItemCounts[decryptedItemType]++; + }; + // Don't log in production as it results in many messages when importing many items // this.logger().debug('DecryptionWorker: decrypting: ' + item.id + ' (' + ItemClass.tableName() + ')'); try { @@ -231,13 +237,18 @@ export default class DecryptionWorker { continue; } - const decryptedItem = await ItemClass.decrypt(item); - - await clearDecryptionCounter(); - - if (!decryptedItemCounts[decryptedItem.type_]) decryptedItemCounts[decryptedItem.type_] = 0; + if (item.type_ === ModelType.Note) { + // Validate if still eligible to decrypt using the latest encryption_applied value, as notes may be decrypted on demand while the decryption worker is running. + // If it has been decrypted already, avoid decrypting it again to avoid potentially overwriting it with an outdated version + const encryptionApplied = (await ItemClass.load(item.id, { fields: ['encryption_applied'] }))?.encryption_applied; + if (!encryptionApplied) { + await markSuccessfulDecryption(item.type_); + continue; + } + } - decryptedItemCounts[decryptedItem.type_]++; + const decryptedItem = await ItemClass.decrypt(item); + await markSuccessfulDecryption(decryptedItem.type_); if (decryptedItem.type_ === Resource.modelType() && !!decryptedItem.encryption_blob_encrypted) { // itemsThatNeedDecryption() will return the resource again if the blob has not been decrypted, From 6b30592d2eecb7484fe3b8d13e49c8111558f960 Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Mon, 10 Aug 2026 01:53:00 +0100 Subject: [PATCH 3/4] Add logging --- packages/lib/services/DecryptionWorker.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lib/services/DecryptionWorker.ts b/packages/lib/services/DecryptionWorker.ts index f72ffbc2f5f..2b886f5c151 100644 --- a/packages/lib/services/DecryptionWorker.ts +++ b/packages/lib/services/DecryptionWorker.ts @@ -242,6 +242,7 @@ export default class DecryptionWorker { // If it has been decrypted already, avoid decrypting it again to avoid potentially overwriting it with an outdated version const encryptionApplied = (await ItemClass.load(item.id, { fields: ['encryption_applied'] }))?.encryption_applied; if (!encryptionApplied) { + this.logger().info(`DecryptionWorker: Skipping decryption for note ${item.id} as it was already decrypted on demand`); await markSuccessfulDecryption(item.type_); continue; } From 1f3032782cfb55837d7ab900c4482bffbcfa1bb3 Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Mon, 10 Aug 2026 03:06:58 +0100 Subject: [PATCH 4/4] Make null safe --- packages/lib/components/shared/note-screen-shared.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/components/shared/note-screen-shared.ts b/packages/lib/components/shared/note-screen-shared.ts index 452197d6bdf..d2b4311289f 100644 --- a/packages/lib/components/shared/note-screen-shared.ts +++ b/packages/lib/components/shared/note-screen-shared.ts @@ -299,7 +299,7 @@ shared.reloadNote = async (comp: BaseNoteScreenComponent, useDefaultEditorState const isProvisionalNote = comp.props.provisionalNoteIds.includes(comp.props.noteId); let note = await Note.load(comp.props.noteId); - if (note.encryption_cipher_text) { + if (note?.encryption_cipher_text) { try { note = await Note.decrypt(note); } catch (error) {