1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-06-15 23:00:36 +02:00

All: Added new, more secure encryption methods, so that they can be switched to at a later time

This commit is contained in:
Laurent Cozic
2020-01-22 22:01:58 +00:00
parent b6e0df57eb
commit c01bc1c363
4 changed files with 180 additions and 58 deletions

View File

@ -29,10 +29,13 @@ class EncryptionService {
this.loadedMasterKeys_ = {};
this.activeMasterKeyId_ = null;
this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL;
this.defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_2;
this.logger_ = new Logger();
this.headerTemplates_ = {
// Template version 1
1: {
// Fields are defined as [name, valueSize, valueType]
fields: [['encryptionMethod', 2, 'int'], ['masterKeyId', 32, 'hex']],
},
};
@ -98,13 +101,20 @@ class EncryptionService {
this.logger().info(`Trying to load ${masterKeys.length} master keys...`);
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
let mk = masterKeys[i];
const password = passwords[mk.id];
if (this.isMasterKeyLoaded(mk.id)) continue;
if (!password) continue;
try {
await this.loadMasterKey(mk, password, activeMasterKeyId === mk.id);
// if (mk.encryption_method != this.defaultMasterKeyEncryptionMethod_) {
// const newMkContent = await this.generateMasterKeyContent_(password);
// mk = Object.assign({}, mk, newMkContent);
// await MasterKey.save(mk);
// this.logger().info(`Master key ${mk.id} is using a deprectated encryption method. It has been upgraded to the new method.`);
// }
await this.loadMasterKey_(mk, password, activeMasterKeyId === mk.id);
} catch (error) {
this.logger().warn(`Cannot load master key ${mk.id}. Invalid password?`, error);
}
@ -147,9 +157,9 @@ class EncryptionService {
return !!this.loadedMasterKeys_[id];
}
async loadMasterKey(model, password, makeActive = false) {
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);
this.loadedMasterKeys_[model.id] = await this.decryptMasterKey_(model, password);
if (makeActive) this.setActiveMasterKeyId(model.id);
}
@ -219,29 +229,35 @@ class EncryptionService {
.join('');
}
async generateMasterKey(password) {
async generateMasterKeyContent_(password, options = null) {
options = Object.assign({}, {
encryptionMethod: this.defaultMasterKeyEncryptionMethod_,
}, options);
const bytes = await shim.randomBytes(256);
const hexaBytes = bytes
.map(a => {
return hexPad(a.toString(16), 2);
})
.join('');
const hexaBytes = bytes.map(a => 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 now = Date.now();
const cipherText = await this.encrypt(options.encryptionMethod, password, hexaBytes);
return {
created_time: now,
updated_time: now,
source_application: Setting.value('appId'),
encryption_method: encryptionMethod,
checksum: checksum,
encryption_method: options.encryptionMethod,
content: cipherText,
};
}
async decryptMasterKey(model, password) {
async generateMasterKey(password, options = null) {
const model = await this.generateMasterKeyContent_(password, options);
const now = Date.now();
model.created_time = now;
model.updated_time = now;
model.source_application = Setting.value('appId');
return model;
}
async decryptMasterKey_(model, password) {
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)');
@ -250,7 +266,7 @@ class EncryptionService {
async checkMasterKeyPassword(model, password) {
try {
await this.decryptMasterKey(model, password);
await this.decryptMasterKey_(model, password);
} catch (error) {
return false;
}
@ -264,12 +280,13 @@ class EncryptionService {
const sjcl = shim.sjclModule;
// 2020-01-23: Deprecated and no longer secure due to the use og OCB2 mode - do not use.
if (method === EncryptionService.METHOD_SJCL) {
try {
// Good demo to understand each parameter: https://bitwiseshiftleft.github.io/sjcl/demo/
return sjcl.json.encrypt(key, plainText, {
v: 1, // version
iter: 1000, // Defaults to 10000 in sjcl but since we're running this on mobile devices, use a lower value. Maybe review this after some time. https://security.stackexchange.com/questions/3959/recommended-of-iterations-when-using-pkbdf2-sha256
iter: 1000, // Defaults to 1000 in sjcl but since we're running this on mobile devices, use a lower value. Maybe review this after some time. https://security.stackexchange.com/questions/3959/recommended-of-iterations-when-using-pkbdf2-sha256
ks: 128, // Key size - "128 bits should be secure enough"
ts: 64, // ???
mode: 'ocb2', // The cipher mode is a standard for how to use AES and other algorithms to encrypt and authenticate your message. OCB2 mode is slightly faster and has more features, but CCM mode has wider support because it is not patented.
@ -282,7 +299,8 @@ class EncryptionService {
}
}
// Same as first one but slightly more secure (but slower) to encrypt master keys
// 2020-01-23: Deprectated - see above.
// Was used to encrypt master keys
if (method === EncryptionService.METHOD_SJCL_2) {
try {
return sjcl.json.encrypt(key, plainText, {
@ -299,6 +317,41 @@ class EncryptionService {
}
}
if (method === EncryptionService.METHOD_SJCL_3) {
try {
// Good demo to understand each parameter: https://bitwiseshiftleft.github.io/sjcl/demo/
return sjcl.json.encrypt(key, plainText, {
v: 1, // version
iter: 1000, // Defaults to 1000 in sjcl. Since we're running this on mobile devices we need to be careful it doesn't affect performances too much. Maybe review this after some time. https://security.stackexchange.com/questions/3959/recommended-of-iterations-when-using-pkbdf2-sha256
ks: 128, // Key size - "128 bits should be secure enough"
ts: 64, // ???
mode: 'ccm', // The cipher mode is a standard for how to use AES and other algorithms to encrypt and authenticate your message. OCB2 mode is slightly faster and has more features, but CCM mode has wider support because it is not patented.
// "adata":"", // Associated Data - not needed?
cipher: 'aes',
});
} catch (error) {
// SJCL returns a string as error which means stack trace is missing so convert to an error object here
throw new Error(error.message);
}
}
// Same as above but more secure (but slower) to encrypt master keys
if (method === EncryptionService.METHOD_SJCL_4) {
try {
return sjcl.json.encrypt(key, plainText, {
v: 1,
iter: 10000,
ks: 256,
ts: 64,
mode: 'ccm',
cipher: 'aes',
});
} catch (error) {
// SJCL returns a string as error which means stack trace is missing so convert to an error object here
throw new Error(error.message);
}
}
throw new Error(`Unknown encryption method: ${method}`);
}
@ -307,23 +360,22 @@ class EncryptionService {
if (!key) throw new Error('Encryption key is required');
const sjcl = shim.sjclModule;
if (!this.isValidEncryptionMethod(method)) throw new Error(`Unknown decryption method: ${method}`);
if (method === EncryptionService.METHOD_SJCL || method === EncryptionService.METHOD_SJCL_2) {
try {
return sjcl.json.decrypt(key, cipherText);
} catch (error) {
// SJCL returns a string as error which means stack trace is missing so convert to an error object here
throw new Error(error.message);
}
try {
return sjcl.json.decrypt(key, cipherText);
} catch (error) {
// SJCL returns a string as error which means stack trace is missing so convert to an error object here
throw new Error(error.message);
}
throw new Error(`Unknown decryption method: ${method}`);
}
async encryptAbstract_(source, destination, options = null) {
if (!options) options = {};
options = Object.assign({}, {
encryptionMethod: this.defaultEncryptionMethod(),
}, options);
const method = this.defaultEncryptionMethod();
const method = options.encryptionMethod;
const masterKeyId = this.activeMasterKeyId();
const masterKeyPlainText = this.loadedMasterKey(masterKeyId);
@ -357,13 +409,7 @@ class EncryptionService {
async decryptAbstract_(source, destination, options = null) {
if (!options) options = {};
const identifier = await source.read(5);
if (!this.isValidHeaderIdentifier(identifier)) throw new JoplinError(`Invalid encryption identifier. Data is not actually encrypted? ID was: ${identifier}`, 'invalidIdentifier');
const mdSizeHex = await source.read(6);
const mdSize = parseInt(mdSizeHex, 16);
if (isNaN(mdSize) || !mdSize) throw new Error(`Invalid header metadata size: ${mdSizeHex}`);
const md = await source.read(parseInt(mdSizeHex, 16));
const header = this.decodeHeader_(identifier + mdSizeHex + md);
const header = await this.decodeHeaderSource_(source);
const masterKeyPlainText = this.loadedMasterKey(header.masterKeyId);
let doneSize = 0;
@ -501,11 +547,6 @@ class EncryptionService {
await cleanUp();
}
decodeHeaderVersion_(hexaByte) {
if (hexaByte.length !== 2) throw new Error(`Invalid header version length: ${hexaByte}`);
return parseInt(hexaByte, 16);
}
headerTemplate(version) {
const r = this.headerTemplates_[version];
if (!r) throw new Error(`Unknown header version: ${version}`);
@ -523,7 +564,22 @@ class EncryptionService {
return `JED01${encryptionMetadata}`;
}
decodeHeader_(headerHexaBytes) {
async decodeHeaderString(cipherText) {
const source = this.stringReader_(cipherText);
return this.decodeHeaderSource_(source);
}
async decodeHeaderSource_(source) {
const identifier = await source.read(5);
if (!this.isValidHeaderIdentifier(identifier)) throw new JoplinError(`Invalid encryption identifier. Data is not actually encrypted? ID was: ${identifier}`, 'invalidIdentifier');
const mdSizeHex = await source.read(6);
const mdSize = parseInt(mdSizeHex, 16);
if (isNaN(mdSize) || !mdSize) throw new Error(`Invalid header metadata size: ${mdSizeHex}`);
const md = await source.read(parseInt(mdSizeHex, 16));
return this.decodeHeaderBytes_(identifier + mdSizeHex + md);
}
decodeHeaderBytes_(headerHexaBytes) {
const reader = this.stringReader_(headerHexaBytes, true);
const identifier = reader.read(3);
const version = parseInt(reader.read(2), 16);
@ -561,6 +617,10 @@ class EncryptionService {
return /JED\d\d/.test(id);
}
isValidEncryptionMethod(method) {
return [EncryptionService.METHOD_SJCL, EncryptionService.METHOD_SJCL_2, EncryptionService.METHOD_SJCL_3, EncryptionService.METHOD_SJCL_4].indexOf(method) >= 0;
}
async itemIsEncrypted(item) {
if (!item) throw new Error('No item');
const ItemClass = BaseItem.itemClass(item);
@ -578,6 +638,8 @@ class EncryptionService {
EncryptionService.METHOD_SJCL = 1;
EncryptionService.METHOD_SJCL_2 = 2;
EncryptionService.METHOD_SJCL_3 = 3;
EncryptionService.METHOD_SJCL_4 = 4;
EncryptionService.fsDriver_ = null;