From 1d5d44d63d2c07ac51f9886eb426c8ee8f9a22ab Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 12 Jan 2017 11:45:47 +0000 Subject: [PATCH 1/8] TextEncoder polyfill Apparently Safari doesn't sport a TextEncoder, so here's a polyfill for it. --- src/utils/TextDecoderPolyfill.js | 131 +++++++++++++++++++++++++ src/utils/TextEncoderPolyfill.js | 78 +++++++++++++++ test/utils/TextDecoderPolyfill-test.js | 85 ++++++++++++++++ test/utils/TextEncoderPolyfill-test.js | 39 ++++++++ 4 files changed, 333 insertions(+) create mode 100644 src/utils/TextDecoderPolyfill.js create mode 100644 src/utils/TextEncoderPolyfill.js create mode 100644 test/utils/TextDecoderPolyfill-test.js create mode 100644 test/utils/TextEncoderPolyfill-test.js diff --git a/src/utils/TextDecoderPolyfill.js b/src/utils/TextDecoderPolyfill.js new file mode 100644 index 0000000000..e203676bb7 --- /dev/null +++ b/src/utils/TextDecoderPolyfill.js @@ -0,0 +1,131 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +// Polyfill for TextDecoder. + +const REPLACEMENT_CHAR = '\uFFFD'; + +export default class TextDecoder { + /** + * Decode a UTF-8 byte array as a javascript string + * + * @param {Uint8Array} u8Array UTF-8-encoded onput + * @return {str} + */ + decode(u8Array) { + let u0, u1, u2, u3; + + let str = ''; + let idx = 0; + while (idx < u8Array.length) { + u0 = u8Array[idx++]; + if (!(u0 & 0x80)) { + str += String.fromCharCode(u0); + continue; + } + + if ((u0 & 0xC0) != 0xC0) { + // continuation byte where we expect a leading byte + str += REPLACEMENT_CHAR; + continue; + } + + if (u0 > 0xF4) { + // this would imply a 5-byte or longer encoding, which is + // invalid and unsupported here. + str += REPLACEMENT_CHAR; + continue; + } + + u1 = u8Array[idx++]; + if (u1 === undefined) { + str += REPLACEMENT_CHAR; + continue; + } + + if ((u1 & 0xC0) != 0x80) { + // leading byte where we expect a continuation byte + str += REPLACEMENT_CHAR.repeat(2); + continue; + } + u1 &= 0x3F; + if (!(u0 & 0x20)) { + const u = ((u0 & 0x1F) << 6) | u1; + if (u < 0x80) { + // over-long + str += REPLACEMENT_CHAR.repeat(2); + } else { + str += String.fromCharCode(u); + } + continue; + } + + u2 = u8Array[idx++]; + if (u2 === undefined) { + str += REPLACEMENT_CHAR.repeat(2); + continue; + } + if ((u2 & 0xC0) != 0x80) { + // leading byte where we expect a continuation byte + str += REPLACEMENT_CHAR.repeat(3); + continue; + } + u2 &= 0x3F; + if (!(u0 & 0x10)) { + const u = ((u0 & 0x0F) << 12) | (u1 << 6) | u2; + if (u < 0x800) { + // over-long + str += REPLACEMENT_CHAR.repeat(3); + } else if (u == 0xFEFF && idx == 3) { + // byte-order mark: do not add to output + } else { + str += String.fromCharCode(u); + } + continue; + } + + u3 = u8Array[idx++]; + if (u3 === undefined) { + str += REPLACEMENT_CHAR.repeat(3); + continue; + } + if ((u3 & 0xC0) != 0x80) { + // leading byte where we expect a continuation byte + str += REPLACEMENT_CHAR.repeat(4); + continue; + } + u3 &= 0x3F; + const u = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | u3; + if (u < 0x10000) { + // over-long + str += REPLACEMENT_CHAR.repeat(4); + continue; + } + if (u > 0x1FFFF) { + // unicode stops here. + str += REPLACEMENT_CHAR.repeat(4); + continue; + } + + // encode as utf-16 + const v = u - 0x10000; + str += String.fromCharCode(0xD800 | (v >> 10), 0xDC00 | (v & 0x3FF)); + } + return str; + } +} diff --git a/src/utils/TextEncoderPolyfill.js b/src/utils/TextEncoderPolyfill.js new file mode 100644 index 0000000000..2da09710f2 --- /dev/null +++ b/src/utils/TextEncoderPolyfill.js @@ -0,0 +1,78 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +// Polyfill for TextEncoder. Based on emscripten's stringToUTF8Array. + +function utf8len(str) { + var len = 0; + for (var i = 0; i < str.length; ++i) { + var u = str.charCodeAt(i); + if (u >= 0xD800 && u <= 0xDFFF && i < str.length-1) { + // lead surrogate - combine with next surrogate + u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); + } + + if (u <= 0x7F) { + ++len; + } else if (u <= 0x7FF) { + len += 2; + } else if (u <= 0xFFFF) { + len += 3; + } else { + len += 4; + } + } + return len; +} + +export default class TextEncoder { + /** + * Encode a javascript string as utf-8 + * + * @param {String} str String to encode + * @return {Uint8Array} UTF-8-encoded output + */ + encode(str) { + const outU8Array = new Uint8Array(utf8len(str)); + var outIdx = 0; + for (var i = 0; i < str.length; ++i) { + var u = str.charCodeAt(i); + if (u >= 0xD800 && u <= 0xDFFF && i < str.length-1) { + // lead surrogate - combine with next surrogate + u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); + } + + if (u <= 0x7F) { + outU8Array[outIdx++] = u; + } else if (u <= 0x7FF) { + outU8Array[outIdx++] = 0xC0 | (u >> 6); + outU8Array[outIdx++] = 0x80 | (u & 63); + } else if (u <= 0xFFFF) { + outU8Array[outIdx++] = 0xE0 | (u >> 12); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); + outU8Array[outIdx++] = 0x80 | (u & 63); + } else { + outU8Array[outIdx++] = 0xF0 | (u >> 18); + outU8Array[outIdx++] = 0x80 | ((u >> 12) & 63); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); + outU8Array[outIdx++] = 0x80 | (u & 63); + } + } + return outU8Array; + } +} diff --git a/test/utils/TextDecoderPolyfill-test.js b/test/utils/TextDecoderPolyfill-test.js new file mode 100644 index 0000000000..84f5edf187 --- /dev/null +++ b/test/utils/TextDecoderPolyfill-test.js @@ -0,0 +1,85 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +import TextDecoderPolyfill from 'utils/TextDecoderPolyfill'; + +import * as testUtils from '../test-utils'; +import expect from 'expect'; + +describe('textDecoderPolyfill', function() { + beforeEach(function() { + testUtils.beforeEach(this); + }); + + it('should correctly decode a range of strings', function() { + const decoder = new TextDecoderPolyfill(); + + expect(decoder.decode(Uint8Array.of(65, 66, 67))).toEqual('ABC'); + expect(decoder.decode(Uint8Array.of(0xC3, 0xA6))).toEqual('æ'); + expect(decoder.decode(Uint8Array.of(0xE2, 0x82, 0xAC))).toEqual('€'); + expect(decoder.decode(Uint8Array.of(0xF0, 0x9F, 0x92, 0xA9))).toEqual('\uD83D\uDCA9'); + }); + + it('should ignore byte-order marks', function() { + const decoder = new TextDecoderPolyfill(); + expect(decoder.decode(Uint8Array.of(0xEF, 0xBB, 0xBF, 65))) + .toEqual('A'); + }); + + it('should not ignore byte-order marks in the middle of the array', function() { + const decoder = new TextDecoderPolyfill(); + expect(decoder.decode(Uint8Array.of(65, 0xEF, 0xBB, 0xBF, 66))) + .toEqual('A\uFEFFB'); + }); + + it('should reject overlong encodings', function() { + const decoder = new TextDecoderPolyfill(); + + // euro, as 4 bytes + expect(decoder.decode(Uint8Array.of(65, 0xF0, 0x82, 0x82, 0xAC, 67))) + .toEqual('A\uFFFD\uFFFD\uFFFD\uFFFDC'); + }); + + it('should reject 5 and 6-byte encodings', function() { + const decoder = new TextDecoderPolyfill(); + + expect(decoder.decode(Uint8Array.of(65, 0xF8, 0x82, 0x82, 0x82, 0x82, 67))) + .toEqual('A\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDC'); + }); + + it('should reject code points beyond 0x10000', function() { + const decoder = new TextDecoderPolyfill(); + + expect(decoder.decode(Uint8Array.of(0xF4, 0xA0, 0x80, 0x80))) + .toEqual('\uFFFD\uFFFD\uFFFD\uFFFD'); + }); + + it('should cope with end-of-string', function() { + const decoder = new TextDecoderPolyfill(); + + expect(decoder.decode(Uint8Array.of(65, 0xC3))) + .toEqual('A\uFFFD'); + + expect(decoder.decode(Uint8Array.of(65, 0xE2, 0x82))) + .toEqual('A\uFFFD\uFFFD'); + + expect(decoder.decode(Uint8Array.of(65, 0xF0, 0x9F, 0x92))) + .toEqual('A\uFFFD\uFFFD\uFFFD'); + }); + +}); diff --git a/test/utils/TextEncoderPolyfill-test.js b/test/utils/TextEncoderPolyfill-test.js new file mode 100644 index 0000000000..4f422ec375 --- /dev/null +++ b/test/utils/TextEncoderPolyfill-test.js @@ -0,0 +1,39 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +import TextEncoderPolyfill from 'utils/TextEncoderPolyfill'; + +import * as testUtils from '../test-utils'; +import expect from 'expect'; + +describe('textEncoderPolyfill', function() { + beforeEach(function() { + testUtils.beforeEach(this); + }); + + it('should correctly encode a range of strings', function() { + const encoder = new TextEncoderPolyfill(); + + expect(encoder.encode('ABC')).toEqual(Uint8Array.of(65, 66, 67)); + expect(encoder.encode('æ')).toEqual(Uint8Array.of(0xC3, 0xA6)); + expect(encoder.encode('€')).toEqual(Uint8Array.of(0xE2, 0x82, 0xAC)); + + // PILE OF POO (💩) + expect(encoder.encode('\uD83D\uDCA9')).toEqual(Uint8Array.of(0xF0, 0x9F, 0x92, 0xA9)); + }); +}); From f8e56778ea5b0577d0d594f4416305b79ee6d281 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 11 Jan 2017 22:22:11 +0000 Subject: [PATCH 2/8] Encryption and decryption for megolm backups --- src/utils/MegolmExportEncryption.js | 312 +++++++++++++++++++++ test/utils/MegolmExportEncryption-test.js | 115 ++++++++ test/utils/generate-megolm-test-vectors.py | 117 ++++++++ 3 files changed, 544 insertions(+) create mode 100644 src/utils/MegolmExportEncryption.js create mode 100644 test/utils/MegolmExportEncryption-test.js create mode 100755 test/utils/generate-megolm-test-vectors.py diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js new file mode 100644 index 0000000000..5b2e16ef29 --- /dev/null +++ b/src/utils/MegolmExportEncryption.js @@ -0,0 +1,312 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +// polyfill textencoder if necessary +let TextEncoder = window.TextEncoder; +if (!TextEncoder) { + TextEncoder = require('./TextEncoderPolyfill'); +} +let TextDecoder = window.TextDecoder; +if (TextDecoder) { + TextDecoder = require('./TextDecoderPolyfill'); +} + +const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle; + +/** + * Decrypt a megolm key file + * + * @param {ArrayBuffer} file + * @param {String} password + * @return {Promise} promise for decrypted output + */ +export function decryptMegolmKeyFile(data, password) { + const body = unpackMegolmKeyFile(data); + + // check we have a version byte + if (body.length < 1) { + throw new Error('Invalid file: too short'); + } + + const version = body[0]; + if (version !== 1) { + throw new Error('Unsupported version'); + } + + const ciphertextLength = body.length-(1+16+16+4+32); + if (body.length < 0) { + throw new Error('Invalid file: too short'); + } + + const salt = body.subarray(1, 1+16); + const iv = body.subarray(17, 17+16); + const iterations = body[33] << 24 | body[34] << 16 | body[35] << 8 | body[36]; + const ciphertext = body.subarray(37, 37+ciphertextLength); + const hmac = body.subarray(-32); + + return deriveKeys(salt, iterations, password).then((keys) => { + const [aes_key, sha_key] = keys; + + const toVerify = body.subarray(0, -32); + return subtleCrypto.verify( + {name: 'HMAC'}, + sha_key, + hmac, + toVerify, + ).then((isValid) => { + if (!isValid) { + throw new Error('Authentication check failed: incorrect password?') + } + + return subtleCrypto.decrypt( + { + name: "AES-CTR", + counter: iv, + length: 64, + }, + aes_key, + ciphertext, + ); + }); + }).then((plaintext) => { + return new TextDecoder().decode(new Uint8Array(plaintext)); + }); +} + + +/** + * Encrypt a megolm key file + * + * @param {String} data + * @param {String} password + * @param {Object=} options + * @param {Nunber=} options.kdf_rounds Number of iterations to perform of the + * key-derivation function. + * @return {Promise} promise for encrypted output + */ +export function encryptMegolmKeyFile(data, password, options) { + options = options || {}; + const kdf_rounds = options.kdf_rounds || 100000; + + const salt = new Uint8Array(16); + window.crypto.getRandomValues(salt); + const iv = new Uint8Array(16); + window.crypto.getRandomValues(iv); + + return deriveKeys(salt, kdf_rounds, password).then((keys) => { + const [aes_key, sha_key] = keys; + + return subtleCrypto.encrypt( + { + name: "AES-CTR", + counter: iv, + length: 64, + }, + aes_key, + new TextEncoder().encode(data), + ).then((ciphertext) => { + const cipherArray = new Uint8Array(ciphertext); + const bodyLength = (1+salt.length+iv.length+4+cipherArray.length+32); + const resultBuffer = new Uint8Array(bodyLength); + let idx = 0; + resultBuffer[idx++] = 1; // version + resultBuffer.set(salt, idx); idx += salt.length; + resultBuffer.set(iv, idx); idx += iv.length; + resultBuffer[idx++] = kdf_rounds >> 24; + resultBuffer[idx++] = (kdf_rounds >> 16) & 0xff; + resultBuffer[idx++] = (kdf_rounds >> 8) & 0xff; + resultBuffer[idx++] = kdf_rounds & 0xff; + resultBuffer.set(cipherArray, idx); idx += cipherArray.length; + + const toSign = resultBuffer.subarray(0, idx); + + return subtleCrypto.sign( + {name: 'HMAC'}, + sha_key, + toSign, + ).then((hmac) => { + hmac = new Uint8Array(hmac); + resultBuffer.set(hmac, idx); + return packMegolmKeyFile(resultBuffer); + }); + }); + }); +} + +/** + * Derive the AES and SHA keys for the file + * + * @param {Unit8Array} salt salt for pbkdf + * @param {Number} iterations number of pbkdf iterations + * @param {String} password password + * @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, sha key] + */ +function deriveKeys(salt, iterations, password) { + return subtleCrypto.importKey( + 'raw', + new TextEncoder().encode(password), + {name: 'PBKDF2'}, + false, + ['deriveBits'] + ).then((key) => { + return subtleCrypto.deriveBits( + { + name: 'PBKDF2', + salt: salt, + iterations: iterations, + hash: 'SHA-512', + }, + key, + 512 + ); + }).then((keybits) => { + const aes_key = keybits.slice(0, 32); + const sha_key = keybits.slice(32); + + const aes_prom = subtleCrypto.importKey( + 'raw', + aes_key, + {name: 'AES-CTR'}, + false, + ['encrypt', 'decrypt'] + ); + const sha_prom = subtleCrypto.importKey( + 'raw', + sha_key, + { + name: 'HMAC', + hash: {name: 'SHA-256'}, + }, + false, + ['sign', 'verify'] + ); + return Promise.all([aes_prom, sha_prom]); + }); +} + +const HEADER_LINE = '-----BEGIN MEGOLM SESSION DATA-----'; +const TRAILER_LINE = '-----END MEGOLM SESSION DATA-----'; + +/** + * Unbase64 an ascii-armoured megolm key file + * + * Strips the header and trailer lines, and unbase64s the content + * + * @param {ArrayBuffer} data input file + * @return {Uint8Array} unbase64ed content + */ +function unpackMegolmKeyFile(data) { + // parse the file as a great big String. This should be safe, because there + // should be no non-ASCII characters, and it means that we can do string + // comparisons to find the header and footer, and feed it into window.atob. + const fileStr = new TextDecoder().decode(new Uint8Array(data)); + + // look for the start line + let lineStart = 0; + while (1) { + const lineEnd = fileStr.indexOf('\n', lineStart); + if (lineEnd < 0) { + throw new Error('Header line not found'); + } + const line = fileStr.slice(lineStart, lineEnd).trim(); + + // start the next line after the newline + lineStart = lineEnd+1; + + if (line === HEADER_LINE) { + break; + } + } + + const dataStart = lineStart; + + // look for the end line + while (1) { + const lineEnd = fileStr.indexOf('\n', lineStart); + const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd) + .trim(); + if (line === TRAILER_LINE) { + break; + } + + if (lineEnd < 0) { + throw new Error('Trailer line not found'); + } + + // start the next line after the newline + lineStart = lineEnd+1; + } + + const dataEnd = lineStart; + return decodeBase64(fileStr.slice(dataStart, dataEnd)); +} + +/** + * ascii-armour a megolm key file + * + * base64s the content, and adds header and trailer lines + * + * @param {Uint8Array} data raw data + * @return {ArrayBuffer} formatted file + */ +function packMegolmKeyFile(data) { + // we split into lines before base64ing, because encodeBase64 doesn't deal + // terribly well with large arrays. + const LINE_LENGTH = (72 * 4 / 3); + const nLines = Math.ceil(data.length / LINE_LENGTH); + const lines = new Array(nLines + 3); + lines[0] = HEADER_LINE; + let o = 0; + let i; + for (i = 1; i <= nLines; i++) { + lines[i] = encodeBase64(data.subarray(o, o+LINE_LENGTH)); + o += LINE_LENGTH; + } + lines[i++] = TRAILER_LINE; + lines[i] = ''; + return (new TextEncoder().encode(lines.join('\n'))).buffer; +} + +/** + * Encode a typed array of uint8 as base64. + * @param {Uint8Array} uint8Array The data to encode. + * @return {string} The base64. + */ +function encodeBase64(uint8Array) { + // Misinterpt the Uint8Array as Latin-1. + // window.btoa expects a unicode string with codepoints in the range 0-255. + var latin1String = String.fromCharCode.apply(null, uint8Array); + // Use the builtin base64 encoder. + return window.btoa(latin1String); +} + +/** + * Decode a base64 string to a typed array of uint8. + * @param {string} base64 The base64 to decode. + * @return {Uint8Array} The decoded data. + */ +function decodeBase64(base64) { + // window.atob returns a unicode string with codepoints in the range 0-255. + var latin1String = window.atob(base64); + // Encode the string as a Uint8Array + var uint8Array = new Uint8Array(latin1String.length); + for (var i = 0; i < latin1String.length; i++) { + uint8Array[i] = latin1String.charCodeAt(i); + } + return uint8Array; +} diff --git a/test/utils/MegolmExportEncryption-test.js b/test/utils/MegolmExportEncryption-test.js new file mode 100644 index 0000000000..fa51d83c6d --- /dev/null +++ b/test/utils/MegolmExportEncryption-test.js @@ -0,0 +1,115 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +"use strict"; + +import * as MegolmExportEncryption from 'utils/MegolmExportEncryption'; + +import * as testUtils from '../test-utils'; +import expect from 'expect'; + +// polyfill textencoder if necessary +let TextEncoder = window.TextEncoder; +if (!TextEncoder) { + TextEncoder = require('utils/TextEncoderPolyfill'); +} + +const TEST_VECTORS=[ + [ + "plain", + "password", + "-----BEGIN MEGOLM SESSION DATA-----\nAXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\ncissyYBxjsfsAndErh065A8=\n-----END MEGOLM SESSION DATA-----" + ], + [ + "Hello, World", + "betterpassword", + "-----BEGIN MEGOLM SESSION DATA-----\nAW1vcmVzYWx0bW9yZXNhbHT//////////wAAAAAAAAAAAAAD6KyBpe1Niv5M5NPm4ZATsJo5nghk\nKYu63a0YQ5DRhUWEKk7CcMkrKnAUiZny\n-----END MEGOLM SESSION DATA-----" + ], + [ + "alphanumericallyalphanumericallyalphanumericallyalphanumerically", + "SWORDFISH", + "-----BEGIN MEGOLM SESSION DATA-----\nAXllc3NhbHR5Z29vZG5lc3P//////////wAAAAAAAAAAAAAD6OIW+Je7gwvjd4kYrb+49gKCfExw\nMgJBMD4mrhLkmgAngwR1pHjbWXaoGybtiAYr0moQ93GrBQsCzPbvl82rZhaXO3iH5uHo/RCEpOqp\nPgg29363BGR+/Ripq/VCLKGNbw==\n-----END MEGOLM SESSION DATA-----" + ], + [ + "alphanumericallyalphanumericallyalphanumericallyalphanumerically", + "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword", + "-----BEGIN MEGOLM SESSION DATA-----\nAf//////////////////////////////////////////AAAD6IAZJy7IQ7Y0idqSw/bmpngEEVVh\ngsH+8ptgqxw6ZVWQnohr8JsuwH9SwGtiebZuBu5smPCO+RFVWH2cQYslZijXv/BEH/txvhUrrtCd\nbWnSXS9oymiqwUIGs08sXI33ZA==\n-----END MEGOLM SESSION DATA-----" + ] +] +; + +function stringToArray(s) { + return new TextEncoder().encode(s).buffer; +} + +describe('MegolmExportEncryption', function() { + beforeEach(function() { + testUtils.beforeEach(this); + }); + + describe('decrypt', function() { + it('should handle missing header', function() { + const input=stringToArray(`-----`); + expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')}) + .toThrow('Header line not found'); + }); + + it('should handle missing trailer', function() { + const input=stringToArray(`-----BEGIN MEGOLM SESSION DATA----- +-----`); + expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')}) + .toThrow('Trailer line not found'); + }); + + it('should decrypt a range of inputs', function(done) { + function next(i) { + if (i >= TEST_VECTORS.length) { + done(); + return; + } + + const [plain, password, input] = TEST_VECTORS[i]; + return MegolmExportEncryption.decryptMegolmKeyFile( + stringToArray(input), password + ).then((decrypted) => { + expect(decrypted).toEqual(plain); + return next(i+1); + }) + }; + return next(0).catch(done); + }); + }); + + describe('encrypt', function() { + it('should round-trip', function(done) { + const input = + 'words words many words in plain text here'.repeat(100); + + const password = 'my super secret passphrase'; + + return MegolmExportEncryption.encryptMegolmKeyFile( + input, password, {kdf_rounds: 1000}, + ).then((ciphertext) => { + return MegolmExportEncryption.decryptMegolmKeyFile( + ciphertext, password + ); + }).then((plaintext) => { + expect(plaintext).toEqual(input); + done(); + }).catch(done); + }); + }); +}); diff --git a/test/utils/generate-megolm-test-vectors.py b/test/utils/generate-megolm-test-vectors.py new file mode 100755 index 0000000000..0ce5f5e4b3 --- /dev/null +++ b/test/utils/generate-megolm-test-vectors.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import base64 +import json +import struct + +from cryptography.hazmat import backends +from cryptography.hazmat.primitives import ciphers, hashes, hmac +from cryptography.hazmat.primitives.kdf import pbkdf2 +from cryptography.hazmat.primitives.ciphers import algorithms, modes + +backend = backends.default_backend() + +def parse_u128(s): + a, b = struct.unpack(">QQ", s) + return (a << 64) | b + +def encrypt_ctr(key, iv, plaintext, counter_bits=64): + alg = algorithms.AES(key) + + # Some AES-CTR implementations treat some parts of the IV as a nonce (which + # remains constant throughought encryption), and some as a counter (which + # increments every block, ie 16 bytes, and wraps after a while). Different + # implmententations use different amounts of the IV for each part. + # + # The python cryptography library uses the whole IV as a counter; to make + # it match other implementations with a given counter size, we manually + # implement wrapping the counter. + + # number of AES blocks between each counter wrap + limit = 1 << counter_bits + + # parse IV as a 128-bit int + parsed_iv = parse_u128(iv) + + # split IV into counter and nonce + counter = parsed_iv & (limit - 1) + nonce = parsed_iv & ~(limit - 1) + + # encrypt up to the first counter wraparound + size = 16 * (limit - counter) + encryptor = ciphers.Cipher( + alg, + modes.CTR(iv), + backend=backend + ).encryptor() + input = plaintext[:size] + result = encryptor.update(input) + encryptor.finalize() + offset = size + + # do remaining data starting with a counter of zero + iv = struct.pack(">QQ", nonce >> 64, nonce & ((1 << 64) - 1)) + size = 16 * limit + + while offset < len(plaintext): + encryptor = ciphers.Cipher( + alg, + modes.CTR(iv), + backend=backend + ).encryptor() + input = plaintext[offset:offset+size] + result += encryptor.update(input) + encryptor.finalize() + offset += size + + return result + +def hmac_sha256(key, message): + h = hmac.HMAC(key, hashes.SHA256(), backend=backend) + h.update(message) + return h.finalize() + +def encrypt(key, iv, salt, plaintext, iterations=1000): + """ + Returns: + (bytes) ciphertext + """ + if len(salt) != 16: + raise Exception("Expected 128 bits of salt - got %i bits" % len((salt) * 8)) + if len(iv) != 16: + raise Exception("Expected 128 bits of IV - got %i bits" % (len(iv) * 8)) + + sha = hashes.SHA512() + kdf = pbkdf2.PBKDF2HMAC(sha, 64, salt, iterations, backend) + k = kdf.derive(key) + + aes_key = k[0:32] + sha_key = k[32:] + + packed_file = ( + b"\x01" # version + + salt + + iv + + struct.pack(">L", iterations) + + encrypt_ctr(aes_key, iv, plaintext) + ) + packed_file += hmac_sha256(sha_key, packed_file) + + return ( + b"-----BEGIN MEGOLM SESSION DATA-----\n" + + base64.encodestring(packed_file) + + b"-----END MEGOLM SESSION DATA-----" + ) + +def gen(password, iv, salt, plaintext, iterations=1000): + ciphertext = encrypt( + password.encode('utf-8'), iv, salt, plaintext.encode('utf-8'), iterations + ) + return (plaintext, password, ciphertext.decode('utf-8')) + +print (json.dumps([ + gen("password", b"\x88"*16, b"saltsaltsaltsalt", "plain", 10), + gen("betterpassword", b"\xFF"*8 + b"\x00"*8, b"moresaltmoresalt", "Hello, World"), + gen("SWORDFISH", b"\xFF"*8 + b"\x00"*8, b"yessaltygoodness", "alphanumerically" * 4), + gen("password"*32, b"\xFF"*16, b"\xFF"*16, "alphanumerically" * 4), +], indent=4)) From d63f7e83599937a8b9e5dbff638b0a93efa7fafa Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sat, 14 Jan 2017 01:21:26 +0000 Subject: [PATCH 3/8] Expose megolm import/export via the devtools --- src/index.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/index.js b/src/index.js index 4b920d95d4..5d4145a39b 100644 --- a/src/index.js +++ b/src/index.js @@ -28,3 +28,27 @@ module.exports.getComponent = function(componentName) { return Skinner.getComponent(componentName); }; + +/* hacky functions for megolm import/export until we give it a UI */ +import * as MegolmExportEncryption from './utils/MegolmExportEncryption'; +import MatrixClientPeg from './MatrixClientPeg'; + +window.exportKeys = function(password) { + return MatrixClientPeg.get().exportRoomKeys().then((k) => { + return MegolmExportEncryption.encryptMegolmKeyFile( + JSON.stringify(k), password + ); + }).then((f) => { + console.log(new TextDecoder().decode(new Uint8Array(f))); + }).done(); +}; + +window.importKeys = function(password, data) { + const arrayBuffer = new TextEncoder().encode(data).buffer; + return MegolmExportEncryption.decryptMegolmKeyFile( + arrayBuffer, password + ).then((j) => { + const k = JSON.parse(j); + return MatrixClientPeg.get().importRoomKeys(k); + }); +}; From e37bf6b7be824caade5019da1bc1a4b92c9d6114 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sat, 14 Jan 2017 01:41:48 +0000 Subject: [PATCH 4/8] Skip crypto tests on PhantomJS --- test/utils/MegolmExportEncryption-test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/utils/MegolmExportEncryption-test.js b/test/utils/MegolmExportEncryption-test.js index fa51d83c6d..db38a931ed 100644 --- a/test/utils/MegolmExportEncryption-test.js +++ b/test/utils/MegolmExportEncryption-test.js @@ -56,6 +56,13 @@ function stringToArray(s) { } describe('MegolmExportEncryption', function() { + before(function() { + // if we don't have subtlecrypto, go home now + if (!window.crypto.subtle && !window.crypto.webkitSubtle) { + this.skip(); + } + }) + beforeEach(function() { testUtils.beforeEach(this); }); From 09ce74cc767a5f98021f5980ce7f4e2b2aece7c4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 16 Jan 2017 18:44:46 +0000 Subject: [PATCH 5/8] Fix a couple of minor review comments --- src/utils/MegolmExportEncryption.js | 20 ++++++++++---------- src/utils/TextEncoderPolyfill.js | 12 ++++++------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index 5b2e16ef29..351f58aaa6 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -60,12 +60,12 @@ export function decryptMegolmKeyFile(data, password) { const hmac = body.subarray(-32); return deriveKeys(salt, iterations, password).then((keys) => { - const [aes_key, sha_key] = keys; + const [aes_key, hmac_key] = keys; const toVerify = body.subarray(0, -32); return subtleCrypto.verify( {name: 'HMAC'}, - sha_key, + hmac_key, hmac, toVerify, ).then((isValid) => { @@ -109,7 +109,7 @@ export function encryptMegolmKeyFile(data, password, options) { window.crypto.getRandomValues(iv); return deriveKeys(salt, kdf_rounds, password).then((keys) => { - const [aes_key, sha_key] = keys; + const [aes_key, hmac_key] = keys; return subtleCrypto.encrypt( { @@ -137,7 +137,7 @@ export function encryptMegolmKeyFile(data, password, options) { return subtleCrypto.sign( {name: 'HMAC'}, - sha_key, + hmac_key, toSign, ).then((hmac) => { hmac = new Uint8Array(hmac); @@ -149,12 +149,12 @@ export function encryptMegolmKeyFile(data, password, options) { } /** - * Derive the AES and SHA keys for the file + * Derive the AES and HMAC-SHA-256 keys for the file * * @param {Unit8Array} salt salt for pbkdf * @param {Number} iterations number of pbkdf iterations * @param {String} password password - * @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, sha key] + * @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key] */ function deriveKeys(salt, iterations, password) { return subtleCrypto.importKey( @@ -176,7 +176,7 @@ function deriveKeys(salt, iterations, password) { ); }).then((keybits) => { const aes_key = keybits.slice(0, 32); - const sha_key = keybits.slice(32); + const hmac_key = keybits.slice(32); const aes_prom = subtleCrypto.importKey( 'raw', @@ -185,9 +185,9 @@ function deriveKeys(salt, iterations, password) { false, ['encrypt', 'decrypt'] ); - const sha_prom = subtleCrypto.importKey( + const hmac_prom = subtleCrypto.importKey( 'raw', - sha_key, + hmac_key, { name: 'HMAC', hash: {name: 'SHA-256'}, @@ -195,7 +195,7 @@ function deriveKeys(salt, iterations, password) { false, ['sign', 'verify'] ); - return Promise.all([aes_prom, sha_prom]); + return Promise.all([aes_prom, hmac_prom]); }); } diff --git a/src/utils/TextEncoderPolyfill.js b/src/utils/TextEncoderPolyfill.js index 2da09710f2..41ee4782a9 100644 --- a/src/utils/TextEncoderPolyfill.js +++ b/src/utils/TextEncoderPolyfill.js @@ -61,16 +61,16 @@ export default class TextEncoder { outU8Array[outIdx++] = u; } else if (u <= 0x7FF) { outU8Array[outIdx++] = 0xC0 | (u >> 6); - outU8Array[outIdx++] = 0x80 | (u & 63); + outU8Array[outIdx++] = 0x80 | (u & 0x3F); } else if (u <= 0xFFFF) { outU8Array[outIdx++] = 0xE0 | (u >> 12); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); - outU8Array[outIdx++] = 0x80 | (u & 63); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 0x3F); + outU8Array[outIdx++] = 0x80 | (u & 0x3F); } else { outU8Array[outIdx++] = 0xF0 | (u >> 18); - outU8Array[outIdx++] = 0x80 | ((u >> 12) & 63); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); - outU8Array[outIdx++] = 0x80 | (u & 63); + outU8Array[outIdx++] = 0x80 | ((u >> 12) & 0x3F); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 0x3F); + outU8Array[outIdx++] = 0x80 | (u & 0x3F); } } return outU8Array; From 31df78f946ef64f73007c6183d17e1c4dc326448 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 18 Jan 2017 11:39:44 +0000 Subject: [PATCH 6/8] Use text-encoding-utf-8 as a TextEncoder polyfill Somebody else seems to have done a good job of polyfilling TextEncoder, so let's use that. --- package.json | 1 + .../views/dialogs/ExportE2eKeysDialog.js | 84 +++++++++++ src/utils/MegolmExportEncryption.js | 7 +- src/utils/TextDecoderPolyfill.js | 131 ------------------ src/utils/TextEncoderPolyfill.js | 78 ----------- test/utils/TextDecoderPolyfill-test.js | 85 ------------ test/utils/TextEncoderPolyfill-test.js | 39 ------ 7 files changed, 89 insertions(+), 336 deletions(-) create mode 100644 src/async-components/views/dialogs/ExportE2eKeysDialog.js delete mode 100644 src/utils/TextDecoderPolyfill.js delete mode 100644 src/utils/TextEncoderPolyfill.js delete mode 100644 test/utils/TextDecoderPolyfill-test.js delete mode 100644 test/utils/TextEncoderPolyfill-test.js diff --git a/package.json b/package.json index 1eaee39c41..e0cfb72148 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "react-dom": "^15.4.0", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "sanitize-html": "^1.11.1", + "text-encoding-utf-8": "^1.0.1", "velocity-vector": "vector-im/velocity#059e3b2", "whatwg-fetch": "^1.0.0" }, diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js new file mode 100644 index 0000000000..284d299f4b --- /dev/null +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -0,0 +1,84 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import sdk from '../../../index'; + +import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; + +export default React.createClass({ + displayName: 'ExportE2eKeysDialog', + + getInitialState: function() { + return { + collectedPassword: false, + }; + }, + + _onPassphraseFormSubmit: function(ev) { + ev.preventDefault(); + console.log(this.refs.passphrase1.value); + return false; + }, + + render: function() { + let content; + if (!this.state.collectedPassword) { + content = ( +
+

+ This process will allow you to export the keys for messages + you have received in encrypted rooms to a local file. You + will then be able to import the file into another Matrix + client in the future, so that client will also be able to + decrypt these messages. +

+

+ The exported file will allow anyone who can read it to decrypt + any encrypted messages that you can see, so you should be + careful to keep it secure. To help with this, you should enter + a passphrase below, which will be used to encrypt the exported + data. It will only be possible to import the data by using the + same passphrase. +

+
+
+ +
+
+ +
+
+ +
+
+
+ ); + } + + return ( +
+
+ Export room keys +
+ {content} +
+ ); + }, +}); diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index 351f58aaa6..e3ca7e68f2 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -17,13 +17,14 @@ limitations under the License. "use strict"; // polyfill textencoder if necessary +import * as TextEncodingUtf8 from 'text-encoding-utf-8'; let TextEncoder = window.TextEncoder; if (!TextEncoder) { - TextEncoder = require('./TextEncoderPolyfill'); + TextEncoder = TextEncodingUtf8.TextEncoder; } let TextDecoder = window.TextDecoder; -if (TextDecoder) { - TextDecoder = require('./TextDecoderPolyfill'); +if (!TextDecoder) { + TextDecoder = TextEncodingUtf8.TextDecoder; } const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle; diff --git a/src/utils/TextDecoderPolyfill.js b/src/utils/TextDecoderPolyfill.js deleted file mode 100644 index e203676bb7..0000000000 --- a/src/utils/TextDecoderPolyfill.js +++ /dev/null @@ -1,131 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -"use strict"; - -// Polyfill for TextDecoder. - -const REPLACEMENT_CHAR = '\uFFFD'; - -export default class TextDecoder { - /** - * Decode a UTF-8 byte array as a javascript string - * - * @param {Uint8Array} u8Array UTF-8-encoded onput - * @return {str} - */ - decode(u8Array) { - let u0, u1, u2, u3; - - let str = ''; - let idx = 0; - while (idx < u8Array.length) { - u0 = u8Array[idx++]; - if (!(u0 & 0x80)) { - str += String.fromCharCode(u0); - continue; - } - - if ((u0 & 0xC0) != 0xC0) { - // continuation byte where we expect a leading byte - str += REPLACEMENT_CHAR; - continue; - } - - if (u0 > 0xF4) { - // this would imply a 5-byte or longer encoding, which is - // invalid and unsupported here. - str += REPLACEMENT_CHAR; - continue; - } - - u1 = u8Array[idx++]; - if (u1 === undefined) { - str += REPLACEMENT_CHAR; - continue; - } - - if ((u1 & 0xC0) != 0x80) { - // leading byte where we expect a continuation byte - str += REPLACEMENT_CHAR.repeat(2); - continue; - } - u1 &= 0x3F; - if (!(u0 & 0x20)) { - const u = ((u0 & 0x1F) << 6) | u1; - if (u < 0x80) { - // over-long - str += REPLACEMENT_CHAR.repeat(2); - } else { - str += String.fromCharCode(u); - } - continue; - } - - u2 = u8Array[idx++]; - if (u2 === undefined) { - str += REPLACEMENT_CHAR.repeat(2); - continue; - } - if ((u2 & 0xC0) != 0x80) { - // leading byte where we expect a continuation byte - str += REPLACEMENT_CHAR.repeat(3); - continue; - } - u2 &= 0x3F; - if (!(u0 & 0x10)) { - const u = ((u0 & 0x0F) << 12) | (u1 << 6) | u2; - if (u < 0x800) { - // over-long - str += REPLACEMENT_CHAR.repeat(3); - } else if (u == 0xFEFF && idx == 3) { - // byte-order mark: do not add to output - } else { - str += String.fromCharCode(u); - } - continue; - } - - u3 = u8Array[idx++]; - if (u3 === undefined) { - str += REPLACEMENT_CHAR.repeat(3); - continue; - } - if ((u3 & 0xC0) != 0x80) { - // leading byte where we expect a continuation byte - str += REPLACEMENT_CHAR.repeat(4); - continue; - } - u3 &= 0x3F; - const u = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | u3; - if (u < 0x10000) { - // over-long - str += REPLACEMENT_CHAR.repeat(4); - continue; - } - if (u > 0x1FFFF) { - // unicode stops here. - str += REPLACEMENT_CHAR.repeat(4); - continue; - } - - // encode as utf-16 - const v = u - 0x10000; - str += String.fromCharCode(0xD800 | (v >> 10), 0xDC00 | (v & 0x3FF)); - } - return str; - } -} diff --git a/src/utils/TextEncoderPolyfill.js b/src/utils/TextEncoderPolyfill.js deleted file mode 100644 index 41ee4782a9..0000000000 --- a/src/utils/TextEncoderPolyfill.js +++ /dev/null @@ -1,78 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -"use strict"; - -// Polyfill for TextEncoder. Based on emscripten's stringToUTF8Array. - -function utf8len(str) { - var len = 0; - for (var i = 0; i < str.length; ++i) { - var u = str.charCodeAt(i); - if (u >= 0xD800 && u <= 0xDFFF && i < str.length-1) { - // lead surrogate - combine with next surrogate - u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); - } - - if (u <= 0x7F) { - ++len; - } else if (u <= 0x7FF) { - len += 2; - } else if (u <= 0xFFFF) { - len += 3; - } else { - len += 4; - } - } - return len; -} - -export default class TextEncoder { - /** - * Encode a javascript string as utf-8 - * - * @param {String} str String to encode - * @return {Uint8Array} UTF-8-encoded output - */ - encode(str) { - const outU8Array = new Uint8Array(utf8len(str)); - var outIdx = 0; - for (var i = 0; i < str.length; ++i) { - var u = str.charCodeAt(i); - if (u >= 0xD800 && u <= 0xDFFF && i < str.length-1) { - // lead surrogate - combine with next surrogate - u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); - } - - if (u <= 0x7F) { - outU8Array[outIdx++] = u; - } else if (u <= 0x7FF) { - outU8Array[outIdx++] = 0xC0 | (u >> 6); - outU8Array[outIdx++] = 0x80 | (u & 0x3F); - } else if (u <= 0xFFFF) { - outU8Array[outIdx++] = 0xE0 | (u >> 12); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 0x3F); - outU8Array[outIdx++] = 0x80 | (u & 0x3F); - } else { - outU8Array[outIdx++] = 0xF0 | (u >> 18); - outU8Array[outIdx++] = 0x80 | ((u >> 12) & 0x3F); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 0x3F); - outU8Array[outIdx++] = 0x80 | (u & 0x3F); - } - } - return outU8Array; - } -} diff --git a/test/utils/TextDecoderPolyfill-test.js b/test/utils/TextDecoderPolyfill-test.js deleted file mode 100644 index 84f5edf187..0000000000 --- a/test/utils/TextDecoderPolyfill-test.js +++ /dev/null @@ -1,85 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -"use strict"; - -import TextDecoderPolyfill from 'utils/TextDecoderPolyfill'; - -import * as testUtils from '../test-utils'; -import expect from 'expect'; - -describe('textDecoderPolyfill', function() { - beforeEach(function() { - testUtils.beforeEach(this); - }); - - it('should correctly decode a range of strings', function() { - const decoder = new TextDecoderPolyfill(); - - expect(decoder.decode(Uint8Array.of(65, 66, 67))).toEqual('ABC'); - expect(decoder.decode(Uint8Array.of(0xC3, 0xA6))).toEqual('æ'); - expect(decoder.decode(Uint8Array.of(0xE2, 0x82, 0xAC))).toEqual('€'); - expect(decoder.decode(Uint8Array.of(0xF0, 0x9F, 0x92, 0xA9))).toEqual('\uD83D\uDCA9'); - }); - - it('should ignore byte-order marks', function() { - const decoder = new TextDecoderPolyfill(); - expect(decoder.decode(Uint8Array.of(0xEF, 0xBB, 0xBF, 65))) - .toEqual('A'); - }); - - it('should not ignore byte-order marks in the middle of the array', function() { - const decoder = new TextDecoderPolyfill(); - expect(decoder.decode(Uint8Array.of(65, 0xEF, 0xBB, 0xBF, 66))) - .toEqual('A\uFEFFB'); - }); - - it('should reject overlong encodings', function() { - const decoder = new TextDecoderPolyfill(); - - // euro, as 4 bytes - expect(decoder.decode(Uint8Array.of(65, 0xF0, 0x82, 0x82, 0xAC, 67))) - .toEqual('A\uFFFD\uFFFD\uFFFD\uFFFDC'); - }); - - it('should reject 5 and 6-byte encodings', function() { - const decoder = new TextDecoderPolyfill(); - - expect(decoder.decode(Uint8Array.of(65, 0xF8, 0x82, 0x82, 0x82, 0x82, 67))) - .toEqual('A\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDC'); - }); - - it('should reject code points beyond 0x10000', function() { - const decoder = new TextDecoderPolyfill(); - - expect(decoder.decode(Uint8Array.of(0xF4, 0xA0, 0x80, 0x80))) - .toEqual('\uFFFD\uFFFD\uFFFD\uFFFD'); - }); - - it('should cope with end-of-string', function() { - const decoder = new TextDecoderPolyfill(); - - expect(decoder.decode(Uint8Array.of(65, 0xC3))) - .toEqual('A\uFFFD'); - - expect(decoder.decode(Uint8Array.of(65, 0xE2, 0x82))) - .toEqual('A\uFFFD\uFFFD'); - - expect(decoder.decode(Uint8Array.of(65, 0xF0, 0x9F, 0x92))) - .toEqual('A\uFFFD\uFFFD\uFFFD'); - }); - -}); diff --git a/test/utils/TextEncoderPolyfill-test.js b/test/utils/TextEncoderPolyfill-test.js deleted file mode 100644 index 4f422ec375..0000000000 --- a/test/utils/TextEncoderPolyfill-test.js +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -"use strict"; - -import TextEncoderPolyfill from 'utils/TextEncoderPolyfill'; - -import * as testUtils from '../test-utils'; -import expect from 'expect'; - -describe('textEncoderPolyfill', function() { - beforeEach(function() { - testUtils.beforeEach(this); - }); - - it('should correctly encode a range of strings', function() { - const encoder = new TextEncoderPolyfill(); - - expect(encoder.encode('ABC')).toEqual(Uint8Array.of(65, 66, 67)); - expect(encoder.encode('æ')).toEqual(Uint8Array.of(0xC3, 0xA6)); - expect(encoder.encode('€')).toEqual(Uint8Array.of(0xE2, 0x82, 0xAC)); - - // PILE OF POO (💩) - expect(encoder.encode('\uD83D\uDCA9')).toEqual(Uint8Array.of(0xF0, 0x9F, 0x92, 0xA9)); - }); -}); From 8b60cb9df0594bb032a560428dfa1f67f068c08c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 10:43:41 +0000 Subject: [PATCH 7/8] Megolm export: Clear bit 63 of the salt --- src/utils/MegolmExportEncryption.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index e3ca7e68f2..983ec2c75f 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -106,6 +106,12 @@ export function encryptMegolmKeyFile(data, password, options) { const salt = new Uint8Array(16); window.crypto.getRandomValues(salt); + + // clear bit 63 of the salt to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of salt is a price we have to pay. + salt[9] &= 0x7f; + const iv = new Uint8Array(16); window.crypto.getRandomValues(iv); From fdc213cbb80f84a202c7d55fa79a7c45a66af4d0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 10:44:01 +0000 Subject: [PATCH 8/8] Megolm export: fix test --- test/utils/MegolmExportEncryption-test.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/utils/MegolmExportEncryption-test.js b/test/utils/MegolmExportEncryption-test.js index db38a931ed..28752ae529 100644 --- a/test/utils/MegolmExportEncryption-test.js +++ b/test/utils/MegolmExportEncryption-test.js @@ -21,12 +21,6 @@ import * as MegolmExportEncryption from 'utils/MegolmExportEncryption'; import * as testUtils from '../test-utils'; import expect from 'expect'; -// polyfill textencoder if necessary -let TextEncoder = window.TextEncoder; -if (!TextEncoder) { - TextEncoder = require('utils/TextEncoderPolyfill'); -} - const TEST_VECTORS=[ [ "plain",