diff --git a/dist/converse.js b/dist/converse.js index 3f3cc9bd4..664b8ee15 100644 --- a/dist/converse.js +++ b/dist/converse.js @@ -71769,23 +71769,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ }); }, - getKeyAndTag(string) { - return { - 'key': string.slice(0, 22), - 'tag': string.slice(22) - }; - }, - decryptMessage(obj) { - const _converse = this.__super__._converse, - key_obj = { - "alg": "A128GCM", - "ext": true, - "k": obj.key, - "key_ops": ["encrypt", "decrypt"], - "kty": "oct" - }; - return crypto.subtle.importKey('jwk', key_obj, KEY_ALGO, true, ['encrypt', 'decrypt']).then(key_obj => { + return crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt', 'decrypt']).then(key_obj => { const algo = { 'name': "AES-GCM", 'iv': u.base64ToArrayBuffer(obj.iv), @@ -71808,17 +71793,17 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ decrypt(attrs) { const _converse = this.__super__._converse, - address = new libsignal.SignalProtocolAddress(attrs.from, parseInt(attrs.encrypted.device_id, 10)), - session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); // https://xmpp.org/extensions/xep-0384.html#usecases-receiving + session_cipher = this.getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10)); // https://xmpp.org/extensions/xep-0384.html#usecases-receiving if (attrs.encrypted.prekey === 'true') { let plaintext; return session_cipher.decryptPreKeyWhisperMessage(u.base64ToArrayBuffer(attrs.encrypted.key), 'binary').then(key_and_tag => { if (attrs.encrypted.payload) { - const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag)); + const key = key_and_tag.slice(0, 16), + tag = key_and_tag.slice(16); return this.decryptMessage(_.extend(attrs.encrypted, { - 'key': aes_data.key, - 'tag': aes_data.tag + 'key': key, + 'tag': tag })); } @@ -71842,10 +71827,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ }); } else { return session_cipher.decryptWhisperMessage(u.base64ToArrayBuffer(attrs.encrypted.key), 'binary').then(key_and_tag => { - const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag)); + const key = key_and_tag.slice(0, 16), + tag = key_and_tag.slice(16); return this.decryptMessage(_.extend(attrs.encrypted, { - 'key': aes_data.key, - 'tag': aes_data.tag + 'key': key, + 'tag': tag })); }).then(plaintext => _.extend(attrs, { 'plaintext': plaintext @@ -71909,12 +71895,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ }; return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext)); }).then(ciphertext => { - return window.crypto.subtle.exportKey("jwk", key).then(key_obj => { - const tag = u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3))); + return window.crypto.subtle.exportKey("raw", key).then(key => { + const tag = ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3)); return Promise.resolve({ - 'key': key_obj.k, - 'tag': tag, - 'key_and_tag': key_obj.k + tag, + 'key': key, + 'key_and_tag': u.appendArrayBuffer(key, tag), 'payload': u.arrayBufferToBase64(ciphertext), 'iv': u.arrayBufferToBase64(iv) }); @@ -71922,11 +71907,18 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ }); }, + getSessionCipher(jid, id) { + if (!this.session_cipher) { + const _converse = this.__super__._converse, + address = new libsignal.SignalProtocolAddress(jid, id); + this.session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); + } + + return this.session_cipher; + }, + encryptKey(plaintext, device) { - const _converse = this.__super__._converse, - address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')), - session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); - return session_cipher.encrypt(plaintext).then(payload => ({ + return this.getSessionCipher(device.get('jid'), device.get('id')).encrypt(plaintext).then(payload => ({ 'payload': payload, 'device': device })); @@ -81290,26 +81282,37 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ return fp; }; + u.appendArrayBuffer = function (buffer1, buffer2) { + const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); + tmp.set(new Uint8Array(buffer1), 0); + tmp.set(new Uint8Array(buffer2), buffer1.byteLength); + return tmp.buffer; + }; + u.arrayBufferToHex = function (ab) { // https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex#40031979 return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join(''); }; u.arrayBufferToString = function (ab) { - const enc = new TextDecoder("utf-8"); - return enc.decode(ab); + return new Uint8Array(ab).reduce((data, byte) => data + String.fromCharCode(byte), ''); + }; + + u.stringToArrayBuffer = function (string) { + const len = string.length, + bytes = new Uint8Array(len); + + for (let i = 0; i < len; i++) { + bytes[i] = string.charCodeAt(i); + } + + return bytes.buffer; }; u.arrayBufferToBase64 = function (ab) { return btoa(new Uint8Array(ab).reduce((data, byte) => data + String.fromCharCode(byte), '')); }; - u.stringToArrayBuffer = function (string) { - const enc = new TextEncoder(); // always utf-8 - - return enc.encode(string); - }; - u.base64ToArrayBuffer = function (b64) { const binary_string = window.atob(b64), len = binary_string.length, diff --git a/spec/omemo.js b/spec/omemo.js index 3337f94bf..17a015dbc 100644 --- a/spec/omemo.js +++ b/spec/omemo.js @@ -88,9 +88,9 @@ .then((v) => { view = v; return view.model.encryptMessage(message); - }).then((payload) => { + }).then(payload => { return view.model.decryptMessage(payload); - }).then((result) => { + }).then(result => { expect(result).toBe(message); done(); }); @@ -199,10 +199,13 @@ // Test reception of an encrypted message return view.model.encryptMessage('This is an encrypted message from the contact') - }).then((obj) => { + }).then(obj => { // XXX: Normally the key will be encrypted via libsignal. // However, we're mocking libsignal in the tests, so we include // it as plaintext in the message. + + // u.stringToArrayBuffer(atob(u.arrayBufferToBase64(obj.key_and_tag))); + // u.stringToArrayBuffer(u.arrayBufferToString(obj.key_and_tag)); const stanza = $msg({ 'from': contact_jid, 'to': _converse.connection.jid, @@ -211,7 +214,7 @@ }).c('body').t('This is a fallback message').up() .c('encrypted', {'xmlns': Strophe.NS.OMEMO}) .c('header', {'sid': '555'}) - .c('key', {'rid': _converse.omemo_store.get('device_id')}).t(btoa(obj.key_and_tag)).up() + .c('key', {'rid': _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up() .c('iv').t(obj.iv) .up().up() .c('payload').t(obj.payload); @@ -255,7 +258,7 @@ .c('key', { 'prekey': 'true', 'rid': _converse.omemo_store.get('device_id') - }).t(btoa(obj.key_and_tag)).up() + }).t(u.arrayBufferToBase64(obj.key_and_tag)).up() .c('iv').t(obj.iv) .up().up() .c('payload').t(obj.payload); diff --git a/src/converse-omemo.js b/src/converse-omemo.js index 02bde2b52..dba18eebe 100644 --- a/src/converse-omemo.js +++ b/src/converse-omemo.js @@ -201,24 +201,9 @@ }); }, - getKeyAndTag (string) { - return { - 'key': string.slice(0, 22), - 'tag': string.slice(22) - } - }, - decryptMessage (obj) { - const { _converse } = this.__super__, - key_obj = { - "alg": "A128GCM", - "ext": true, - "k": obj.key, - "key_ops": ["encrypt","decrypt"], - "kty": "oct" - }; - return crypto.subtle.importKey('jwk', key_obj, KEY_ALGO, true, ['encrypt','decrypt']) - .then((key_obj) => { + return crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt','decrypt']) + .then(key_obj => { const algo = { 'name': "AES-GCM", 'iv': u.base64ToArrayBuffer(obj.iv), @@ -240,8 +225,7 @@ decrypt (attrs) { const { _converse } = this.__super__, - address = new libsignal.SignalProtocolAddress(attrs.from, parseInt(attrs.encrypted.device_id, 10)), - session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); + session_cipher = this.getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10)); // https://xmpp.org/extensions/xep-0384.html#usecases-receiving if (attrs.encrypted.prekey === 'true') { @@ -249,8 +233,9 @@ return session_cipher.decryptPreKeyWhisperMessage(u.base64ToArrayBuffer(attrs.encrypted.key), 'binary') .then(key_and_tag => { if (attrs.encrypted.payload) { - const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag)); - return this.decryptMessage(_.extend(attrs.encrypted, {'key': aes_data.key, 'tag': aes_data.tag})); + const key = key_and_tag.slice(0, 16), + tag = key_and_tag.slice(16); + return this.decryptMessage(_.extend(attrs.encrypted, {'key': key, 'tag': tag})); } return Promise.resolve(); }).then(pt => { @@ -270,8 +255,9 @@ } else { return session_cipher.decryptWhisperMessage(u.base64ToArrayBuffer(attrs.encrypted.key), 'binary') .then(key_and_tag => { - const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag)); - return this.decryptMessage(_.extend(attrs.encrypted, {'key': aes_data.key, 'tag': aes_data.tag})); + const key = key_and_tag.slice(0, 16), + tag = key_and_tag.slice(16); + return this.decryptMessage(_.extend(attrs.encrypted, {'key': key, 'tag': tag})); }).then(plaintext => _.extend(attrs, {'plaintext': plaintext})) .catch(e => { this.reportDecryptionError(e); @@ -332,13 +318,12 @@ } return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext)); }).then(ciphertext => { - return window.crypto.subtle.exportKey("jwk", key) - .then(key_obj => { - const tag = u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3))); + return window.crypto.subtle.exportKey("raw", key) + .then(key => { + const tag = ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3)); return Promise.resolve({ - 'key': key_obj.k, - 'tag': tag, - 'key_and_tag': key_obj.k + tag, + 'key': key, + 'key_and_tag': u.appendArrayBuffer(key, tag), 'payload': u.arrayBufferToBase64(ciphertext), 'iv': u.arrayBufferToBase64(iv) }); @@ -346,12 +331,19 @@ }); }, - encryptKey (plaintext, device) { - const { _converse } = this.__super__, - address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')), - session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); + getSessionCipher (jid, id) { + if (!this.session_cipher) { + const { _converse } = this.__super__, + address = new libsignal.SignalProtocolAddress(jid, id); + this.session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); + } + return this.session_cipher; + }, - return session_cipher.encrypt(plaintext).then(payload => ({'payload': payload, 'device': device})); + encryptKey (plaintext, device) { + return this.getSessionCipher(device.get('jid'), device.get('id')) + .encrypt(plaintext) + .then(payload => ({'payload': payload, 'device': device})); }, addKeysToMessageStanza (stanza, dicts, iv) { diff --git a/src/utils/core.js b/src/utils/core.js index 1b29b41ff..b56297207 100644 --- a/src/utils/core.js +++ b/src/utils/core.js @@ -916,25 +916,36 @@ return fp; }; + u.appendArrayBuffer = function (buffer1, buffer2) { + const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); + tmp.set(new Uint8Array(buffer1), 0); + tmp.set(new Uint8Array(buffer2), buffer1.byteLength); + return tmp.buffer; + }; + u.arrayBufferToHex = function (ab) { // https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex#40031979 return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join(''); }; u.arrayBufferToString = function (ab) { - const enc = new TextDecoder("utf-8"); - return enc.decode(ab); + return (new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), ''); + }; + + u.stringToArrayBuffer = function (string) { + const len = string.length, + bytes = new Uint8Array(len); + + for (let i = 0; i < len; i++) { + bytes[i] = string.charCodeAt(i) + } + return bytes.buffer }; u.arrayBufferToBase64 = function (ab) { return btoa((new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), '')); }; - u.stringToArrayBuffer = function (string) { - const enc = new TextEncoder(); // always utf-8 - return enc.encode(string); - }; - u.base64ToArrayBuffer = function (b64) { const binary_string = window.atob(b64), len = binary_string.length, diff --git a/tests/mock.js b/tests/mock.js index 7b710c25b..ac7f25158 100644 --- a/tests/mock.js +++ b/tests/mock.js @@ -22,7 +22,6 @@ 'registrationId': '1337' }); this.decryptPreKeyWhisperMessage = (key_and_tag) => { - // TODO: remove the prekey return Promise.resolve(u.stringToArrayBuffer(key_and_tag)); };