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);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
});
|
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();
|
||||
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 };
|
@ -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.
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user