Working example of AES-GCM encryption and decryption

with key import and export.

updates #497
This commit is contained in:
JC Brand 2018-08-04 20:20:05 +02:00
parent f2c283c907
commit 713f49453f
4 changed files with 72 additions and 67 deletions

View File

@ -9,6 +9,7 @@
"plugins": ["lodash"],
"extends": ["eslint:recommended", "plugin:lodash/canonical"],
"globals": {
"Uint8Array": true,
"Promise": true,
"converse": true,
"define": true,

53
dist/converse.js vendored
View File

@ -73292,6 +73292,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
const UNDECIDED = 0;
const TRUSTED = 1;
const UNTRUSTED = -1;
const TAG_LENGTH = 128;
const KEY_ALGO = {
'name': "AES-GCM",
'length': 256
};
function parseBundle(bundle_el) {
/* Given an XML element representing a user's OMEMO bundle, parse it
@ -73396,25 +73401,23 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
};
},
decryptMessage(key_and_tag, attrs) {
const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
const CryptoKeyObject = {
decryptMessage(obj) {
const _converse = this.__super__._converse,
key_obj = {
"alg": "A256GCM",
"ext": true,
"k": aes_data.key,
"k": obj.key,
"key_ops": ["encrypt", "decrypt"],
"kty": "oct"
};
return crypto.subtle.importKey('jwk', CryptoKeyObject, 'AES-GCM', true, ['encrypt', 'decrypt']).then(key_obj => {
return window.crypto.subtle.decrypt({
return crypto.subtle.importKey('jwk', key_obj, KEY_ALGO, true, ['encrypt', 'decrypt']).then(key_obj => {
const algo = {
'name': "AES-GCM",
'iv': u.base64ToArrayBuffer(attrs.iv),
'tagLength': 128
}, key_obj, u.stringToArrayBuffer(attrs.payload));
}).then(out => {
const decoder = new TextDecoder();
return decoder.decode(out);
});
'iv': u.base64ToArrayBuffer(obj.iv),
'tagLength': TAG_LENGTH
};
return window.crypto.subtle.decrypt(algo, key_obj, u.base64ToArrayBuffer(obj.payload));
}).then(out => new TextDecoder().decode(out)).catch(e => _converse.log(e.toString(), Strophe.LogLevel.ERROR));
},
decrypt(attrs) {
@ -73455,7 +73458,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
const address = new libsignal.SignalProtocolAddress(attrs.from, attrs.encrypted.device_id),
session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address),
libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
session_cipher.decryptPreKeyWhisperMessage(libsignal_payload.body, 'binary').then(key_and_tag => this.decryptMessage(key_and_tag, attrs.encrypted)).then(f => {
session_cipher.decryptPreKeyWhisperMessage(libsignal_payload.body, 'binary').then(key_and_tag => this.decryptMessage(attrs.encrypted)).then(f => {
// TODO handle new key...
// _converse.omemo.publishBundle()
resolve(f);
@ -73490,13 +73493,9 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
encryptMessage(plaintext) {
// The client MUST use fresh, randomly generated key/IV pairs
// with AES-128 in Galois/Counter Mode (GCM).
const TAG_LENGTH = 128,
iv = window.crypto.getRandomValues(new window.Uint8Array(16));
const iv = window.crypto.getRandomValues(new window.Uint8Array(16));
let key;
return window.crypto.subtle.generateKey({
'name': "AES-GCM",
'length': 256
}, true, // extractable
return window.crypto.subtle.generateKey(KEY_ALGO, true, // extractable
["encrypt", "decrypt"] // key usages
).then(result => {
key = result;
@ -73509,10 +73508,10 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}).then(ciphertext => {
return window.crypto.subtle.exportKey("jwk", key).then(key_obj => {
return Promise.resolve({
'key_str': key_obj.k,
'tag': btoa(ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3))),
'ciphertext': btoa(ciphertext),
'iv': btoa(iv)
'key': key_obj.k,
'tag': u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3))),
'payload': u.arrayBufferToBase64(ciphertext),
'iv': u.arrayBufferToBase64(iv)
});
});
});
@ -73579,15 +73578,15 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}).c('header', {
'sid': _converse.omemo_store.get('device_id')
});
return this.encryptMessage(message).then(payload => {
return this.encryptMessage(message).then(obj => {
// The 16 bytes key and the GCM authentication tag (The tag
// SHOULD have at least 128 bit) are concatenated and for each
// intended recipient device, i.e. both own devices as well as
// devices associated with the contact, the result of this
// concatenation is encrypted using the corresponding
// long-standing SignalProtocol session.
const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => this.encryptKey(payload.key_str + payload.tag, device));
return Promise.all(promises).then(dicts => this.addKeysToMessageStanza(stanza, dicts, payload.iv)).then(stanza => stanza.c('payload').t(payload.ciphertext)).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => this.encryptKey(obj.key + obj.tag, device));
return Promise.all(promises).then(dicts => this.addKeysToMessageStanza(stanza, dicts, obj.iv)).then(stanza => stanza.c('payload').t(obj.payload)).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
});
},

View File

@ -15,16 +15,21 @@
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
function (done, _converse) {
let iq_stanza, view, sent_stanza;
const message = 'This message will be encrypted'
let view;
test_utils.createContacts(_converse, 'current', 1);
_converse.emit('rosterContactsFetched');
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
test_utils.openChatBoxFor(_converse, contact_jid)
.then((view) => view.model.encryptMessage('This message will be encrypted'))
.then((payload) => {
debugger;
.then((v) => {
view = v;
return view.model.encryptMessage(message);
}).then((payload) => {
return view.model.decryptMessage(payload);
}).then(done);
}).then((result) => {
expect(result).toBe(message);
done();
});
}));

View File

@ -25,6 +25,11 @@
const UNDECIDED = 0;
const TRUSTED = 1;
const UNTRUSTED = -1;
const TAG_LENGTH = 128;
const KEY_ALGO = {
'name': "AES-GCM",
'length': 256
};
function parseBundle (bundle_el) {
@ -135,26 +140,25 @@
}
},
decryptMessage (key_and_tag, attrs) {
const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
const CryptoKeyObject = {
"alg": "A256GCM",
"ext": true,
"k": aes_data.key,
"key_ops": ["encrypt","decrypt"],
"kty": "oct"
}
return crypto.subtle.importKey('jwk', CryptoKeyObject, 'AES-GCM', true, ['encrypt','decrypt'])
decryptMessage (obj) {
const { _converse } = this.__super__,
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) => {
return window.crypto.subtle.decrypt(
{'name': "AES-GCM", 'iv': u.base64ToArrayBuffer(attrs.iv), 'tagLength': 128},
key_obj,
u.stringToArrayBuffer(attrs.payload)
);
}).then((out) => {
const decoder = new TextDecoder()
return decoder.decode(out)
})
const algo = {
'name': "AES-GCM",
'iv': u.base64ToArrayBuffer(obj.iv),
'tagLength': TAG_LENGTH
}
return window.crypto.subtle.decrypt(algo, key_obj, u.base64ToArrayBuffer(obj.payload));
}).then(out => (new TextDecoder()).decode(out))
.catch(e => _converse.log(e.toString(), Strophe.LogLevel.ERROR));
},
decrypt (attrs) {
@ -200,7 +204,7 @@
libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
session_cipher.decryptPreKeyWhisperMessage(libsignal_payload.body, 'binary')
.then(key_and_tag => this.decryptMessage(key_and_tag, attrs.encrypted))
.then(key_and_tag => this.decryptMessage(attrs.encrypted))
.then((f) => {
// TODO handle new key...
// _converse.omemo.publishBundle()
@ -236,14 +240,10 @@
encryptMessage (plaintext) {
// The client MUST use fresh, randomly generated key/IV pairs
// with AES-128 in Galois/Counter Mode (GCM).
const TAG_LENGTH = 128,
iv = window.crypto.getRandomValues(new window.Uint8Array(16));
const iv = window.crypto.getRandomValues(new window.Uint8Array(16));
let key;
return window.crypto.subtle.generateKey({
'name': "AES-GCM",
'length': 256
},
return window.crypto.subtle.generateKey(
KEY_ALGO,
true, // extractable
["encrypt", "decrypt"] // key usages
).then((result) => {
@ -258,10 +258,10 @@
return window.crypto.subtle.exportKey("jwk", key)
.then((key_obj) => {
return Promise.resolve({
'key_str': key_obj.k,
'tag': btoa(ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3))),
'ciphertext': btoa(ciphertext),
'iv': btoa(iv)
'key': key_obj.k,
'tag': u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3))),
'payload': u.arrayBufferToBase64(ciphertext),
'iv': u.arrayBufferToBase64(iv)
});
});
});
@ -319,7 +319,7 @@
.c('encrypted', {'xmlns': Strophe.NS.OMEMO})
.c('header', {'sid': _converse.omemo_store.get('device_id')});
return this.encryptMessage(message).then((payload) => {
return this.encryptMessage(message).then((obj) => {
// The 16 bytes key and the GCM authentication tag (The tag
// SHOULD have at least 128 bit) are concatenated and for each
// intended recipient device, i.e. both own devices as well as
@ -328,11 +328,11 @@
// long-standing SignalProtocol session.
const promises = devices
.filter(device => device.get('trusted') != UNTRUSTED)
.map(device => this.encryptKey(payload.key_str+payload.tag, device));
.map(device => this.encryptKey(obj.key+obj.tag, device));
return Promise.all(promises)
.then((dicts) => this.addKeysToMessageStanza(stanza, dicts, payload.iv))
.then((stanza) => stanza.c('payload').t(payload.ciphertext))
.then((dicts) => this.addKeysToMessageStanza(stanza, dicts, obj.iv))
.then((stanza) => stanza.c('payload').t(obj.payload))
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
});
},