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 {
|
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) {
|
} catch (error) {
|
||||||
if (error.code === 'invalidIdentifier') {
|
if (error.code === 'invalidIdentifier') {
|
||||||
// As the identifier is invalid it most likely means that this is not encrypted data
|
// 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 };
|
if (resource.encryption_blob_encrypted) return { path: encryptedPath, resource: resource };
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
if (error.code === 'ENOENT') throw new JoplinError('File not found:' + error.toString(), 'fileNotFound');
|
if (error.code === 'ENOENT') throw new JoplinError('File not found:' + error.toString(), 'fileNotFound');
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -14,9 +14,20 @@ function hexPad(s, length) {
|
|||||||
class EncryptionService {
|
class EncryptionService {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Note: 1 MB is very slow with Node and probably even worse on mobile. 50 KB seems to work well
|
// Note: 1 MB is very slow with Node and probably even worse on mobile.
|
||||||
// and doesn't produce too much overhead in terms of headers.
|
//
|
||||||
this.chunkSize_ = 50000;
|
// 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.loadedMasterKeys_ = {};
|
||||||
this.activeMasterKeyId_ = null;
|
this.activeMasterKeyId_ = null;
|
||||||
this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL;
|
this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL;
|
||||||
@ -299,7 +310,9 @@ class EncryptionService {
|
|||||||
throw new Error('Unknown decryption method: ' + method);
|
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 method = this.defaultEncryptionMethod();
|
||||||
const masterKeyId = this.activeMasterKeyId();
|
const masterKeyId = this.activeMasterKeyId();
|
||||||
const masterKeyPlainText = this.loadedMasterKey(masterKeyId);
|
const masterKeyPlainText = this.loadedMasterKey(masterKeyId);
|
||||||
@ -311,17 +324,29 @@ class EncryptionService {
|
|||||||
|
|
||||||
await destination.append(this.encodeHeader_(header));
|
await destination.append(this.encodeHeader_(header));
|
||||||
|
|
||||||
|
let doneSize = 0;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const block = await source.read(this.chunkSize_);
|
const block = await source.read(this.chunkSize_);
|
||||||
if (!block) break;
|
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);
|
const encrypted = await this.encrypt(method, masterKeyPlainText, block);
|
||||||
|
|
||||||
await destination.append(padLeft(encrypted.length.toString(16), 6, '0'));
|
await destination.append(padLeft(encrypted.length.toString(16), 6, '0'));
|
||||||
await destination.append(encrypted);
|
await destination.append(encrypted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptAbstract_(source, destination) {
|
async decryptAbstract_(source, destination, options = null) {
|
||||||
|
if (!options) options = {};
|
||||||
|
|
||||||
const identifier = await source.read(5);
|
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 (!this.isValidHeaderIdentifier(identifier)) throw new JoplinError('Invalid encryption identifier. Data is not actually encrypted? ID was: ' + identifier, 'invalidIdentifier');
|
||||||
const mdSizeHex = await source.read(6);
|
const mdSizeHex = await source.read(6);
|
||||||
@ -331,6 +356,8 @@ class EncryptionService {
|
|||||||
const header = this.decodeHeader_(identifier + mdSizeHex + md);
|
const header = this.decodeHeader_(identifier + mdSizeHex + md);
|
||||||
const masterKeyPlainText = this.loadedMasterKey(header.masterKeyId);
|
const masterKeyPlainText = this.loadedMasterKey(header.masterKeyId);
|
||||||
|
|
||||||
|
let doneSize = 0;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const lengthHex = await source.read(6);
|
const lengthHex = await source.read(6);
|
||||||
if (!lengthHex) break;
|
if (!lengthHex) break;
|
||||||
@ -338,6 +365,11 @@ class EncryptionService {
|
|||||||
const length = parseInt(lengthHex, 16);
|
const length = parseInt(lengthHex, 16);
|
||||||
if (!length) continue; // Weird but could be not completely invalid (block of size 0) so continue decrypting
|
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 block = await source.read(length);
|
||||||
|
|
||||||
const plainText = await this.decrypt(header.encryptionMethod, masterKeyPlainText, block);
|
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 source = this.stringReader_(plainText);
|
||||||
const destination = this.stringWriter_();
|
const destination = this.stringWriter_();
|
||||||
await this.encryptAbstract_(source, destination);
|
await this.encryptAbstract_(source, destination, options);
|
||||||
return destination.result();
|
return destination.result();
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptString(cipherText) {
|
async decryptString(cipherText, options = null) {
|
||||||
const source = this.stringReader_(cipherText);
|
const source = this.stringReader_(cipherText);
|
||||||
const destination = this.stringWriter_();
|
const destination = this.stringWriter_();
|
||||||
await this.decryptAbstract_(source, destination);
|
await this.decryptAbstract_(source, destination, options);
|
||||||
return destination.data.join('');
|
return destination.data.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
async encryptFile(srcPath, destPath) {
|
async encryptFile(srcPath, destPath, options = null) {
|
||||||
let source = await this.fileReader_(srcPath, 'base64');
|
let source = await this.fileReader_(srcPath, 'base64');
|
||||||
let destination = await this.fileWriter_(destPath, 'ascii');
|
let destination = await this.fileWriter_(destPath, 'ascii');
|
||||||
|
|
||||||
@ -422,7 +454,7 @@ class EncryptionService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.fsDriver().unlink(destPath);
|
await this.fsDriver().unlink(destPath);
|
||||||
await this.encryptAbstract_(source, destination);
|
await this.encryptAbstract_(source, destination, options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
cleanUp();
|
cleanUp();
|
||||||
await this.fsDriver().unlink(destPath);
|
await this.fsDriver().unlink(destPath);
|
||||||
@ -432,7 +464,7 @@ class EncryptionService {
|
|||||||
await cleanUp();
|
await cleanUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptFile(srcPath, destPath) {
|
async decryptFile(srcPath, destPath, options = null) {
|
||||||
let source = await this.fileReader_(srcPath, 'ascii');
|
let source = await this.fileReader_(srcPath, 'ascii');
|
||||||
let destination = await this.fileWriter_(destPath, 'base64');
|
let destination = await this.fileWriter_(destPath, 'base64');
|
||||||
|
|
||||||
@ -445,7 +477,7 @@ class EncryptionService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.fsDriver().unlink(destPath);
|
await this.fsDriver().unlink(destPath);
|
||||||
await this.decryptAbstract_(source, destination);
|
await this.decryptAbstract_(source, destination, options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
cleanUp();
|
cleanUp();
|
||||||
await this.fsDriver().unlink(destPath);
|
await this.fsDriver().unlink(destPath);
|
||||||
|
@ -300,6 +300,8 @@ function shimInit() {
|
|||||||
bridge().openExternal(url)
|
bridge().openExternal(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shim.waitForFrame = () => {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { shimInit };
|
module.exports = { shimInit };
|
@ -123,6 +123,12 @@ function shimInit() {
|
|||||||
shim.openUrl = (url) => {
|
shim.openUrl = (url) => {
|
||||||
Linking.openURL(url);
|
Linking.openURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shim.waitForFrame = () => {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
requestAnimationFrame(function() { resolve(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { shimInit };
|
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.imageFromDataUrl = async function(imageDataUrl, filePath, options = null) { throw new Error('Not implemented') }
|
||||||
shim.Buffer = null;
|
shim.Buffer = null;
|
||||||
shim.openUrl = () => { throw new Error('Not implemented'); }
|
shim.openUrl = () => { throw new Error('Not implemented'); }
|
||||||
|
shim.waitForFrame = () => { throw new Error('Not implemented'); }
|
||||||
|
|
||||||
module.exports = { shim };
|
module.exports = { shim };
|
Loading…
Reference in New Issue
Block a user