Working example of AES-GCM encryption and decryption
with key import and export. updates #497
This commit is contained in:
parent
f2c283c907
commit
713f49453f
@ -9,6 +9,7 @@
|
|||||||
"plugins": ["lodash"],
|
"plugins": ["lodash"],
|
||||||
"extends": ["eslint:recommended", "plugin:lodash/canonical"],
|
"extends": ["eslint:recommended", "plugin:lodash/canonical"],
|
||||||
"globals": {
|
"globals": {
|
||||||
|
"Uint8Array": true,
|
||||||
"Promise": true,
|
"Promise": true,
|
||||||
"converse": true,
|
"converse": true,
|
||||||
"define": true,
|
"define": true,
|
||||||
|
53
dist/converse.js
vendored
53
dist/converse.js
vendored
@ -73292,6 +73292,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
|
|||||||
const UNDECIDED = 0;
|
const UNDECIDED = 0;
|
||||||
const TRUSTED = 1;
|
const TRUSTED = 1;
|
||||||
const UNTRUSTED = -1;
|
const UNTRUSTED = -1;
|
||||||
|
const TAG_LENGTH = 128;
|
||||||
|
const KEY_ALGO = {
|
||||||
|
'name': "AES-GCM",
|
||||||
|
'length': 256
|
||||||
|
};
|
||||||
|
|
||||||
function parseBundle(bundle_el) {
|
function parseBundle(bundle_el) {
|
||||||
/* Given an XML element representing a user's OMEMO bundle, parse it
|
/* 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) {
|
decryptMessage(obj) {
|
||||||
const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
|
const _converse = this.__super__._converse,
|
||||||
const CryptoKeyObject = {
|
key_obj = {
|
||||||
"alg": "A256GCM",
|
"alg": "A256GCM",
|
||||||
"ext": true,
|
"ext": true,
|
||||||
"k": aes_data.key,
|
"k": obj.key,
|
||||||
"key_ops": ["encrypt", "decrypt"],
|
"key_ops": ["encrypt", "decrypt"],
|
||||||
"kty": "oct"
|
"kty": "oct"
|
||||||
};
|
};
|
||||||
return crypto.subtle.importKey('jwk', CryptoKeyObject, 'AES-GCM', true, ['encrypt', 'decrypt']).then(key_obj => {
|
return crypto.subtle.importKey('jwk', key_obj, KEY_ALGO, true, ['encrypt', 'decrypt']).then(key_obj => {
|
||||||
return window.crypto.subtle.decrypt({
|
const algo = {
|
||||||
'name': "AES-GCM",
|
'name': "AES-GCM",
|
||||||
'iv': u.base64ToArrayBuffer(attrs.iv),
|
'iv': u.base64ToArrayBuffer(obj.iv),
|
||||||
'tagLength': 128
|
'tagLength': TAG_LENGTH
|
||||||
}, key_obj, u.stringToArrayBuffer(attrs.payload));
|
};
|
||||||
}).then(out => {
|
return window.crypto.subtle.decrypt(algo, key_obj, u.base64ToArrayBuffer(obj.payload));
|
||||||
const decoder = new TextDecoder();
|
}).then(out => new TextDecoder().decode(out)).catch(e => _converse.log(e.toString(), Strophe.LogLevel.ERROR));
|
||||||
return decoder.decode(out);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
decrypt(attrs) {
|
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),
|
const address = new libsignal.SignalProtocolAddress(attrs.from, attrs.encrypted.device_id),
|
||||||
session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address),
|
session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address),
|
||||||
libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
|
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...
|
// TODO handle new key...
|
||||||
// _converse.omemo.publishBundle()
|
// _converse.omemo.publishBundle()
|
||||||
resolve(f);
|
resolve(f);
|
||||||
@ -73490,13 +73493,9 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
|
|||||||
encryptMessage(plaintext) {
|
encryptMessage(plaintext) {
|
||||||
// The client MUST use fresh, randomly generated key/IV pairs
|
// The client MUST use fresh, randomly generated key/IV pairs
|
||||||
// with AES-128 in Galois/Counter Mode (GCM).
|
// with AES-128 in Galois/Counter Mode (GCM).
|
||||||
const TAG_LENGTH = 128,
|
const iv = window.crypto.getRandomValues(new window.Uint8Array(16));
|
||||||
iv = window.crypto.getRandomValues(new window.Uint8Array(16));
|
|
||||||
let key;
|
let key;
|
||||||
return window.crypto.subtle.generateKey({
|
return window.crypto.subtle.generateKey(KEY_ALGO, true, // extractable
|
||||||
'name': "AES-GCM",
|
|
||||||
'length': 256
|
|
||||||
}, true, // extractable
|
|
||||||
["encrypt", "decrypt"] // key usages
|
["encrypt", "decrypt"] // key usages
|
||||||
).then(result => {
|
).then(result => {
|
||||||
key = result;
|
key = result;
|
||||||
@ -73509,10 +73508,10 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
|
|||||||
}).then(ciphertext => {
|
}).then(ciphertext => {
|
||||||
return window.crypto.subtle.exportKey("jwk", key).then(key_obj => {
|
return window.crypto.subtle.exportKey("jwk", key).then(key_obj => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
'key_str': key_obj.k,
|
'key': key_obj.k,
|
||||||
'tag': btoa(ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3))),
|
'tag': u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3))),
|
||||||
'ciphertext': btoa(ciphertext),
|
'payload': u.arrayBufferToBase64(ciphertext),
|
||||||
'iv': btoa(iv)
|
'iv': u.arrayBufferToBase64(iv)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -73579,15 +73578,15 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
|
|||||||
}).c('header', {
|
}).c('header', {
|
||||||
'sid': _converse.omemo_store.get('device_id')
|
'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
|
// The 16 bytes key and the GCM authentication tag (The tag
|
||||||
// SHOULD have at least 128 bit) are concatenated and for each
|
// SHOULD have at least 128 bit) are concatenated and for each
|
||||||
// intended recipient device, i.e. both own devices as well as
|
// intended recipient device, i.e. both own devices as well as
|
||||||
// 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(payload.key_str + payload.tag, device));
|
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, payload.iv)).then(stanza => stanza.c('payload').t(payload.ciphertext)).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
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));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -15,16 +15,21 @@
|
|||||||
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||||
function (done, _converse) {
|
function (done, _converse) {
|
||||||
|
|
||||||
let iq_stanza, view, sent_stanza;
|
const message = 'This message will be encrypted'
|
||||||
|
let view;
|
||||||
test_utils.createContacts(_converse, 'current', 1);
|
test_utils.createContacts(_converse, 'current', 1);
|
||||||
_converse.emit('rosterContactsFetched');
|
_converse.emit('rosterContactsFetched');
|
||||||
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
|
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
|
||||||
test_utils.openChatBoxFor(_converse, contact_jid)
|
test_utils.openChatBoxFor(_converse, contact_jid)
|
||||||
.then((view) => view.model.encryptMessage('This message will be encrypted'))
|
.then((v) => {
|
||||||
.then((payload) => {
|
view = v;
|
||||||
debugger;
|
return view.model.encryptMessage(message);
|
||||||
|
}).then((payload) => {
|
||||||
return view.model.decryptMessage(payload);
|
return view.model.decryptMessage(payload);
|
||||||
}).then(done);
|
}).then((result) => {
|
||||||
|
expect(result).toBe(message);
|
||||||
|
done();
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,6 +25,11 @@
|
|||||||
const UNDECIDED = 0;
|
const UNDECIDED = 0;
|
||||||
const TRUSTED = 1;
|
const TRUSTED = 1;
|
||||||
const UNTRUSTED = -1;
|
const UNTRUSTED = -1;
|
||||||
|
const TAG_LENGTH = 128;
|
||||||
|
const KEY_ALGO = {
|
||||||
|
'name': "AES-GCM",
|
||||||
|
'length': 256
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
function parseBundle (bundle_el) {
|
function parseBundle (bundle_el) {
|
||||||
@ -135,26 +140,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
decryptMessage (key_and_tag, attrs) {
|
decryptMessage (obj) {
|
||||||
const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
|
const { _converse } = this.__super__,
|
||||||
const CryptoKeyObject = {
|
key_obj = {
|
||||||
"alg": "A256GCM",
|
"alg": "A256GCM",
|
||||||
"ext": true,
|
"ext": true,
|
||||||
"k": aes_data.key,
|
"k": obj.key,
|
||||||
"key_ops": ["encrypt","decrypt"],
|
"key_ops": ["encrypt","decrypt"],
|
||||||
"kty": "oct"
|
"kty": "oct"
|
||||||
}
|
};
|
||||||
return crypto.subtle.importKey('jwk', CryptoKeyObject, 'AES-GCM', true, ['encrypt','decrypt'])
|
return crypto.subtle.importKey('jwk', key_obj, KEY_ALGO, true, ['encrypt','decrypt'])
|
||||||
.then((key_obj) => {
|
.then((key_obj) => {
|
||||||
return window.crypto.subtle.decrypt(
|
const algo = {
|
||||||
{'name': "AES-GCM", 'iv': u.base64ToArrayBuffer(attrs.iv), 'tagLength': 128},
|
'name': "AES-GCM",
|
||||||
key_obj,
|
'iv': u.base64ToArrayBuffer(obj.iv),
|
||||||
u.stringToArrayBuffer(attrs.payload)
|
'tagLength': TAG_LENGTH
|
||||||
);
|
}
|
||||||
}).then((out) => {
|
return window.crypto.subtle.decrypt(algo, key_obj, u.base64ToArrayBuffer(obj.payload));
|
||||||
const decoder = new TextDecoder()
|
}).then(out => (new TextDecoder()).decode(out))
|
||||||
return decoder.decode(out)
|
.catch(e => _converse.log(e.toString(), Strophe.LogLevel.ERROR));
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
decrypt (attrs) {
|
decrypt (attrs) {
|
||||||
@ -200,7 +204,7 @@
|
|||||||
libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
|
libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
|
||||||
|
|
||||||
session_cipher.decryptPreKeyWhisperMessage(libsignal_payload.body, 'binary')
|
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) => {
|
.then((f) => {
|
||||||
// TODO handle new key...
|
// TODO handle new key...
|
||||||
// _converse.omemo.publishBundle()
|
// _converse.omemo.publishBundle()
|
||||||
@ -236,14 +240,10 @@
|
|||||||
encryptMessage (plaintext) {
|
encryptMessage (plaintext) {
|
||||||
// The client MUST use fresh, randomly generated key/IV pairs
|
// The client MUST use fresh, randomly generated key/IV pairs
|
||||||
// with AES-128 in Galois/Counter Mode (GCM).
|
// with AES-128 in Galois/Counter Mode (GCM).
|
||||||
const TAG_LENGTH = 128,
|
const iv = window.crypto.getRandomValues(new window.Uint8Array(16));
|
||||||
iv = window.crypto.getRandomValues(new window.Uint8Array(16));
|
|
||||||
|
|
||||||
let key;
|
let key;
|
||||||
return window.crypto.subtle.generateKey({
|
return window.crypto.subtle.generateKey(
|
||||||
'name': "AES-GCM",
|
KEY_ALGO,
|
||||||
'length': 256
|
|
||||||
},
|
|
||||||
true, // extractable
|
true, // extractable
|
||||||
["encrypt", "decrypt"] // key usages
|
["encrypt", "decrypt"] // key usages
|
||||||
).then((result) => {
|
).then((result) => {
|
||||||
@ -258,10 +258,10 @@
|
|||||||
return window.crypto.subtle.exportKey("jwk", key)
|
return window.crypto.subtle.exportKey("jwk", key)
|
||||||
.then((key_obj) => {
|
.then((key_obj) => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
'key_str': key_obj.k,
|
'key': key_obj.k,
|
||||||
'tag': btoa(ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3))),
|
'tag': u.arrayBufferToBase64(ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3))),
|
||||||
'ciphertext': btoa(ciphertext),
|
'payload': u.arrayBufferToBase64(ciphertext),
|
||||||
'iv': btoa(iv)
|
'iv': u.arrayBufferToBase64(iv)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -319,7 +319,7 @@
|
|||||||
.c('encrypted', {'xmlns': Strophe.NS.OMEMO})
|
.c('encrypted', {'xmlns': Strophe.NS.OMEMO})
|
||||||
.c('header', {'sid': _converse.omemo_store.get('device_id')});
|
.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
|
// The 16 bytes key and the GCM authentication tag (The tag
|
||||||
// SHOULD have at least 128 bit) are concatenated and for each
|
// SHOULD have at least 128 bit) are concatenated and for each
|
||||||
// intended recipient device, i.e. both own devices as well as
|
// intended recipient device, i.e. both own devices as well as
|
||||||
@ -328,11 +328,11 @@
|
|||||||
// 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(payload.key_str+payload.tag, device));
|
.map(device => this.encryptKey(obj.key+obj.tag, device));
|
||||||
|
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
.then((dicts) => this.addKeysToMessageStanza(stanza, dicts, payload.iv))
|
.then((dicts) => this.addKeysToMessageStanza(stanza, dicts, obj.iv))
|
||||||
.then((stanza) => stanza.c('payload').t(payload.ciphertext))
|
.then((stanza) => stanza.c('payload').t(obj.payload))
|
||||||
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user