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:
parent
4c0b472f67
commit
3f4f154949
@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
BIN
CliClient/tests/support/photo.jpg
Normal file
BIN
CliClient/tests/support/photo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
@ -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 };
|
@ -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.
|
@ -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 decryptAbstract_(source, destination) {
|
||||||
}
|
const headerVersionHexaBytes = await source.read(2);
|
||||||
|
const headerVersion = this.decodeHeaderVersion_(headerVersionHexaBytes);
|
||||||
async decryptString(cipherText) {
|
const headerSize = this.headerSize_(headerVersion);
|
||||||
const header = this.decodeHeader_(cipherText);
|
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('');
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
async encryptFile(method, key, srcPath, destPath) {
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
let handle = await fsDriver.open(srcPath, 'r');
|
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);
|
||||||
|
Loading…
Reference in New Issue
Block a user