mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-03 08:35:29 +02:00
All: Optimised encryption and decryption of items so that it doesn't freeze the UI, especially on mobile
This commit is contained in:
parent
84adf64271
commit
fea83e28c4
@ -73,7 +73,12 @@ class Resource extends BaseItem {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.encryptionService().decryptFile(encryptedPath, plainTextPath);
|
||||
// const stat = await this.fsDriver().stat(encryptedPath);
|
||||
await this.encryptionService().decryptFile(encryptedPath, plainTextPath, {
|
||||
// onProgress: (progress) => {
|
||||
// console.info('Decryption: ', progress.doneSize / stat.size);
|
||||
// },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'invalidIdentifier') {
|
||||
// As the identifier is invalid it most likely means that this is not encrypted data
|
||||
@ -108,7 +113,12 @@ class Resource extends BaseItem {
|
||||
if (resource.encryption_blob_encrypted) return { path: encryptedPath, resource: resource };
|
||||
|
||||
try {
|
||||
await this.encryptionService().encryptFile(plainTextPath, encryptedPath);
|
||||
// const stat = await this.fsDriver().stat(plainTextPath);
|
||||
await this.encryptionService().encryptFile(plainTextPath, encryptedPath, {
|
||||
// onProgress: (progress) => {
|
||||
// console.info(progress.doneSize / stat.size);
|
||||
// },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') throw new JoplinError('File not found:' + error.toString(), 'fileNotFound');
|
||||
throw error;
|
||||
|
@ -14,9 +14,20 @@ function hexPad(s, length) {
|
||||
class EncryptionService {
|
||||
|
||||
constructor() {
|
||||
// Note: 1 MB is very slow with Node and probably even worse on mobile. 50 KB seems to work well
|
||||
// and doesn't produce too much overhead in terms of headers.
|
||||
this.chunkSize_ = 50000;
|
||||
// Note: 1 MB is very slow with Node and probably even worse on mobile.
|
||||
//
|
||||
// On mobile the time it takes to decrypt increases exponentially for some reason, so it's important
|
||||
// to have a relatively small size so as not to freeze the app. For example, on Android 7.1 simulator
|
||||
// with 4.1 GB RAM, it takes this much to decrypt a block;
|
||||
//
|
||||
// 50KB => 1000 ms
|
||||
// 25KB => 250ms
|
||||
// 10KB => 200ms
|
||||
// 5KB => 10ms
|
||||
//
|
||||
// So making the block 10 times smaller make it 100 times faster! So for now using 5KB. This can be
|
||||
// changed easily since the chunk size is incorporated into the encrypted data.
|
||||
this.chunkSize_ = 5000;
|
||||
this.loadedMasterKeys_ = {};
|
||||
this.activeMasterKeyId_ = null;
|
||||
this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL;
|
||||
@ -299,7 +310,9 @@ class EncryptionService {
|
||||
throw new Error('Unknown decryption method: ' + method);
|
||||
}
|
||||
|
||||
async encryptAbstract_(source, destination) {
|
||||
async encryptAbstract_(source, destination, options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
const method = this.defaultEncryptionMethod();
|
||||
const masterKeyId = this.activeMasterKeyId();
|
||||
const masterKeyPlainText = this.loadedMasterKey(masterKeyId);
|
||||
@ -311,17 +324,29 @@ class EncryptionService {
|
||||
|
||||
await destination.append(this.encodeHeader_(header));
|
||||
|
||||
let doneSize = 0;
|
||||
|
||||
while (true) {
|
||||
const block = await source.read(this.chunkSize_);
|
||||
if (!block) break;
|
||||
|
||||
doneSize += this.chunkSize_;
|
||||
if (options.onProgress) options.onProgress({ doneSize: doneSize });
|
||||
|
||||
// Wait for a frame so that the app remains responsive in mobile.
|
||||
// https://corbt.com/posts/2015/12/22/breaking-up-heavy-processing-in-react-native.html
|
||||
await shim.waitForFrame();
|
||||
|
||||
const encrypted = await this.encrypt(method, masterKeyPlainText, block);
|
||||
|
||||
await destination.append(padLeft(encrypted.length.toString(16), 6, '0'));
|
||||
await destination.append(encrypted);
|
||||
}
|
||||
}
|
||||
|
||||
async decryptAbstract_(source, destination) {
|
||||
async decryptAbstract_(source, destination, options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
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');
|
||||
const mdSizeHex = await source.read(6);
|
||||
@ -331,6 +356,8 @@ class EncryptionService {
|
||||
const header = this.decodeHeader_(identifier + mdSizeHex + md);
|
||||
const masterKeyPlainText = this.loadedMasterKey(header.masterKeyId);
|
||||
|
||||
let doneSize = 0;
|
||||
|
||||
while (true) {
|
||||
const lengthHex = await source.read(6);
|
||||
if (!lengthHex) break;
|
||||
@ -338,6 +365,11 @@ class EncryptionService {
|
||||
const length = parseInt(lengthHex, 16);
|
||||
if (!length) continue; // Weird but could be not completely invalid (block of size 0) so continue decrypting
|
||||
|
||||
doneSize += length;
|
||||
if (options.onProgress) options.onProgress({ doneSize: doneSize });
|
||||
|
||||
await shim.waitForFrame();
|
||||
|
||||
const block = await source.read(length);
|
||||
|
||||
const plainText = await this.decrypt(header.encryptionMethod, masterKeyPlainText, block);
|
||||
@ -395,21 +427,21 @@ class EncryptionService {
|
||||
};
|
||||
}
|
||||
|
||||
async encryptString(plainText) {
|
||||
async encryptString(plainText, options = null) {
|
||||
const source = this.stringReader_(plainText);
|
||||
const destination = this.stringWriter_();
|
||||
await this.encryptAbstract_(source, destination);
|
||||
await this.encryptAbstract_(source, destination, options);
|
||||
return destination.result();
|
||||
}
|
||||
|
||||
async decryptString(cipherText) {
|
||||
async decryptString(cipherText, options = null) {
|
||||
const source = this.stringReader_(cipherText);
|
||||
const destination = this.stringWriter_();
|
||||
await this.decryptAbstract_(source, destination);
|
||||
await this.decryptAbstract_(source, destination, options);
|
||||
return destination.data.join('');
|
||||
}
|
||||
|
||||
async encryptFile(srcPath, destPath) {
|
||||
async encryptFile(srcPath, destPath, options = null) {
|
||||
let source = await this.fileReader_(srcPath, 'base64');
|
||||
let destination = await this.fileWriter_(destPath, 'ascii');
|
||||
|
||||
@ -422,7 +454,7 @@ class EncryptionService {
|
||||
|
||||
try {
|
||||
await this.fsDriver().unlink(destPath);
|
||||
await this.encryptAbstract_(source, destination);
|
||||
await this.encryptAbstract_(source, destination, options);
|
||||
} catch (error) {
|
||||
cleanUp();
|
||||
await this.fsDriver().unlink(destPath);
|
||||
@ -432,7 +464,7 @@ class EncryptionService {
|
||||
await cleanUp();
|
||||
}
|
||||
|
||||
async decryptFile(srcPath, destPath) {
|
||||
async decryptFile(srcPath, destPath, options = null) {
|
||||
let source = await this.fileReader_(srcPath, 'ascii');
|
||||
let destination = await this.fileWriter_(destPath, 'base64');
|
||||
|
||||
@ -445,7 +477,7 @@ class EncryptionService {
|
||||
|
||||
try {
|
||||
await this.fsDriver().unlink(destPath);
|
||||
await this.decryptAbstract_(source, destination);
|
||||
await this.decryptAbstract_(source, destination, options);
|
||||
} catch (error) {
|
||||
cleanUp();
|
||||
await this.fsDriver().unlink(destPath);
|
||||
|
@ -300,6 +300,8 @@ function shimInit() {
|
||||
bridge().openExternal(url)
|
||||
}
|
||||
|
||||
shim.waitForFrame = () => {}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { shimInit };
|
@ -123,6 +123,12 @@ function shimInit() {
|
||||
shim.openUrl = (url) => {
|
||||
Linking.openURL(url);
|
||||
}
|
||||
|
||||
shim.waitForFrame = () => {
|
||||
return new Promise(function(resolve, reject) {
|
||||
requestAnimationFrame(function() { resolve(); });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { shimInit };
|
@ -139,5 +139,6 @@ shim.attachFileToNote = async (note, filePath) => {}
|
||||
shim.imageFromDataUrl = async function(imageDataUrl, filePath, options = null) { throw new Error('Not implemented') }
|
||||
shim.Buffer = null;
|
||||
shim.openUrl = () => { throw new Error('Not implemented'); }
|
||||
shim.waitForFrame = () => { throw new Error('Not implemented'); }
|
||||
|
||||
module.exports = { shim };
|
Loading…
Reference in New Issue
Block a user