Use raw key format and not jwk. Set correct key size.

This commit is contained in:
JC Brand 2018-08-31 17:15:03 +02:00
parent ed9afe9afb
commit 6a061b4525
5 changed files with 100 additions and 94 deletions

91
dist/converse.js vendored
View File

@ -71600,7 +71600,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
const TAG_LENGTH = 128; const TAG_LENGTH = 128;
const KEY_ALGO = { const KEY_ALGO = {
'name': "AES-GCM", 'name': "AES-GCM",
'length': 256 'length': 128
}; };
function parseBundle(bundle_el) { function parseBundle(bundle_el) {
@ -71769,25 +71769,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}); });
}, },
getKeyAndTag(string) {
return {
'key': string.slice(0, 43),
// 256bit key
'tag': string.slice(43, string.length) // rest is tag
};
},
decryptMessage(obj) { decryptMessage(obj) {
const _converse = this.__super__._converse, return crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt', 'decrypt']).then(key_obj => {
key_obj = {
"alg": "A256GCM",
"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 => {
const algo = { const algo = {
'name': "AES-GCM", 'name': "AES-GCM",
'iv': u.base64ToArrayBuffer(obj.iv), 'iv': u.base64ToArrayBuffer(obj.iv),
@ -71810,17 +71793,17 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
decrypt(attrs) { decrypt(attrs) {
const _converse = this.__super__._converse, const _converse = this.__super__._converse,
address = new libsignal.SignalProtocolAddress(attrs.from, parseInt(attrs.encrypted.device_id, 10)), session_cipher = this.getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10)); // https://xmpp.org/extensions/xep-0384.html#usecases-receiving
session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); // https://xmpp.org/extensions/xep-0384.html#usecases-receiving
if (attrs.encrypted.prekey === 'true') { if (attrs.encrypted.prekey === 'true') {
let plaintext; let plaintext;
return session_cipher.decryptPreKeyWhisperMessage(atob(attrs.encrypted.key), 'binary').then(key_and_tag => { return session_cipher.decryptPreKeyWhisperMessage(atob(attrs.encrypted.key), 'binary').then(key_and_tag => {
if (attrs.encrypted.payload) { 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, { return this.decryptMessage(_.extend(attrs.encrypted, {
'key': aes_data.key, 'key': key,
'tag': aes_data.tag 'tag': tag
})); }));
} }
@ -71844,10 +71827,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}); });
} else { } else {
return session_cipher.decryptWhisperMessage(atob(attrs.encrypted.key), 'binary').then(key_and_tag => { return session_cipher.decryptWhisperMessage(atob(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, { return this.decryptMessage(_.extend(attrs.encrypted, {
'key': aes_data.key, 'key': key,
'tag': aes_data.tag 'tag': tag
})); }));
}).then(plaintext => _.extend(attrs, { }).then(plaintext => _.extend(attrs, {
'plaintext': plaintext 'plaintext': plaintext
@ -71911,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)); return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext));
}).then(ciphertext => { }).then(ciphertext => {
return window.crypto.subtle.exportKey("jwk", key).then(key_obj => { return window.crypto.subtle.exportKey("raw", key).then(key => {
const tag = u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3))); const tag = ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3));
return Promise.resolve({ return Promise.resolve({
'key': key_obj.k, 'key': key,
'tag': tag, 'key_and_tag': u.appendArrayBuffer(key, tag),
'key_and_tag': key_obj.k + tag,
'payload': u.arrayBufferToBase64(ciphertext), 'payload': u.arrayBufferToBase64(ciphertext),
'iv': u.arrayBufferToBase64(iv) 'iv': u.arrayBufferToBase64(iv)
}); });
@ -71924,11 +71907,18 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}); });
}, },
encryptKey(plaintext, device) { getSessionCipher(jid, id) {
if (!this.session_cipher) {
const _converse = this.__super__._converse, const _converse = this.__super__._converse,
address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')), address = new libsignal.SignalProtocolAddress(jid, id);
session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); this.session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address);
return session_cipher.encrypt(plaintext).then(payload => ({ }
return this.session_cipher;
},
encryptKey(plaintext, device) {
return this.getSessionCipher(device.get('jid'), device.get('id')).encrypt(plaintext).then(payload => ({
'payload': payload, 'payload': payload,
'device': device 'device': device
})); }));
@ -71994,7 +71984,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
// devices associated with the contact, the result of this // devices associated with the contact, the result of this
// concatenation is encrypted using the corresponding // concatenation is encrypted using the corresponding
// long-standing SignalProtocol session. // long-standing SignalProtocol session.
const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => this.encryptKey(obj.key_and_tag, device)); const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => this.encryptKey(u.arrayBufferToString(obj.key_and_tag), device));
return Promise.all(promises).then(dicts => this.addKeysToMessageStanza(stanza, dicts, obj.iv)).then(stanza => { return Promise.all(promises).then(dicts => this.addKeysToMessageStanza(stanza, dicts, obj.iv)).then(stanza => {
stanza.c('payload').t(obj.payload).up().up(); stanza.c('payload').t(obj.payload).up().up();
stanza.c('store', { stanza.c('store', {
@ -81292,26 +81282,37 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
return fp; 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) { u.arrayBufferToHex = function (ab) {
// https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex#40031979 // 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(''); return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
}; };
u.arrayBufferToString = function (ab) { u.arrayBufferToString = function (ab) {
const enc = new TextDecoder("utf-8"); return new Uint8Array(ab).reduce((data, byte) => data + String.fromCharCode(byte), '');
return enc.decode(ab); };
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) { u.arrayBufferToBase64 = function (ab) {
return btoa(new Uint8Array(ab).reduce((data, byte) => data + String.fromCharCode(byte), '')); 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) { u.base64ToArrayBuffer = function (b64) {
const binary_string = window.atob(b64), const binary_string = window.atob(b64),
len = binary_string.length, len = binary_string.length,

View File

@ -88,9 +88,9 @@
.then((v) => { .then((v) => {
view = v; view = v;
return view.model.encryptMessage(message); return view.model.encryptMessage(message);
}).then((payload) => { }).then(payload => {
return view.model.decryptMessage(payload); return view.model.decryptMessage(payload);
}).then((result) => { }).then(result => {
expect(result).toBe(message); expect(result).toBe(message);
done(); done();
}); });
@ -199,10 +199,13 @@
// Test reception of an encrypted message // Test reception of an encrypted message
return view.model.encryptMessage('This is an encrypted message from the contact') 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. // XXX: Normally the key will be encrypted via libsignal.
// However, we're mocking libsignal in the tests, so we include // However, we're mocking libsignal in the tests, so we include
// it as plaintext in the message. // 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({ const stanza = $msg({
'from': contact_jid, 'from': contact_jid,
'to': _converse.connection.jid, 'to': _converse.connection.jid,
@ -211,7 +214,7 @@
}).c('body').t('This is a fallback message').up() }).c('body').t('This is a fallback message').up()
.c('encrypted', {'xmlns': Strophe.NS.OMEMO}) .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
.c('header', {'sid': '555'}) .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) .c('iv').t(obj.iv)
.up().up() .up().up()
.c('payload').t(obj.payload); .c('payload').t(obj.payload);
@ -255,7 +258,7 @@
.c('key', { .c('key', {
'prekey': 'true', 'prekey': 'true',
'rid': _converse.omemo_store.get('device_id') '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) .c('iv').t(obj.iv)
.up().up() .up().up()
.c('payload').t(obj.payload); .c('payload').t(obj.payload);

View File

@ -28,7 +28,7 @@
const TAG_LENGTH = 128; const TAG_LENGTH = 128;
const KEY_ALGO = { const KEY_ALGO = {
'name': "AES-GCM", 'name': "AES-GCM",
'length': 256 'length': 128
}; };
@ -201,24 +201,9 @@
}); });
}, },
getKeyAndTag (string) {
return {
'key': string.slice(0, 43), // 256bit key
'tag': string.slice(43, string.length) // rest is tag
}
},
decryptMessage (obj) { decryptMessage (obj) {
const { _converse } = this.__super__, return crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt','decrypt'])
key_obj = { .then(key_obj => {
"alg": "A256GCM",
"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) => {
const algo = { const algo = {
'name': "AES-GCM", 'name': "AES-GCM",
'iv': u.base64ToArrayBuffer(obj.iv), 'iv': u.base64ToArrayBuffer(obj.iv),
@ -240,8 +225,7 @@
decrypt (attrs) { decrypt (attrs) {
const { _converse } = this.__super__, const { _converse } = this.__super__,
address = new libsignal.SignalProtocolAddress(attrs.from, parseInt(attrs.encrypted.device_id, 10)), session_cipher = this.getSessionCipher(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 // https://xmpp.org/extensions/xep-0384.html#usecases-receiving
if (attrs.encrypted.prekey === 'true') { if (attrs.encrypted.prekey === 'true') {
@ -249,8 +233,9 @@
return session_cipher.decryptPreKeyWhisperMessage(atob(attrs.encrypted.key), 'binary') return session_cipher.decryptPreKeyWhisperMessage(atob(attrs.encrypted.key), 'binary')
.then(key_and_tag => { .then(key_and_tag => {
if (attrs.encrypted.payload) { if (attrs.encrypted.payload) {
const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag)); const key = key_and_tag.slice(0, 16),
return this.decryptMessage(_.extend(attrs.encrypted, {'key': aes_data.key, 'tag': aes_data.tag})); tag = key_and_tag.slice(16);
return this.decryptMessage(_.extend(attrs.encrypted, {'key': key, 'tag': tag}));
} }
return Promise.resolve(); return Promise.resolve();
}).then(pt => { }).then(pt => {
@ -270,8 +255,9 @@
} else { } else {
return session_cipher.decryptWhisperMessage(atob(attrs.encrypted.key), 'binary') return session_cipher.decryptWhisperMessage(atob(attrs.encrypted.key), 'binary')
.then(key_and_tag => { .then(key_and_tag => {
const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag)); const key = key_and_tag.slice(0, 16),
return this.decryptMessage(_.extend(attrs.encrypted, {'key': aes_data.key, 'tag': aes_data.tag})); tag = key_and_tag.slice(16);
return this.decryptMessage(_.extend(attrs.encrypted, {'key': key, 'tag': tag}));
}).then(plaintext => _.extend(attrs, {'plaintext': plaintext})) }).then(plaintext => _.extend(attrs, {'plaintext': plaintext}))
.catch(e => { .catch(e => {
this.reportDecryptionError(e); this.reportDecryptionError(e);
@ -332,13 +318,12 @@
} }
return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext)); return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext));
}).then(ciphertext => { }).then(ciphertext => {
return window.crypto.subtle.exportKey("jwk", key) return window.crypto.subtle.exportKey("raw", key)
.then(key_obj => { .then(key => {
const tag = u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3))); const tag = ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3));
return Promise.resolve({ return Promise.resolve({
'key': key_obj.k, 'key': key,
'tag': tag, 'key_and_tag': u.appendArrayBuffer(key, tag),
'key_and_tag': key_obj.k + tag,
'payload': u.arrayBufferToBase64(ciphertext), 'payload': u.arrayBufferToBase64(ciphertext),
'iv': u.arrayBufferToBase64(iv) 'iv': u.arrayBufferToBase64(iv)
}); });
@ -346,12 +331,19 @@
}); });
}, },
encryptKey (plaintext, device) { getSessionCipher (jid, id) {
if (!this.session_cipher) {
const { _converse } = this.__super__, const { _converse } = this.__super__,
address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')), address = new libsignal.SignalProtocolAddress(jid, id);
session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); 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) { addKeysToMessageStanza (stanza, dicts, iv) {
@ -406,7 +398,7 @@
// long-standing SignalProtocol session. // long-standing SignalProtocol session.
const promises = devices const promises = devices
.filter(device => device.get('trusted') != UNTRUSTED) .filter(device => device.get('trusted') != UNTRUSTED)
.map(device => this.encryptKey(obj.key_and_tag, device)); .map(device => this.encryptKey(u.arrayBufferToString(obj.key_and_tag), device));
return Promise.all(promises) return Promise.all(promises)
.then(dicts => this.addKeysToMessageStanza(stanza, dicts, obj.iv)) .then(dicts => this.addKeysToMessageStanza(stanza, dicts, obj.iv))

View File

@ -916,25 +916,36 @@
return fp; 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) { u.arrayBufferToHex = function (ab) {
// https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex#40031979 // 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(''); return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
}; };
u.arrayBufferToString = function (ab) { u.arrayBufferToString = function (ab) {
const enc = new TextDecoder("utf-8"); return (new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), '');
return enc.decode(ab); };
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) { u.arrayBufferToBase64 = function (ab) {
return btoa((new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), '')); 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) { u.base64ToArrayBuffer = function (b64) {
const binary_string = window.atob(b64), const binary_string = window.atob(b64),
len = binary_string.length, len = binary_string.length,

View File

@ -22,7 +22,6 @@
'registrationId': '1337' 'registrationId': '1337'
}); });
this.decryptPreKeyWhisperMessage = (key_and_tag) => { this.decryptPreKeyWhisperMessage = (key_and_tag) => {
// TODO: remove the prekey
return Promise.resolve(u.stringToArrayBuffer(key_and_tag)); return Promise.resolve(u.stringToArrayBuffer(key_and_tag));
}; };