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); require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js'); 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 Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js'); const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js'); const Tag = require('lib/models/Tag.js');
@ -160,4 +160,22 @@ describe('Encryption', function() {
done(); 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(); const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver; Logger.fsDriver_ = fsDriver;
Resource.fsDriver_ = fsDriver; Resource.fsDriver_ = fsDriver;
EncryptionService.fsDriver_ = fsDriver;
const logDir = __dirname + '/../tests/logs'; const logDir = __dirname + '/../tests/logs';
fs.mkdirpSync(logDir, 0o755); fs.mkdirpSync(logDir, 0o755);
@ -87,6 +88,9 @@ async function switchClient(id) {
Setting.db_ = databases_[id]; Setting.db_ = databases_[id];
BaseItem.encryptionService_ = encryptionServices_[id]; BaseItem.encryptionService_ = encryptionServices_[id];
Resource.encryptionService_ = encryptionServices_[id];
Setting.setConstant('resourceDir', resourceDir(id));
return Setting.load(); return Setting.load();
} }
@ -120,6 +124,7 @@ function setupDatabase(id = null) {
} }
const filePath = __dirname + '/data/test-' + id + '.sqlite'; const filePath = __dirname + '/data/test-' + id + '.sqlite';
// Setting.setConstant('resourceDir', RNFetchBlob.fs.dirs.DocumentDir);
return fs.unlink(filePath).catch(() => { return fs.unlink(filePath).catch(() => {
// Don't care if the file doesn't exist // Don't care if the file doesn't exist
}).then(() => { }).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) { async function setupDatabaseAndSynchronizer(id = null) {
if (id === null) id = currentClient_; if (id === null) id = currentClient_;
await setupDatabase(id); await setupDatabase(id);
await fs.remove(resourceDir(id));
await fs.mkdirp(resourceDir(id), 0o755);
if (!synchronizers_[id]) { if (!synchronizers_[id]) {
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_); const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_);
const syncTarget = new SyncTargetClass(db(id)); const syncTarget = new SyncTargetClass(db(id));
@ -243,4 +256,11 @@ async function checkThrowAsync(asyncFn) {
return hasThrown; 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 ## 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". 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); throw new Error('Unknown decryption method: ' + method);
} }
async encryptString(plainText) { async encryptAbstract_(source, destination) {
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);
@ -255,76 +255,130 @@ class EncryptionService {
masterKeyId: masterKeyId, masterKeyId: masterKeyId,
}; };
let cipherText = []; await destination.append(this.encodeHeader_(header));
cipherText.push(this.encodeHeader_(header));
let fromIndex = 0; let fromIndex = 0;
while (true) { while (true) {
const block = plainText.substr(fromIndex, this.chunkSize_); const block = await source.read(this.chunkSize_);
if (!block) break; if (!block) break;
fromIndex += block.length; fromIndex += block.length;
const encrypted = await this.encrypt(method, masterKeyPlainText, block); const encrypted = await this.encrypt(method, masterKeyPlainText, block);
cipherText.push(padLeft(encrypted.length.toString(16), 6, '0')); await destination.append(padLeft(encrypted.length.toString(16), 6, '0'));
cipherText.push(encrypted); await destination.append(encrypted);
} }
return cipherText.join('');
} }
async decryptString(cipherText) { async decryptAbstract_(source, destination) {
const header = this.decodeHeader_(cipherText); 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); const masterKeyPlainText = this.loadedMasterKey(header.masterKeyId);
let index = header.length; let index = header.length;
let output = []; while (true) {
const lengthHex = await source.read(6);
while (index < cipherText.length) { if (!lengthHex) break;
const length = parseInt(cipherText.substr(index, 6), 16); if (lengthHex.length !== 6) throw new Error('Invalid block size: ' + lengthHex);
const length = parseInt(lengthHex, 16);
index += 6; index += 6;
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
const block = cipherText.substr(index, length);
const block = await source.read(length);
index += length; index += length;
const plainText = await this.decrypt(header.encryptionMethod, masterKeyPlainText, block); 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) { stringReader_(string) {
const fsDriver = this.fsDriver(); 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 = () => { const cleanUp = () => {
if (handle) fsDriver.close(handle); if (source) source.close();
handle = null; if (destination) destination.close();
source = null;
destination = null;
} }
try { try {
await fsDriver.unlink(destPath); await fsDriver.unlink(destPath);
await this.encryptAbstract_(source, destination);
// 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
}
} catch (error) { } catch (error) {
cleanUp(); cleanUp();
await fsDriver.unlink(destPath); await fsDriver.unlink(destPath);
@ -334,35 +388,21 @@ class EncryptionService {
cleanUp(); cleanUp();
} }
async decryptFile(key, srcPath, destPath) { async decryptFile(srcPath, destPath) {
const fsDriver = this.fsDriver(); const fsDriver = this.fsDriver();
let source = await this.fileReader_(srcPath, 'ascii');
let handle = await fsDriver.open(srcPath, 'r'); let destination = await this.fileWriter_(destPath, 'base64');
const cleanUp = () => { const cleanUp = () => {
if (handle) fsDriver.close(handle); if (source) source.close();
handle = null; if (destination) destination.close();
source = null;
destination = null;
} }
try { try {
await fsDriver.unlink(destPath); await fsDriver.unlink(destPath);
await this.decryptAbstract_(source, destination);
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');
}
} catch (error) { } catch (error) {
cleanUp(); cleanUp();
await fsDriver.unlink(destPath); await fsDriver.unlink(destPath);
@ -372,6 +412,16 @@ class EncryptionService {
cleanUp(); 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) { encodeHeader_(header) {
// Sanity check // Sanity check
if (header.masterKeyId.length !== 32) throw new Error('Invalid master key ID size: ' + header.masterKeyId); if (header.masterKeyId.length !== 32) throw new Error('Invalid master key ID size: ' + header.masterKeyId);