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

All: Support encrypting notes and notebooks

This commit is contained in:
Laurent Cozic 2017-12-13 18:57:40 +00:00
parent f6fbf3ba0f
commit 5951ed3f55
9 changed files with 503 additions and 24 deletions

View File

@ -7,4 +7,5 @@ rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
mkdir -p "$BUILD_DIR/data"
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)
(cd "$ROOT_DIR" && npm test tests-build/encryption.js)

View File

@ -0,0 +1,163 @@
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 { Folder } = require('lib/models/folder.js');
const { Note } = require('lib/models/note.js');
const { Tag } = require('lib/models/tag.js');
const { Database } = require('lib/database.js');
const { Setting } = require('lib/models/setting.js');
const { BaseItem } = require('lib/models/base-item.js');
const { BaseModel } = require('lib/base-model.js');
const MasterKey = require('lib/models/MasterKey');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const EncryptionService = require('lib/services/EncryptionService.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; // The first test is slow because the database needs to be built
let service = null;
describe('Encryption', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
//await setupDatabaseAndSynchronizer(2);
//await switchClient(1);
service = new EncryptionService();
BaseItem.encryptionService_ = service;
Setting.setValue('encryption.enabled', true);
done();
});
it('should encode and decode header', async (done) => {
const header = {
version: 1,
encryptionMethod: EncryptionService.METHOD_SJCL,
masterKeyId: '01234568abcdefgh01234568abcdefgh',
};
const encodedHeader = service.encodeHeader_(header);
const decodedHeader = service.decodeHeader_(encodedHeader);
delete decodedHeader.length;
expect(objectsEqual(header, decodedHeader)).toBe(true);
done();
});
it('should generate and decrypt a master key', async (done) => {
const masterKey = await service.generateMasterKey('123456');
expect(!!masterKey.checksum).toBe(true);
expect(!!masterKey.content).toBe(true);
let hasThrown = false;
try {
await service.decryptMasterKey(masterKey, 'wrongpassword');
} catch (error) {
hasThrown = true;
}
expect(hasThrown).toBe(true);
const decryptedMasterKey = await service.decryptMasterKey(masterKey, '123456');
expect(decryptedMasterKey.length).toBe(512);
done();
});
it('should encrypt and decrypt with a master key', async (done) => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
const cipherText = await service.encryptString('some secret');
const plainText = await service.decryptString(cipherText);
expect(plainText).toBe('some secret');
// Test that a long string, that is going to be split into multiple chunks, encrypt
// and decrypt properly too.
let veryLongSecret = '';
for (let i = 0; i < service.chunkSize() * 3; i++) veryLongSecret += Math.floor(Math.random() * 9);
const cipherText2 = await service.encryptString(veryLongSecret);
const plainText2 = await service.decryptString(cipherText2);
expect(plainText2 === veryLongSecret).toBe(true);
done();
});
it('should fail to decrypt if master key not present', async (done) => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
const cipherText = await service.encryptString('some secret');
await service.unloadMasterKey(masterKey);
let hasThrown = await checkThrowAsync(async () => await service.decryptString(cipherText));
expect(hasThrown).toBe(true);
done();
});
it('should fail to decrypt if data tampered with', async (done) => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
let cipherText = await service.encryptString('some secret');
cipherText += "ABCDEFGHIJ";
let hasThrown = await checkThrowAsync(async () => await service.decryptString(cipherText));
expect(hasThrown).toBe(true);
done();
});
it('should encrypt and decrypt serialised data', async (done) => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
let folder = await Folder.save({ title: 'folder' });
let note = await Note.save({ title: 'encrypted note', body: 'something', parent_id: folder.id });
let serialized = await Note.serializeForSync(note);
let deserialized = Note.filter(await Note.unserialize(serialized));
// Check that required properties are not encrypted
expect(deserialized.id).toBe(note.id);
expect(deserialized.parent_id).toBe(note.parent_id);
expect(deserialized.updated_time).toBe(note.updated_time);
// Check that at least title and body are encrypted
expect(!deserialized.title).toBe(true);
expect(!deserialized.body).toBe(true);
// Check that encrypted data is there
expect(!!deserialized.encryption_cipher_text).toBe(true);
encryptedNote = await Note.save(deserialized);
decryptedNote = await Note.decrypt(encryptedNote);
expect(decryptedNote.title).toBe(note.title);
expect(decryptedNote.body).toBe(note.body);
expect(decryptedNote.id).toBe(note.id);
expect(decryptedNote.parent_id).toBe(note.parent_id);
done();
});
});

View File

@ -1,12 +1,13 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId } = require('test-utils.js');
const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey } = 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');
const { Database } = require('lib/database.js');
const { Setting } = require('lib/models/setting.js');
const MasterKey = require('lib/models/MasterKey');
const { BaseItem } = require('lib/models/base-item.js');
const { BaseModel } = require('lib/base-model.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
@ -634,7 +635,7 @@ describe('Synchronizer', function() {
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
const noteId = note1.id;
await synchronizer().start();
let disabledItems = await BaseItem.syncDisabledItems();
let disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(0);
await Note.save({ id: noteId, title: "un mod", });
synchronizer().debugFlags_ = ['cannotSync'];
@ -651,10 +652,57 @@ describe('Synchronizer', function() {
await switchClient(1);
disabledItems = await BaseItem.syncDisabledItems();
disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(1);
done();
});
it('notes and folders should get encrypted when encryption is enabled', async (done) => {
Setting.setValue('encryption.enabled', true);
const masterKey = await loadEncryptionMasterKey();
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", body: 'to be encrypted', parent_id: folder1.id });
await synchronizer().start();
// After synchronisation, remote items should be encrypted but local ones remain plain text
note1 = await Note.load(note1.id);
expect(note1.title).toBe('un');
await switchClient(2);
await synchronizer().start();
let folder1_2 = await Folder.load(folder1.id);
let note1_2 = await Note.load(note1.id);
let masterKey_2 = await MasterKey.load(masterKey.id);
// On this side however it should be received encrypted
expect(!note1_2.title).toBe(true);
expect(!folder1_2.title).toBe(true);
expect(!!note1_2.encryption_cipher_text).toBe(true);
expect(!!folder1_2.encryption_cipher_text).toBe(true);
// Master key is already encrypted so it does not get re-encrypted during sync
expect(masterKey_2.content).toBe(masterKey.content);
expect(masterKey_2.checksum).toBe(masterKey.checksum);
// Now load the master key we got from client 1 and try to decrypt
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
// Get the decrypted items back
await Folder.decrypt(folder1_2);
await Note.decrypt(note1_2);
folder1_2 = await Folder.load(folder1.id);
note1_2 = await Note.load(note1.id);
// Check that properties match the original items. Also check
// the encryption did not affect the updated_time timestamp.
expect(note1_2.title).toBe(note1.title);
expect(note1_2.body).toBe(note1.body);
expect(note1_2.updated_time).toBe(note1.updated_time);
expect(!note1_2.encryption_cipher_text).toBe(true);
expect(folder1_2.title).toBe(folder1.title);
expect(folder1_2.updated_time).toBe(folder1.updated_time);
expect(!folder1_2.encryption_cipher_text).toBe(true);
done();
});
// TODO: test tags
// TODO: test resources
});

View File

@ -9,6 +9,7 @@ const { Tag } = require('lib/models/tag.js');
const { NoteTag } = require('lib/models/note-tag.js');
const { Logger } = require('lib/logger.js');
const { Setting } = require('lib/models/setting.js');
const MasterKey = require('lib/models/MasterKey');
const { BaseItem } = require('lib/models/base-item.js');
const { Synchronizer } = require('lib/synchronizer.js');
const { FileApi } = require('lib/file-api.js');
@ -16,16 +17,21 @@ const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const { FsDriverNode } = require('lib/fs-driver-node.js');
const { time } = require('lib/time-utils.js');
const { shimInit } = require('lib/shim-init-node.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const SyncTargetMemory = require('lib/SyncTargetMemory.js');
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
const EncryptionService = require('lib/services/EncryptionService.js');
let databases_ = [];
let synchronizers_ = [];
let encryptionServices_ = [];
let fileApi_ = null;
let currentClient_ = 1;
shimInit();
const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver;
Resource.fsDriver_ = fsDriver;
@ -52,6 +58,7 @@ BaseItem.loadClass('Folder', Folder);
BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
Setting.setConstant('appId', 'net.cozic.joplin-cli');
Setting.setConstant('appType', 'cli');
@ -79,6 +86,8 @@ async function switchClient(id) {
BaseItem.db_ = databases_[id];
Setting.db_ = databases_[id];
BaseItem.encryptionService_ = encryptionServices_[id];
return Setting.load();
}
@ -91,6 +100,7 @@ function clearDatabase(id = null) {
'DELETE FROM resources',
'DELETE FROM tags',
'DELETE FROM note_tags',
'DELETE FROM master_keys',
'DELETE FROM deleted_items',
'DELETE FROM sync_items',
@ -135,6 +145,10 @@ async function setupDatabaseAndSynchronizer(id = null) {
synchronizers_[id] = await syncTarget.synchronizer();
}
if (!encryptionServices_[id]) {
encryptionServices_[id] = new EncryptionService();
}
if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) {
fs.removeSync(syncDir)
fs.mkdirpSync(syncDir, 0o755);
@ -153,6 +167,22 @@ function synchronizer(id = null) {
return synchronizers_[id];
}
function encryptionService(id = null) {
if (id === null) id = currentClient_;
return encryptionServices_[id];
}
async function loadEncryptionMasterKey(id = null) {
const service = encryptionService(id);
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey(masterKey, '123456', true);
return masterKey;
}
function fileApi() {
if (fileApi_) return fileApi_;
@ -185,4 +215,23 @@ function fileApi() {
return fileApi_;
}
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId };
function objectsEqual(o1, o2) {
if (Object.getOwnPropertyNames(o1).length !== Object.getOwnPropertyNames(o2).length) return false;
for (let n in o1) {
if (!o1.hasOwnProperty(n)) continue;
if (o1[n] !== o2[n]) return false;
}
return true;
}
async function checkThrowAsync(asyncFn) {
let hasThrown = false;
try {
await asyncFn();
} catch (error) {
hasThrown = true;
}
return hasThrown;
}
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey };

View File

@ -202,7 +202,7 @@ class JoplinDatabase extends Database {
// default value and thus might cause problems. In that case, the default value
// must be set in the synchronizer too.
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8];
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
// currentVersionIndex < 0 if for the case where an old version of Joplin used with a newer
@ -270,6 +270,15 @@ class JoplinDatabase extends Database {
queries.push('ALTER TABLE sync_items ADD COLUMN sync_disabled_reason TEXT NOT NULL DEFAULT ""');
}
if (targetVersion == 9) {
queries.push('CREATE TABLE master_keys (id TEXT PRIMARY KEY, created_time INT NOT NULL, updated_time INT NOT NULL, encryption_method INT NOT NULL, checksum TEXT NOT NULL, content TEXT NOT NULL);');
queries.push('ALTER TABLE notes ADD COLUMN encryption_cipher_text TEXT NOT NULL DEFAULT ""');
queries.push('ALTER TABLE folders ADD COLUMN encryption_cipher_text TEXT NOT NULL DEFAULT ""');
queries.push('ALTER TABLE tags ADD COLUMN encryption_cipher_text TEXT NOT NULL DEFAULT ""');
queries.push('ALTER TABLE note_tags ADD COLUMN encryption_cipher_text TEXT NOT NULL DEFAULT ""');
queries.push('ALTER TABLE resources ADD COLUMN encryption_cipher_text TEXT NOT NULL DEFAULT ""');
}
queries.push({ sql: 'UPDATE version SET version = ?', params: [targetVersion] });
await this.transactionExecBatch(queries);

View File

@ -11,6 +11,16 @@ class MasterKey extends BaseItem {
return BaseModel.TYPE_MASTER_KEY;
}
static encryptionSupported() {
return false;
}
static async serialize(item, type = null, shownKeys = null) {
let fieldNames = this.fieldNames();
fieldNames.push('type_');
return super.serialize(item, 'master_key', fieldNames);
}
}
module.exports = MasterKey;

View File

@ -11,6 +11,10 @@ class BaseItem extends BaseModel {
return true;
}
static encryptionSupported() {
return true;
}
static loadClass(className, classRef) {
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
if (BaseItem.syncItemDefinitions_[i].className == className) {
@ -245,6 +249,48 @@ class BaseItem extends BaseModel {
return temp.join("\n\n");
}
static async serializeForSync(item) {
const ItemClass = this.itemClass(item);
let serialized = await ItemClass.serialize(item);
if (!Setting.value('encryption.enabled') || !ItemClass.encryptionSupported()) return serialized;
if (!BaseItem.encryptionService_) throw new Error('BaseItem.encryptionService_ is not set!!');
const cipherText = await BaseItem.encryptionService_.encryptString(serialized);
const reducedItem = Object.assign({}, item);
const keepKeys = ['id', 'title', 'parent_id', 'body', 'updated_time', 'type_'];
if ('title' in reducedItem) reducedItem.title = '';
if ('body' in reducedItem) reducedItem.body = '';
for (let n in reducedItem) {
if (!reducedItem.hasOwnProperty(n)) continue;
if (keepKeys.indexOf(n) >= 0) {
continue;
} else {
delete reducedItem[n];
}
}
reducedItem.encryption_cipher_text = cipherText;
return ItemClass.serialize(reducedItem)
}
static async decrypt(item) {
if (!item.encryption_cipher_text) throw new Error('Item is not encrypted: ' + item.id);
const ItemClass = this.itemClass(item);
const plainText = await BaseItem.encryptionService_.decryptString(item.encryption_cipher_text);
// Note: decryption does not count has a change, so don't update any timestamp
const plainItem = await ItemClass.unserialize(plainText);
plainItem.updated_time = item.updated_time;
plainItem.encryption_cipher_text = '';
return ItemClass.save(plainItem, { autoTimestamp: false });
}
static async unserialize(content) {
let lines = content.split("\n");
let output = {};
@ -447,6 +493,8 @@ class BaseItem extends BaseModel {
}
BaseItem.encryptionService_ = null;
// Also update:
// - itemsThatNeedSync()
// - syncedItems()

View File

@ -1,8 +1,53 @@
const { padLeft } = require('lib/string-utils.js');
const { shim } = require('lib/shim.js');
function hexPad(s, length) {
return padLeft(s, length, '0');
}
class EncryptionService {
constructor() {
// Note: 1 MB is very slow with Node and probably even worse on mobile. 50 KB seems to work well
// and doesn't produce too much overhead in terms of headers.
this.chunkSize_ = 50000;
this.loadedMasterKeys_ = {};
this.activeMasterKeyId_ = null;
this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL;
}
chunkSize() {
return this.chunkSize_;
}
defaultEncryptionMethod() {
return this.defaultEncryptionMethod_;
}
setActiveMasterKeyId(id) {
this.activeMasterKeyId_ = id;
}
activeMasterKeyId() {
if (!this.activeMasterKeyId_) throw new Error('No master key is defined as active');
return this.activeMasterKeyId_;
}
async loadMasterKey(model, password, makeActive = false) {
if (!model.id) throw new Error('Master key does not have an ID - save it first');
this.loadedMasterKeys_[model.id] = await this.decryptMasterKey(model, password);
if (makeActive) this.setActiveMasterKeyId(model.id);
}
unloadMasterKey(model) {
delete this.loadedMasterKeys_[model.id];
}
loadedMasterKey(id) {
if (!this.loadedMasterKeys_[id]) throw new Error('Master key is not loaded: ' + id);
return this.loadedMasterKeys_[id];
}
fsDriver() {
if (!EncryptionService.fsDriver_) throw new Error('EncryptionService.fsDriver_ not set!');
return EncryptionService.fsDriver_;
@ -14,12 +59,24 @@ class EncryptionService {
return sjcl.codec.hex.fromBits(bitArray);
}
async seedSjcl() {
throw new Error('NOT TESTED');
// Just putting this here in case it becomes needed
const sjcl = shim.sjclModule;
const randomBytes = await shim.randomBytes(1024/8);
const hexBytes = randomBytes.map((a) => { return a.toString(16) });
const hexSeed = sjcl.codec.hex.toBits(hexBytes.join(''));
sjcl.random.addEntropy(hexSeed, 1024, 'shim.randomBytes');
}
async generateMasterKey(password) {
const bytes = await shim.randomBytes(256);
const hexaBytes = bytes.map((a) => { return a.toString(16); }).join('');
const hexaBytes = bytes.map((a) => { return hexPad(a.toString(16), 2); }).join('');
const checksum = this.sha256(hexaBytes);
const encryptionMethod = EncryptionService.METHOD_SJCL_2;
const cipherText = await this.encrypt(encryptionMethod, password, hexaBytes);
const cipherText = await this.encrypt_(encryptionMethod, password, hexaBytes);
const now = Date.now();
return {
@ -32,13 +89,13 @@ class EncryptionService {
}
async decryptMasterKey(model, password) {
const plainText = await this.decrypt(model.encryption_method, password, model.content);
const plainText = await this.decrypt_(model.encryption_method, password, model.content);
const checksum = this.sha256(plainText);
if (checksum !== model.checksum) throw new Error('Could not decrypt master key (checksum failed)');
return plainText;
}
async encrypt(method, key, plainText) {
async encrypt_(method, key, plainText) {
const sjcl = shim.sjclModule;
if (method === EncryptionService.METHOD_SJCL) {
@ -69,7 +126,7 @@ class EncryptionService {
throw new Error('Unknown encryption method: ' + method);
}
async decrypt(method, key, cipherText) {
async decrypt_(method, key, cipherText) {
const sjcl = shim.sjclModule;
if (method === EncryptionService.METHOD_SJCL || method === EncryptionService.METHOD_SJCL_2) {
@ -79,6 +136,61 @@ class EncryptionService {
throw new Error('Unknown decryption method: ' + method);
}
async encryptString(plainText) {
const method = this.defaultEncryptionMethod();
const masterKeyId = this.activeMasterKeyId();
const masterKeyPlainText = this.loadedMasterKey(masterKeyId);
const header = {
version: 1,
encryptionMethod: method,
masterKeyId: masterKeyId,
};
let cipherText = [];
cipherText.push(this.encodeHeader_(header));
let fromIndex = 0;
while (true) {
const block = plainText.substr(fromIndex, 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);
}
return cipherText.join('');
}
async decryptString(cipherText) {
const header = this.decodeHeader_(cipherText);
const masterKeyPlainText = this.loadedMasterKey(header.masterKeyId);
let index = header.length;
let output = [];
while (index < cipherText.length) {
const length = parseInt(cipherText.substr(index, 6), 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);
index += length;
const plainText = await this.decrypt_(header.encryptionMethod, masterKeyPlainText, block);
output.push(plainText);
}
return output.join('');
}
async encryptFile(method, key, srcPath, destPath) {
const fsDriver = this.fsDriver();
@ -89,10 +201,6 @@ class EncryptionService {
handle = null;
}
// Note: 1 MB is very slow with Node and probably even worse on mobile. 50 KB seems to work well
// and doesn't produce too much overhead in terms of headers.
const chunkSize = 50000;
try {
await fsDriver.unlink(destPath);
@ -101,10 +209,10 @@ class EncryptionService {
await fsDriver.appendFile(destPath, padLeft(EncryptionService.METHOD_SJCL.toString(16), 2, '0'), 'ascii'); // Encryption method
while (true) {
const plainText = await fsDriver.readFileChunk(handle, chunkSize, 'base64');
const plainText = await fsDriver.readFileChunk(handle, this.chunkSize_, 'base64');
if (!plainText) break;
const cipherText = await this.encrypt(method, key, plainText);
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
@ -132,7 +240,7 @@ class EncryptionService {
await fsDriver.unlink(destPath);
const headerHexaBytes = await fsDriver.readFileChunk(handle, 4, 'ascii');
const header = this.parseFileHeader_(headerHexaBytes);
const header = this.decodeHeader_(headerHexaBytes);
while (true) {
const lengthHex = await fsDriver.readFileChunk(handle, 6, 'ascii');
@ -143,7 +251,7 @@ class EncryptionService {
const cipherText = await fsDriver.readFileChunk(handle, length, 'ascii');
if (!cipherText) break;
const plainText = await this.decrypt(header.encryptionMethod, key, cipherText);
const plainText = await this.decrypt_(header.encryptionMethod, key, cipherText);
await fsDriver.appendFile(destPath, plainText, 'base64');
}
@ -156,11 +264,54 @@ class EncryptionService {
cleanUp();
}
parseFileHeader_(headerHexaBytes) {
return {
version: parseInt(headerHexaBytes.substr(0,2), 16),
encryptionMethod: parseInt(headerHexaBytes.substr(2,2), 16),
encodeHeader_(header) {
// Sanity check
if (header.masterKeyId.length !== 32) throw new Error('Invalid master key ID size: ' + header.masterKeyId);
const output = [];
output.push(padLeft(header.version.toString(16), 2, '0'));
output.push(padLeft(header.encryptionMethod.toString(16), 2, '0'));
output.push(header.masterKeyId);
return output.join('');
}
decodeHeader_(headerHexaBytes) {
const headerTemplates = {
1: [
[ 'encryptionMethod', 2, 'int' ],
[ 'masterKeyId', 32, 'hex' ],
],
};
const output = {};
const version = parseInt(headerHexaBytes.substr(0, 2), 16);
const template = headerTemplates[version];
if (!template) throw new Error('Invalid header version: ' + version);
output.version = version;
let index = 2;
for (let i = 0; i < template.length; i++) {
const m = template[i];
const type = m[2];
let v = headerHexaBytes.substr(index, m[1]);
if (type === 'int') {
v = parseInt(v, 16);
} else if (type === 'hex') {
// Already in hexa
} else {
throw new Error('Invalid type: ' + type);
}
index += m[1];
output[m[0]] = v;
}
output.length = index;
return output;
}
}

View File

@ -204,7 +204,7 @@ class Synchronizer {
if (donePaths.indexOf(path) > 0) throw new Error(sprintf('Processing a path that has already been done: %s. sync_time was not updated?', path));
let remote = await this.api().stat(path);
let content = await ItemClass.serialize(local);
let content = await ItemClass.serializeForSync(local);
let action = null;
let updateSyncTimeOnly = true;
let reason = '';