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

All: Refactored encryption/decryption method to use same algorithm for both file and string encryption

This commit is contained in:
Laurent Cozic 2017-12-18 20:54:03 +01:00
parent 4c0b472f67
commit 3f4f154949
5 changed files with 161 additions and 65 deletions

View File

@ -1,7 +1,7 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
@ -160,4 +160,22 @@ describe('Encryption', function() {
done();
});
it('should encrypt and decrypt files', async (done) => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
const sourcePath = __dirname + '/../tests/support/photo.jpg';
const encryptedPath = __dirname + '/data/photo.crypted';
const decryptedPath = __dirname + '/data/photo.jpg';
await service.encryptFile(sourcePath, encryptedPath);
await service.decryptFile(encryptedPath, decryptedPath);
expect(fileContentEqual(sourcePath, encryptedPath)).toBe(false);
expect(fileContentEqual(sourcePath, decryptedPath)).toBe(true);
done();
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -35,6 +35,7 @@ shimInit();
const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver;
Resource.fsDriver_ = fsDriver;
EncryptionService.fsDriver_ = fsDriver;
const logDir = __dirname + '/../tests/logs';
fs.mkdirpSync(logDir, 0o755);
@ -87,6 +88,9 @@ async function switchClient(id) {
Setting.db_ = databases_[id];
BaseItem.encryptionService_ = encryptionServices_[id];
Resource.encryptionService_ = encryptionServices_[id];
Setting.setConstant('resourceDir', resourceDir(id));
return Setting.load();
}
@ -120,6 +124,7 @@ function setupDatabase(id = null) {
}
const filePath = __dirname + '/data/test-' + id + '.sqlite';
// Setting.setConstant('resourceDir', RNFetchBlob.fs.dirs.DocumentDir);
return fs.unlink(filePath).catch(() => {
// Don't care if the file doesn't exist
}).then(() => {
@ -133,11 +138,19 @@ function setupDatabase(id = null) {
});
}
function resourceDir(id = null) {
if (id === null) id = currentClient_;
return __dirname + '/data/resources-' + id;
}
async function setupDatabaseAndSynchronizer(id = null) {
if (id === null) id = currentClient_;
await setupDatabase(id);
await fs.remove(resourceDir(id));
await fs.mkdirp(resourceDir(id), 0o755);
if (!synchronizers_[id]) {
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_);
const syncTarget = new SyncTargetClass(db(id));
@ -243,4 +256,11 @@ async function checkThrowAsync(asyncFn) {
return hasThrown;
}
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey };
function fileContentEqual(path1, path2) {
const fs = require('fs-extra');
const content1 = fs.readFileSync(path1, 'base64');
const content2 = fs.readFileSync(path2, 'base64');
return content1 === content2;
}
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual };

View File

@ -36,3 +36,11 @@ Only one master key can be active for encryption purposes. For decryption, the a
## Encryption Service
The applications make use of the EncryptionService class to handle encryption and decryption. Before it can be used, a least one master key must be loaded into it and marked as "active".
## Encryption workflow
Items are encrypted only during synchronisation, when they are serialised (via BaseItem.serializeForSync), so before being sent to the sync target.
They are decrypted by DecryptionWorker in the background.
The apps handle displaying both decrypted and encrypted items, so that user is aware that these items are there even if not yet decrypted. Encrypted items are mostly read-only to the user, except that they can be deleted.

View File

@ -244,7 +244,7 @@ class EncryptionService {
throw new Error('Unknown decryption method: ' + method);
}
async encryptString(plainText) {
async encryptAbstract_(source, destination) {
const method = this.defaultEncryptionMethod();
const masterKeyId = this.activeMasterKeyId();
const masterKeyPlainText = this.loadedMasterKey(masterKeyId);
@ -255,76 +255,130 @@ class EncryptionService {
masterKeyId: masterKeyId,
};
let cipherText = [];
cipherText.push(this.encodeHeader_(header));
await destination.append(this.encodeHeader_(header));
let fromIndex = 0;
while (true) {
const block = plainText.substr(fromIndex, this.chunkSize_);
const block = await source.read(this.chunkSize_);
if (!block) break;
fromIndex += block.length;
const encrypted = await this.encrypt(method, masterKeyPlainText, block);
cipherText.push(padLeft(encrypted.length.toString(16), 6, '0'));
cipherText.push(encrypted);
await destination.append(padLeft(encrypted.length.toString(16), 6, '0'));
await destination.append(encrypted);
}
return cipherText.join('');
}
async decryptString(cipherText) {
const header = this.decodeHeader_(cipherText);
async decryptAbstract_(source, destination) {
const headerVersionHexaBytes = await source.read(2);
const headerVersion = this.decodeHeaderVersion_(headerVersionHexaBytes);
const headerSize = this.headerSize_(headerVersion);
const headerHexaBytes = await source.read(headerSize - 2);
const header = this.decodeHeader_(headerVersionHexaBytes + headerHexaBytes);
const masterKeyPlainText = this.loadedMasterKey(header.masterKeyId);
let index = header.length;
let output = [];
while (index < cipherText.length) {
const length = parseInt(cipherText.substr(index, 6), 16);
while (true) {
const lengthHex = await source.read(6);
if (!lengthHex) break;
if (lengthHex.length !== 6) throw new Error('Invalid block size: ' + lengthHex);
const length = parseInt(lengthHex, 16);
index += 6;
if (!length) continue; // Weird but could be not completely invalid (block of size 0) so continue decrypting
const block = cipherText.substr(index, length);
const block = await source.read(length);
index += length;
const plainText = await this.decrypt(header.encryptionMethod, masterKeyPlainText, block);
output.push(plainText);
await destination.append(plainText);
}
return output.join('');
}
async encryptFile(method, key, srcPath, destPath) {
const fsDriver = this.fsDriver();
stringReader_(string) {
const reader = {
index: 0,
read: async function(size) {
const output = string.substr(reader.index, size);
reader.index += size;
return output;
},
close: function() {},
};
return reader;
}
let handle = await fsDriver.open(srcPath, 'r');
stringWriter_() {
const output = {
data: [],
append: async function(data) {
output.data.push(data);
},
result: function() {
return output.data.join('');
},
close: function() {},
};
return output;
}
async fileReader_(path, encoding) {
const fsDriver = this.fsDriver();
const handle = await fsDriver.open(path, 'r');
const reader = {
handle: handle,
read: async function(size) {
return fsDriver.readFileChunk(reader.handle, size, encoding);
},
close: function() {
fsDriver.close(reader.handle);
},
};
return reader;
}
async fileWriter_(path, encoding) {
const fsDriver = this.fsDriver();
return {
append: async function(data) {
return fsDriver.appendFile(path, data, encoding);
},
close: function() {},
};
}
async encryptString(plainText) {
const source = this.stringReader_(plainText);
const destination = this.stringWriter_();
await this.encryptAbstract_(source, destination);
return destination.result();
}
async decryptString(cipherText) {
const source = this.stringReader_(cipherText);
const destination = this.stringWriter_();
await this.decryptAbstract_(source, destination);
return destination.data.join('');
}
async encryptFile(srcPath, destPath) {
const fsDriver = this.fsDriver();
let source = await this.fileReader_(srcPath, 'base64');
let destination = await this.fileWriter_(destPath, 'ascii');
const cleanUp = () => {
if (handle) fsDriver.close(handle);
handle = null;
if (source) source.close();
if (destination) destination.close();
source = null;
destination = null;
}
try {
await fsDriver.unlink(destPath);
// Header
await fsDriver.appendFile(destPath, '01', 'ascii'); // Version number
await fsDriver.appendFile(destPath, padLeft(EncryptionService.METHOD_SJCL.toString(16), 2, '0'), 'ascii'); // Encryption method
while (true) {
const plainText = await fsDriver.readFileChunk(handle, this.chunkSize_, 'base64');
if (!plainText) break;
const cipherText = await this.encrypt(method, key, plainText);
await fsDriver.appendFile(destPath, padLeft(cipherText.length.toString(16), 6, '0'), 'ascii'); // Data - Length
await fsDriver.appendFile(destPath, cipherText, 'ascii'); // Data - Data
}
await this.encryptAbstract_(source, destination);
} catch (error) {
cleanUp();
await fsDriver.unlink(destPath);
@ -334,35 +388,21 @@ class EncryptionService {
cleanUp();
}
async decryptFile(key, srcPath, destPath) {
async decryptFile(srcPath, destPath) {
const fsDriver = this.fsDriver();
let handle = await fsDriver.open(srcPath, 'r');
let source = await this.fileReader_(srcPath, 'ascii');
let destination = await this.fileWriter_(destPath, 'base64');
const cleanUp = () => {
if (handle) fsDriver.close(handle);
handle = null;
if (source) source.close();
if (destination) destination.close();
source = null;
destination = null;
}
try {
await fsDriver.unlink(destPath);
const headerHexaBytes = await fsDriver.readFileChunk(handle, 4, 'ascii');
const header = this.decodeHeader_(headerHexaBytes);
while (true) {
const lengthHex = await fsDriver.readFileChunk(handle, 6, 'ascii');
if (!lengthHex) break;
const length = parseInt(lengthHex, 16);
const cipherText = await fsDriver.readFileChunk(handle, length, 'ascii');
if (!cipherText) break;
const plainText = await this.decrypt(header.encryptionMethod, key, cipherText);
await fsDriver.appendFile(destPath, plainText, 'base64');
}
await this.decryptAbstract_(source, destination);
} catch (error) {
cleanUp();
await fsDriver.unlink(destPath);
@ -372,6 +412,16 @@ class EncryptionService {
cleanUp();
}
decodeHeaderVersion_(hexaByte) {
if (hexaByte.length !== 2) throw new Error('Invalid header version length: ' + hexaByte);
return parseInt(hexaByte, 16);
}
headerSize_(version) {
if (version === 1) return 36;
throw new Error('Unknown header version: ' + version);
}
encodeHeader_(header) {
// Sanity check
if (header.masterKeyId.length !== 32) throw new Error('Invalid master key ID size: ' + header.masterKeyId);