1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Server: Fixed handling of max item size for encrypted items

This commit is contained in:
Laurent Cozic 2021-06-20 16:29:35 +01:00
parent d129744138
commit 112157e33f
4 changed files with 44 additions and 10 deletions

View File

@ -12,6 +12,12 @@ function hexPad(s: string, length: number) {
return padLeft(s, length, '0');
}
export function isValidHeaderIdentifier(id: string, ignoreTooLongLength = false) {
if (!id) return false;
if (!ignoreTooLongLength && id.length !== 5) return false;
return /JED\d\d/.test(id);
}
export default class EncryptionService {
public static instance_: EncryptionService = null;
@ -657,7 +663,7 @@ export default class EncryptionService {
async decodeHeaderSource_(source: any) {
const identifier = await source.read(5);
if (!this.isValidHeaderIdentifier(identifier)) throw new JoplinError(`Invalid encryption identifier. Data is not actually encrypted? ID was: ${identifier}`, 'invalidIdentifier');
if (!isValidHeaderIdentifier(identifier)) throw new JoplinError(`Invalid encryption identifier. Data is not actually encrypted? ID was: ${identifier}`, 'invalidIdentifier');
const mdSizeHex = await source.read(6);
const mdSize = parseInt(mdSizeHex, 16);
if (isNaN(mdSize) || !mdSize) throw new Error(`Invalid header metadata size: ${mdSizeHex}`);
@ -697,12 +703,6 @@ export default class EncryptionService {
return output;
}
isValidHeaderIdentifier(id: string, ignoreTooLongLength = false) {
if (!id) return false;
if (!ignoreTooLongLength && id.length !== 5) return false;
return /JED\d\d/.test(id);
}
isValidEncryptionMethod(method: number) {
return [EncryptionService.METHOD_SJCL, EncryptionService.METHOD_SJCL_1A, EncryptionService.METHOD_SJCL_2, EncryptionService.METHOD_SJCL_3, EncryptionService.METHOD_SJCL_4].indexOf(method) >= 0;
}
@ -711,13 +711,13 @@ export default class EncryptionService {
if (!item) throw new Error('No item');
const ItemClass = BaseItem.itemClass(item);
if (!ItemClass.encryptionSupported()) return false;
return item.encryption_applied && this.isValidHeaderIdentifier(item.encryption_cipher_text, true);
return item.encryption_applied && isValidHeaderIdentifier(item.encryption_cipher_text, true);
}
async fileIsEncrypted(path: string) {
const handle = await this.fsDriver().open(path, 'r');
const headerIdentifier = await this.fsDriver().readFileChunk(handle, 5, 'ascii');
await this.fsDriver().close(handle);
return this.isValidHeaderIdentifier(headerIdentifier);
return isValidHeaderIdentifier(headerIdentifier);
}
}

View File

@ -5,6 +5,7 @@ import { ErrorUnprocessableEntity, ErrorForbidden, ErrorPayloadTooLarge, ErrorNo
import { ModelType } from '@joplin/lib/BaseModel';
import { _ } from '@joplin/lib/locale';
import { formatBytes, MB } from '../utils/bytes';
import { itemIsEncrypted } from '../utils/joplinUtils';
export enum AccountType {
Default = 0,
@ -147,7 +148,8 @@ export default class UserModel extends BaseModel<User> {
// If the item is encrypted, we apply a multipler because encrypted
// items can be much larger (seems to be up to twice the size but for
// safety let's go with 2.2).
const maxSize = user.max_item_size * (item.jop_encryption_applied ? 2.2 : 1);
const maxSize = user.max_item_size * (itemIsEncrypted(item) ? 2.2 : 1);
if (maxSize && buffer.byteLength > maxSize) {
const itemTitle = joplinItem ? joplinItem.title || '' : '';
const isNote = joplinItem && joplinItem.type_ === ModelType.Note;

View File

@ -0,0 +1,24 @@
import { Item } from '../db';
import { itemIsEncrypted } from './joplinUtils';
import { expectThrow } from './testing/testUtils';
describe('joplinUtils', function() {
it('should check if an item is encrypted', async function() {
type TestCase = [boolean, Item];
const testCases: TestCase[] = [
[true, { jop_encryption_applied: 1 }],
[false, { jop_encryption_applied: 0 }],
[true, { content: Buffer.from('JED01blablablabla', 'utf8') }],
[false, { content: Buffer.from('plain text', 'utf8') }],
];
for (const [expected, input] of testCases) {
expect(itemIsEncrypted(input)).toBe(expected);
}
await expectThrow(async () => itemIsEncrypted({ name: 'missing props' }));
});
});

View File

@ -18,6 +18,7 @@ import { formatDateTime } from './time';
import { ErrorNotFound } from './errors';
import { MarkupToHtml } from '@joplin/renderer';
import { OptionsResourceModel } from '@joplin/renderer/MarkupToHtml';
import { isValidHeaderIdentifier } from '@joplin/lib/services/EncryptionService';
const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js');
import { themeStyle } from '@joplin/lib/theme';
import Setting from '@joplin/lib/models/Setting';
@ -221,6 +222,13 @@ async function renderNote(share: Share, note: NoteEntity, resourceInfos: Resourc
};
}
export function itemIsEncrypted(item: Item): boolean {
if ('jop_encryption_applied' in item) return !!item.jop_encryption_applied;
if (!('content' in item)) throw new Error('Cannot check encryption - item is missing both "content" and "jop_encryption_applied" property');
const header = item.content.toString('utf8', 0, 5);
return isValidHeaderIdentifier(header);
}
export async function renderItem(userId: Uuid, item: Item, share: Share, query: Record<string, any>): Promise<FileViewerResponse> {
const rootNote: NoteEntity = models_.item().itemToJoplinItem(item); // await this.unserializeItem(content);
const linkedItemInfos: LinkedItemInfos = await noteLinkedItemInfos(userId, models_.item(), rootNote);