mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-27 10:32:58 +02:00
286 lines
11 KiB
TypeScript
286 lines
11 KiB
TypeScript
|
import EncryptionService, { EncryptionMethod } from './EncryptionService';
|
||
|
import BaseItem from '../../models/BaseItem';
|
||
|
import Folder from '../../models/Folder';
|
||
|
import MasterKey from '../../models/MasterKey';
|
||
|
import Note from '../../models/Note';
|
||
|
import Setting from '../../models/Setting';
|
||
|
import shim from '../..//shim';
|
||
|
import { getEncryptionEnabled, setEncryptionEnabled } from '../synchronizer/syncInfoUtils';
|
||
|
import Logger from '@joplin/utils/Logger';
|
||
|
|
||
|
interface DecryptTestData {
|
||
|
method: EncryptionMethod;
|
||
|
password: string;
|
||
|
plaintext: string;
|
||
|
ciphertext: string;
|
||
|
}
|
||
|
|
||
|
let serviceInstance: EncryptionService = null;
|
||
|
|
||
|
const logger = Logger.create('Crypto Tests');
|
||
|
|
||
|
// This is convenient to quickly generate some data to verify for example that
|
||
|
// react-native-quick-crypto and node:crypto can decrypt the same data.
|
||
|
export async function createDecryptTestData() {
|
||
|
const method = EncryptionMethod.StringV1;
|
||
|
const password = 'just testing';
|
||
|
const plaintext = '中文にっぽんご한국어😀\uD83D\0\r\nenglish01234567890';
|
||
|
const ciphertext = await serviceInstance.encrypt(method, password, plaintext);
|
||
|
|
||
|
return {
|
||
|
method: method,
|
||
|
password: password,
|
||
|
plaintext: plaintext,
|
||
|
ciphertext: ciphertext,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
interface CheckTestDataOptions {
|
||
|
throwOnError?: boolean;
|
||
|
silent?: boolean;
|
||
|
testLabel?: string;
|
||
|
}
|
||
|
|
||
|
export async function checkDecryptTestData(data: DecryptTestData, options: CheckTestDataOptions = null) {
|
||
|
options = {
|
||
|
throwOnError: false,
|
||
|
silent: false,
|
||
|
...options,
|
||
|
};
|
||
|
|
||
|
// Verify that the ciphertext decrypted on this device and producing the same plaintext.
|
||
|
const messages: string[] = [];
|
||
|
let hasError = false;
|
||
|
|
||
|
try {
|
||
|
const decrypted = await EncryptionService.instance().decrypt(data.method, data.password, data.ciphertext);
|
||
|
if (decrypted !== data.plaintext) {
|
||
|
messages.push('Data could not be decrypted');
|
||
|
messages.push('Expected:', data.plaintext);
|
||
|
messages.push('Got:', decrypted);
|
||
|
hasError = true;
|
||
|
} else {
|
||
|
messages.push('Data could be decrypted');
|
||
|
}
|
||
|
} catch (error) {
|
||
|
hasError = true;
|
||
|
messages.push(`Failed to decrypt data: Error: ${error}`);
|
||
|
}
|
||
|
|
||
|
if (hasError && options.throwOnError) {
|
||
|
const label = options.testLabel ? ` (test ${options.testLabel})` : '';
|
||
|
throw new Error(`Testing Crypto failed${label}: \n${messages.join('\n')}`);
|
||
|
} else {
|
||
|
for (const msg of messages) {
|
||
|
if (hasError) {
|
||
|
logger.warn(msg);
|
||
|
} else {
|
||
|
if (!options.silent) logger.info(msg);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export async function testStringPerformance(method: EncryptionMethod, dataSize: number, count: number, options: CheckTestDataOptions = null) {
|
||
|
options = {
|
||
|
throwOnError: false,
|
||
|
silent: false,
|
||
|
...options,
|
||
|
};
|
||
|
|
||
|
// Verify that the ciphertext decrypted on this device and producing the same plaintext.
|
||
|
const messages: string[] = [];
|
||
|
let hasError = false;
|
||
|
|
||
|
try {
|
||
|
serviceInstance.defaultEncryptionMethod_ = method;
|
||
|
let masterKey = await serviceInstance.generateMasterKey('123456');
|
||
|
masterKey = await MasterKey.save(masterKey);
|
||
|
await serviceInstance.loadMasterKey(masterKey, '123456', true);
|
||
|
|
||
|
const crypto = shim.crypto;
|
||
|
|
||
|
const content = (await crypto.randomBytes(dataSize / 2)).toString('hex');
|
||
|
const folder = await Folder.save({ title: 'folder' });
|
||
|
const note = await Note.save({ title: 'encrypted note', body: content, parent_id: folder.id });
|
||
|
|
||
|
let encryptTime = 0.0;
|
||
|
let decryptTime = 0.0;
|
||
|
for (let i = 0; i < count; i++) {
|
||
|
const tick1 = performance.now();
|
||
|
const serialized = await Note.serializeForSync(note);
|
||
|
const tick2 = performance.now();
|
||
|
const deserialized = await Note.unserialize(serialized);
|
||
|
const decryptedNote = await Note.decrypt(deserialized);
|
||
|
const tick3 = performance.now();
|
||
|
(decryptedNote.title === note.title);
|
||
|
encryptTime += tick2 - tick1;
|
||
|
decryptTime += tick3 - tick2;
|
||
|
}
|
||
|
|
||
|
messages.push(`testStringPerformance(): method: ${method}, count: ${count}, dataSize: ${dataSize}, encryptTime: ${encryptTime}, decryptTime: ${decryptTime}, encryptTime/count: ${encryptTime / count}, decryptTime/count: ${decryptTime / count}.`);
|
||
|
|
||
|
} catch (error) {
|
||
|
hasError = true;
|
||
|
messages.push(`testStringPerformance() failed. Error: ${error}`);
|
||
|
}
|
||
|
|
||
|
if (hasError && options.throwOnError) {
|
||
|
const label = options.testLabel ? ` (test ${options.testLabel})` : '';
|
||
|
throw new Error(`Testing Crypto failed${label}: \n${messages.join('\n')}`);
|
||
|
} else {
|
||
|
for (const msg of messages) {
|
||
|
if (hasError) {
|
||
|
logger.warn(msg);
|
||
|
} else {
|
||
|
if (!options.silent) logger.info(msg);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export async function testFilePerformance(method: EncryptionMethod, dataSize: number, count: number, options: CheckTestDataOptions = null) {
|
||
|
options = {
|
||
|
throwOnError: false,
|
||
|
silent: false,
|
||
|
...options,
|
||
|
};
|
||
|
|
||
|
// Verify that the ciphertext decrypted on this device and producing the same plaintext.
|
||
|
const messages: string[] = [];
|
||
|
let hasError = false;
|
||
|
|
||
|
try {
|
||
|
serviceInstance.defaultFileEncryptionMethod_ = method;
|
||
|
let masterKey = await serviceInstance.generateMasterKey('123456');
|
||
|
masterKey = await MasterKey.save(masterKey);
|
||
|
await serviceInstance.loadMasterKey(masterKey, '123456', true);
|
||
|
|
||
|
const fsDriver = shim.fsDriver();
|
||
|
const crypto = shim.crypto;
|
||
|
|
||
|
const sourcePath = `${Setting.value('tempDir')}/testData.txt`;
|
||
|
const encryptedPath = `${Setting.value('tempDir')}/testData.crypted`;
|
||
|
const decryptedPath = `${Setting.value('tempDir')}/testData.decrypted`;
|
||
|
await fsDriver.writeFile(sourcePath, '');
|
||
|
await fsDriver.appendFile(sourcePath, (await crypto.randomBytes(dataSize)).toString('base64'), 'base64');
|
||
|
|
||
|
let encryptTime = 0.0;
|
||
|
let decryptTime = 0.0;
|
||
|
for (let i = 0; i < count; i++) {
|
||
|
const tick1 = performance.now();
|
||
|
await serviceInstance.encryptFile(sourcePath, encryptedPath);
|
||
|
const tick2 = performance.now();
|
||
|
await serviceInstance.decryptFile(encryptedPath, decryptedPath);
|
||
|
const tick3 = performance.now();
|
||
|
encryptTime += tick2 - tick1;
|
||
|
decryptTime += tick3 - tick2;
|
||
|
}
|
||
|
|
||
|
messages.push(`testFilePerformance(): method: ${method}, count: ${count}, dataSize: ${dataSize}, encryptTime: ${encryptTime}, decryptTime: ${decryptTime}, encryptTime/count: ${encryptTime / count}, decryptTime/count: ${decryptTime / count}.`);
|
||
|
|
||
|
} catch (error) {
|
||
|
hasError = true;
|
||
|
messages.push(`testFilePerformance() failed. Error: ${error}`);
|
||
|
}
|
||
|
|
||
|
if (hasError && options.throwOnError) {
|
||
|
const label = options.testLabel ? ` (test ${options.testLabel})` : '';
|
||
|
throw new Error(`Testing Crypto failed${label}: \n${messages.join('\n')}`);
|
||
|
} else {
|
||
|
for (const msg of messages) {
|
||
|
if (hasError) {
|
||
|
logger.warn(msg);
|
||
|
} else {
|
||
|
if (!options.silent) logger.info(msg);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// cSpell:disable
|
||
|
|
||
|
// Data generated on desktop, using node:crypto in packages/lib/services/e2ee/crypto.ts
|
||
|
const decryptTestData: Record<string, DecryptTestData> = {
|
||
|
shortString: {
|
||
|
method: EncryptionMethod.StringV1,
|
||
|
password: '4BfJl8YbM,nXx.LVgs!AzkWWA]',
|
||
|
plaintext: '中文にっぽんご한국어😀\uD83D\0\r\nenglish01234567890',
|
||
|
ciphertext: '{"salt":"6NKebMdcrFSGSzEWusbY8JUfG9rB98PxNZtk0QkYEFQ=","iv":"zHdXu8V5SYsO+vR/","ct":"4C3uyjOtRsNQZxCECvCeRaP+oPXMxjUMxsND67odnyiFg2A+fG6QW6O8axb6RHWU7QKHRG9/kHEs283DHL3hOAJbl4LS47R/dEDJbl8kWmGtLAsn"}',
|
||
|
},
|
||
|
hexKey: {
|
||
|
method: EncryptionMethod.KeyV1,
|
||
|
password: '4BfJl8YbM,nXx.LVgs!AzkWWA]',
|
||
|
plaintext: 'ea5d8dd43823a812c9a64f4eb09b57e1aa2dbfcda50bf9e416dcd1f8c569a6462f7f61861c3ccda0c7c16bf4192ed9c7ecaf3e1248517bd6f397d1080d2632fc69e1ead59a398a07e7478c8d90142745c0dce39c2105b6d34117424697de03879caa3b6410325f14dc5755b69efa944eb83f06970d04444e83fe054576af20576c0f3a5dc23e0d6dcfa3e84ec05c21139070c0809bd2bdc86a7782368c9d99eb072c858c61ec8926136e6e50dfd57b7e8e0084ad08b2d984db0436f50433cab44b3c462ccd22a8567c8ff86675fff618b11526030f09f735646a9f189f54ba5485d069ee25ac468ec0a873c1223eed34962bd219972020cc147041d4b00a3ab8',
|
||
|
ciphertext: '{"salt":"iv5kKP+scMyXKO2jqzef3q9y9p/o/mj6zAoKVltbPx0=","iv":"Gz1PtbDzoaZzRx5m","ct":"YSS4ga1Q0MeVpFbMn2V45TaAbbuJM12vU/qYQ/VEGYPXhNQ4YchfRt7+LjhVxwpvfc/rD0znt6kpAh4ROGS2CLDy27/n5VICgTVY2VVff+YjPAma6odKn2pm4Z88fZlkoJVLi7QyU96Mvb6bYVbuNjQ16hOjFQ3iIIztLcafsHaW6v6gUFrDWYVQPi1/xovmmCe/GaM3JeMye5QQiFrmLQIxEJMNv8YiNyppMVf5b1YGFtDlOjm3XE2H9bb+wWNAd82mcwcAe+ZUedz3AH9PKlsRyBGHfGQ/rfFzeoFj+Wjm4fvPniPa1muRSMQDHU2Zw5YGWQwMNVUHt+y7lDoqPF2NQv4DvPmY1kLz2yohIzc="}',
|
||
|
},
|
||
|
base64File: {
|
||
|
method: EncryptionMethod.FileV1,
|
||
|
password: '4BfJl8YbM,nXx.LVgs!AzkWWA]',
|
||
|
plaintext: '5Lit5paH44Gr44Gj44G944KT44GU7ZWc6rWt7Ja08J+YgO+/vQANCmVuZ2xpc2gwMTIzNDU2Nzg5MA==',
|
||
|
ciphertext: '{"salt":"19Zx/+hxpZ+Trc7MGBt837SsTOJjHe9aiY5UPnXP6Oo=","iv":"N4PTzsyh4wONNJWa","ct":"LxAibOnVox1q2WBtLAKxeZxIIxKOEd6xdD3NKAn5mgHhv4i60yPiyPbr8rS+MzHmeq7Z3BhHjR7540rtdeBugbmf1+b3tYuRudI="}',
|
||
|
},
|
||
|
};
|
||
|
|
||
|
// cSpell:enable
|
||
|
|
||
|
// This can be used to run integration tests directly on device. It will throw
|
||
|
// an error if something cannot be decrypted, or else print info messages.
|
||
|
export const runIntegrationTests = async (silent = false, testPerformance = false) => {
|
||
|
const log = (s: string) => {
|
||
|
if (silent) return;
|
||
|
logger.info(s);
|
||
|
};
|
||
|
|
||
|
log('Running integration tests...');
|
||
|
const encryptionEnabled = getEncryptionEnabled();
|
||
|
serviceInstance = EncryptionService.instance();
|
||
|
BaseItem.encryptionService_ = EncryptionService.instance();
|
||
|
setEncryptionEnabled(true);
|
||
|
|
||
|
log('Decrypting using known data...');
|
||
|
for (const testLabel in decryptTestData) {
|
||
|
log(`Running decrypt test data case ${testLabel}...`);
|
||
|
await checkDecryptTestData(decryptTestData[testLabel], { silent, testLabel, throwOnError: true });
|
||
|
}
|
||
|
|
||
|
log('Decrypting using local data...');
|
||
|
const newData = await createDecryptTestData();
|
||
|
await checkDecryptTestData(newData, { silent, throwOnError: true });
|
||
|
|
||
|
// The performance test is very slow so it is disabled by default.
|
||
|
if (testPerformance) {
|
||
|
log('Testing performance...');
|
||
|
if (shim.mobilePlatform() === '') {
|
||
|
await testStringPerformance(EncryptionMethod.StringV1, 100, 1000);
|
||
|
await testStringPerformance(EncryptionMethod.StringV1, 1000000, 10);
|
||
|
await testStringPerformance(EncryptionMethod.StringV1, 5000000, 10);
|
||
|
await testStringPerformance(EncryptionMethod.SJCL1a, 100, 1000);
|
||
|
await testStringPerformance(EncryptionMethod.SJCL1a, 1000000, 10);
|
||
|
await testStringPerformance(EncryptionMethod.SJCL1a, 5000000, 10);
|
||
|
await testFilePerformance(EncryptionMethod.FileV1, 100, 1000);
|
||
|
await testFilePerformance(EncryptionMethod.FileV1, 1000000, 3);
|
||
|
await testFilePerformance(EncryptionMethod.FileV1, 5000000, 3);
|
||
|
await testFilePerformance(EncryptionMethod.SJCL1a, 100, 1000);
|
||
|
await testFilePerformance(EncryptionMethod.SJCL1a, 1000000, 3);
|
||
|
await testFilePerformance(EncryptionMethod.SJCL1a, 5000000, 3);
|
||
|
} else {
|
||
|
await testStringPerformance(EncryptionMethod.StringV1, 100, 100);
|
||
|
await testStringPerformance(EncryptionMethod.StringV1, 500000, 3);
|
||
|
await testStringPerformance(EncryptionMethod.StringV1, 1000000, 3);
|
||
|
await testStringPerformance(EncryptionMethod.SJCL1a, 100, 100);
|
||
|
await testStringPerformance(EncryptionMethod.SJCL1a, 500000, 3);
|
||
|
await testStringPerformance(EncryptionMethod.SJCL1a, 1000000, 3);
|
||
|
await testFilePerformance(EncryptionMethod.FileV1, 100, 100);
|
||
|
await testFilePerformance(EncryptionMethod.FileV1, 100000, 3);
|
||
|
await testFilePerformance(EncryptionMethod.FileV1, 500000, 3);
|
||
|
await testFilePerformance(EncryptionMethod.SJCL1a, 100, 100);
|
||
|
await testFilePerformance(EncryptionMethod.SJCL1a, 100000, 3);
|
||
|
await testFilePerformance(EncryptionMethod.SJCL1a, 500000, 3);
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
setEncryptionEnabled(encryptionEnabled);
|
||
|
};
|