Merge pull request #1059 from matrix-org/rav/delint_megolm_export

de-lint MegolmExportEncryption + test
This commit is contained in:
David Baker 2017-06-08 14:43:36 +01:00 committed by GitHub
commit 34e4d8088b
6 changed files with 69 additions and 51 deletions

View file

@ -2,8 +2,6 @@
src/AddThreepid.js src/AddThreepid.js
src/async-components/views/dialogs/EncryptedEventDialog.js src/async-components/views/dialogs/EncryptedEventDialog.js
src/async-components/views/dialogs/ExportE2eKeysDialog.js
src/async-components/views/dialogs/ImportE2eKeysDialog.js
src/autocomplete/AutocompleteProvider.js src/autocomplete/AutocompleteProvider.js
src/autocomplete/Autocompleter.js src/autocomplete/Autocompleter.js
src/autocomplete/Components.js src/autocomplete/Components.js
@ -167,7 +165,6 @@ src/UserActivity.js
src/utils/DecryptFile.js src/utils/DecryptFile.js
src/utils/DMRoomMap.js src/utils/DMRoomMap.js
src/utils/FormattingUtils.js src/utils/FormattingUtils.js
src/utils/MegolmExportEncryption.js
src/utils/MultiInviter.js src/utils/MultiInviter.js
src/utils/Receipt.js src/utils/Receipt.js
src/Velociraptor.js src/Velociraptor.js
@ -188,4 +185,3 @@ test/mock-clock.js
test/skinned-sdk.js test/skinned-sdk.js
test/stores/RoomViewStore-test.js test/stores/RoomViewStore-test.js
test/test-utils.js test/test-utils.js
test/utils/MegolmExportEncryption-test.js

View file

@ -120,7 +120,7 @@ export default React.createClass({
'you have received in encrypted rooms to a local file. You ' + 'you have received in encrypted rooms to a local file. You ' +
'will then be able to import the file into another Matrix ' + 'will then be able to import the file into another Matrix ' +
'client in the future, so that client will also be able to ' + 'client in the future, so that client will also be able to ' +
'decrypt these messages.' 'decrypt these messages.',
) } ) }
</p> </p>
<p> <p>
@ -130,7 +130,7 @@ export default React.createClass({
'careful to keep it secure. To help with this, you should enter ' + 'careful to keep it secure. To help with this, you should enter ' +
'a passphrase below, which will be used to encrypt the exported ' + 'a passphrase below, which will be used to encrypt the exported ' +
'data. It will only be possible to import the data by using the ' + 'data. It will only be possible to import the data by using the ' +
'same passphrase.' 'same passphrase.',
) } ) }
</p> </p>
<div className='error'> <div className='error'>

View file

@ -122,13 +122,13 @@ export default React.createClass({
'This process allows you to import encryption keys ' + 'This process allows you to import encryption keys ' +
'that you had previously exported from another Matrix ' + 'that you had previously exported from another Matrix ' +
'client. You will then be able to decrypt any ' + 'client. You will then be able to decrypt any ' +
'messages that the other client could decrypt.' 'messages that the other client could decrypt.',
) } ) }
</p> </p>
<p> <p>
{ _t( { _t(
'The export file will be protected with a passphrase. ' + 'The export file will be protected with a passphrase. ' +
'You should enter the passphrase here, to decrypt the file.' 'You should enter the passphrase here, to decrypt the file.',
) } ) }
</p> </p>
<div className='error'> <div className='error'>

View file

@ -32,7 +32,7 @@ const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle;
/** /**
* Decrypt a megolm key file * Decrypt a megolm key file
* *
* @param {ArrayBuffer} file * @param {ArrayBuffer} data file to decrypt
* @param {String} password * @param {String} password
* @return {Promise<String>} promise for decrypted output * @return {Promise<String>} promise for decrypted output
*/ */
@ -61,12 +61,12 @@ export function decryptMegolmKeyFile(data, password) {
const hmac = body.subarray(-32); const hmac = body.subarray(-32);
return deriveKeys(salt, iterations, password).then((keys) => { return deriveKeys(salt, iterations, password).then((keys) => {
const [aes_key, hmac_key] = keys; const [aesKey, hmacKey] = keys;
const toVerify = body.subarray(0, -32); const toVerify = body.subarray(0, -32);
return subtleCrypto.verify( return subtleCrypto.verify(
{name: 'HMAC'}, {name: 'HMAC'},
hmac_key, hmacKey,
hmac, hmac,
toVerify, toVerify,
).then((isValid) => { ).then((isValid) => {
@ -80,7 +80,7 @@ export function decryptMegolmKeyFile(data, password) {
counter: iv, counter: iv,
length: 64, length: 64,
}, },
aes_key, aesKey,
ciphertext, ciphertext,
); );
}); });
@ -102,7 +102,7 @@ export function decryptMegolmKeyFile(data, password) {
*/ */
export function encryptMegolmKeyFile(data, password, options) { export function encryptMegolmKeyFile(data, password, options) {
options = options || {}; options = options || {};
const kdf_rounds = options.kdf_rounds || 500000; const kdfRounds = options.kdf_rounds || 500000;
const salt = new Uint8Array(16); const salt = new Uint8Array(16);
window.crypto.getRandomValues(salt); window.crypto.getRandomValues(salt);
@ -115,8 +115,8 @@ export function encryptMegolmKeyFile(data, password, options) {
// of a single bit of iv is a price we have to pay. // of a single bit of iv is a price we have to pay.
iv[9] &= 0x7f; iv[9] &= 0x7f;
return deriveKeys(salt, kdf_rounds, password).then((keys) => { return deriveKeys(salt, kdfRounds, password).then((keys) => {
const [aes_key, hmac_key] = keys; const [aesKey, hmacKey] = keys;
return subtleCrypto.encrypt( return subtleCrypto.encrypt(
{ {
@ -124,7 +124,7 @@ export function encryptMegolmKeyFile(data, password, options) {
counter: iv, counter: iv,
length: 64, length: 64,
}, },
aes_key, aesKey,
new TextEncoder().encode(data), new TextEncoder().encode(data),
).then((ciphertext) => { ).then((ciphertext) => {
const cipherArray = new Uint8Array(ciphertext); const cipherArray = new Uint8Array(ciphertext);
@ -134,17 +134,17 @@ export function encryptMegolmKeyFile(data, password, options) {
resultBuffer[idx++] = 1; // version resultBuffer[idx++] = 1; // version
resultBuffer.set(salt, idx); idx += salt.length; resultBuffer.set(salt, idx); idx += salt.length;
resultBuffer.set(iv, idx); idx += iv.length; resultBuffer.set(iv, idx); idx += iv.length;
resultBuffer[idx++] = kdf_rounds >> 24; resultBuffer[idx++] = kdfRounds >> 24;
resultBuffer[idx++] = (kdf_rounds >> 16) & 0xff; resultBuffer[idx++] = (kdfRounds >> 16) & 0xff;
resultBuffer[idx++] = (kdf_rounds >> 8) & 0xff; resultBuffer[idx++] = (kdfRounds >> 8) & 0xff;
resultBuffer[idx++] = kdf_rounds & 0xff; resultBuffer[idx++] = kdfRounds & 0xff;
resultBuffer.set(cipherArray, idx); idx += cipherArray.length; resultBuffer.set(cipherArray, idx); idx += cipherArray.length;
const toSign = resultBuffer.subarray(0, idx); const toSign = resultBuffer.subarray(0, idx);
return subtleCrypto.sign( return subtleCrypto.sign(
{name: 'HMAC'}, {name: 'HMAC'},
hmac_key, hmacKey,
toSign, toSign,
).then((hmac) => { ).then((hmac) => {
hmac = new Uint8Array(hmac); hmac = new Uint8Array(hmac);
@ -170,7 +170,7 @@ function deriveKeys(salt, iterations, password) {
new TextEncoder().encode(password), new TextEncoder().encode(password),
{name: 'PBKDF2'}, {name: 'PBKDF2'},
false, false,
['deriveBits'] ['deriveBits'],
).then((key) => { ).then((key) => {
return subtleCrypto.deriveBits( return subtleCrypto.deriveBits(
{ {
@ -180,33 +180,33 @@ function deriveKeys(salt, iterations, password) {
hash: 'SHA-512', hash: 'SHA-512',
}, },
key, key,
512 512,
); );
}).then((keybits) => { }).then((keybits) => {
const now = new Date(); const now = new Date();
console.log("E2e import/export: deriveKeys took " + (now - start) + "ms"); console.log("E2e import/export: deriveKeys took " + (now - start) + "ms");
const aes_key = keybits.slice(0, 32); const aesKey = keybits.slice(0, 32);
const hmac_key = keybits.slice(32); const hmacKey = keybits.slice(32);
const aes_prom = subtleCrypto.importKey( const aesProm = subtleCrypto.importKey(
'raw', 'raw',
aes_key, aesKey,
{name: 'AES-CTR'}, {name: 'AES-CTR'},
false, false,
['encrypt', 'decrypt'] ['encrypt', 'decrypt'],
); );
const hmac_prom = subtleCrypto.importKey( const hmacProm = subtleCrypto.importKey(
'raw', 'raw',
hmac_key, hmacKey,
{ {
name: 'HMAC', name: 'HMAC',
hash: {name: 'SHA-256'}, hash: {name: 'SHA-256'},
}, },
false, false,
['sign', 'verify'] ['sign', 'verify'],
); );
return Promise.all([aes_prom, hmac_prom]); return Promise.all([aesProm, hmacProm]);
}); });
} }
@ -301,7 +301,7 @@ function packMegolmKeyFile(data) {
function encodeBase64(uint8Array) { function encodeBase64(uint8Array) {
// Misinterpt the Uint8Array as Latin-1. // Misinterpt the Uint8Array as Latin-1.
// window.btoa expects a unicode string with codepoints in the range 0-255. // window.btoa expects a unicode string with codepoints in the range 0-255.
var latin1String = String.fromCharCode.apply(null, uint8Array); const latin1String = String.fromCharCode.apply(null, uint8Array);
// Use the builtin base64 encoder. // Use the builtin base64 encoder.
return window.btoa(latin1String); return window.btoa(latin1String);
} }
@ -313,10 +313,10 @@ function encodeBase64(uint8Array) {
*/ */
function decodeBase64(base64) { function decodeBase64(base64) {
// window.atob returns a unicode string with codepoints in the range 0-255. // window.atob returns a unicode string with codepoints in the range 0-255.
var latin1String = window.atob(base64); const latin1String = window.atob(base64);
// Encode the string as a Uint8Array // Encode the string as a Uint8Array
var uint8Array = new Uint8Array(latin1String.length); const uint8Array = new Uint8Array(latin1String.length);
for (var i = 0; i < latin1String.length; i++) { for (let i = 0; i < latin1String.length; i++) {
uint8Array[i] = latin1String.charCodeAt(i); uint8Array[i] = latin1String.charCodeAt(i);
} }
return uint8Array; return uint8Array;

View file

@ -2,4 +2,9 @@ module.exports = {
env: { env: {
mocha: true, mocha: true,
}, },
}
// mocha defines a 'this'
rules: {
"babel/no-invalid-this": "off",
},
};

View file

@ -25,23 +25,40 @@ const TEST_VECTORS=[
[ [
"plain", "plain",
"password", "password",
"-----BEGIN MEGOLM SESSION DATA-----\nAXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\ncissyYBxjsfsAndErh065A8=\n-----END MEGOLM SESSION DATA-----" "-----BEGIN MEGOLM SESSION DATA-----\n" +
"AXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\n" +
"cissyYBxjsfsAndErh065A8=\n" +
"-----END MEGOLM SESSION DATA-----",
], ],
[ [
"Hello, World", "Hello, World",
"betterpassword", "betterpassword",
"-----BEGIN MEGOLM SESSION DATA-----\nAW1vcmVzYWx0bW9yZXNhbHT//////////wAAAAAAAAAAAAAD6KyBpe1Niv5M5NPm4ZATsJo5nghk\nKYu63a0YQ5DRhUWEKk7CcMkrKnAUiZny\n-----END MEGOLM SESSION DATA-----" "-----BEGIN MEGOLM SESSION DATA-----\n" +
"AW1vcmVzYWx0bW9yZXNhbHT//////////wAAAAAAAAAAAAAD6KyBpe1Niv5M5NPm4ZATsJo5nghk\n" +
"KYu63a0YQ5DRhUWEKk7CcMkrKnAUiZny\n" +
"-----END MEGOLM SESSION DATA-----",
], ],
[ [
"alphanumericallyalphanumericallyalphanumericallyalphanumerically", "alphanumericallyalphanumericallyalphanumericallyalphanumerically",
"SWORDFISH", "SWORDFISH",
"-----BEGIN MEGOLM SESSION DATA-----\nAXllc3NhbHR5Z29vZG5lc3P//////////wAAAAAAAAAAAAAD6OIW+Je7gwvjd4kYrb+49gKCfExw\nMgJBMD4mrhLkmgAngwR1pHjbWXaoGybtiAYr0moQ93GrBQsCzPbvl82rZhaXO3iH5uHo/RCEpOqp\nPgg29363BGR+/Ripq/VCLKGNbw==\n-----END MEGOLM SESSION DATA-----" "-----BEGIN MEGOLM SESSION DATA-----\n" +
"AXllc3NhbHR5Z29vZG5lc3P//////////wAAAAAAAAAAAAAD6OIW+Je7gwvjd4kYrb+49gKCfExw\n" +
"MgJBMD4mrhLkmgAngwR1pHjbWXaoGybtiAYr0moQ93GrBQsCzPbvl82rZhaXO3iH5uHo/RCEpOqp\n" +
"Pgg29363BGR+/Ripq/VCLKGNbw==\n" +
"-----END MEGOLM SESSION DATA-----",
], ],
[ [
"alphanumericallyalphanumericallyalphanumericallyalphanumerically", "alphanumericallyalphanumericallyalphanumericallyalphanumerically",
"passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword", "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" +
"-----BEGIN MEGOLM SESSION DATA-----\nAf//////////////////////////////////////////AAAD6IAZJy7IQ7Y0idqSw/bmpngEEVVh\ngsH+8ptgqxw6ZVWQnohr8JsuwH9SwGtiebZuBu5smPCO+RFVWH2cQYslZijXv/BEH/txvhUrrtCd\nbWnSXS9oymiqwUIGs08sXI33ZA==\n-----END MEGOLM SESSION DATA-----" "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" +
] "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" +
"passwordpasswordpasswordpasswordpassword",
"-----BEGIN MEGOLM SESSION DATA-----\n" +
"Af//////////////////////////////////////////AAAD6IAZJy7IQ7Y0idqSw/bmpngEEVVh\n" +
"gsH+8ptgqxw6ZVWQnohr8JsuwH9SwGtiebZuBu5smPCO+RFVWH2cQYslZijXv/BEH/txvhUrrtCd\n" +
"bWnSXS9oymiqwUIGs08sXI33ZA==\n" +
"-----END MEGOLM SESSION DATA-----",
],
] ]
; ;
@ -55,7 +72,7 @@ describe('MegolmExportEncryption', function() {
if (!window.crypto.subtle && !window.crypto.webkitSubtle) { if (!window.crypto.subtle && !window.crypto.webkitSubtle) {
this.skip(); this.skip();
} }
}) });
beforeEach(function() { beforeEach(function() {
testUtils.beforeEach(this); testUtils.beforeEach(this);
@ -64,14 +81,14 @@ describe('MegolmExportEncryption', function() {
describe('decrypt', function() { describe('decrypt', function() {
it('should handle missing header', function() { it('should handle missing header', function() {
const input=stringToArray(`-----`); const input=stringToArray(`-----`);
expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')}) expect(()=>MegolmExportEncryption.decryptMegolmKeyFile(input, ''))
.toThrow('Header line not found'); .toThrow('Header line not found');
}); });
it('should handle missing trailer', function() { it('should handle missing trailer', function() {
const input=stringToArray(`-----BEGIN MEGOLM SESSION DATA----- const input=stringToArray(`-----BEGIN MEGOLM SESSION DATA-----
-----`); -----`);
expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')}) expect(()=>MegolmExportEncryption.decryptMegolmKeyFile(input, ''))
.toThrow('Trailer line not found'); .toThrow('Trailer line not found');
}); });
@ -81,7 +98,7 @@ AXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx
cissyYBxjsfsAn cissyYBxjsfsAn
-----END MEGOLM SESSION DATA----- -----END MEGOLM SESSION DATA-----
`); `);
expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')}) expect(()=>MegolmExportEncryption.decryptMegolmKeyFile(input, ''))
.toThrow('Invalid file: too short'); .toThrow('Invalid file: too short');
}); });
@ -94,12 +111,12 @@ cissyYBxjsfsAn
const [plain, password, input] = TEST_VECTORS[i]; const [plain, password, input] = TEST_VECTORS[i];
return MegolmExportEncryption.decryptMegolmKeyFile( return MegolmExportEncryption.decryptMegolmKeyFile(
stringToArray(input), password stringToArray(input), password,
).then((decrypted) => { ).then((decrypted) => {
expect(decrypted).toEqual(plain); expect(decrypted).toEqual(plain);
return next(i+1); return next(i+1);
}) });
}; }
return next(0).catch(done); return next(0).catch(done);
}); });
}); });
@ -115,7 +132,7 @@ cissyYBxjsfsAn
input, password, {kdf_rounds: 1000}, input, password, {kdf_rounds: 1000},
).then((ciphertext) => { ).then((ciphertext) => {
return MegolmExportEncryption.decryptMegolmKeyFile( return MegolmExportEncryption.decryptMegolmKeyFile(
ciphertext, password ciphertext, password,
); );
}).then((plaintext) => { }).then((plaintext) => {
expect(plaintext).toEqual(input); expect(plaintext).toEqual(input);