diff --git a/lib/ake.js b/lib/ake.js index 9813468..c8a5d7d 100644 --- a/lib/ake.js +++ b/lib/ake.js @@ -81,25 +81,38 @@ }, verifySignMac: function (mac, aesctr, m2, c, their_y, our_dh_pk, m1, ctr) { - // verify mac - var vmac = HLP.makeMac(aesctr, m2) - if (!HLP.compare(mac, vmac)) - return ['MACs do not match.'] - - // decrypt x - var x = HLP.decryptAes(aesctr.substring(4), c, ctr) - x = HLP.splitype(['PUBKEY', 'INT', 'SIG'], x.toString(CryptoJS.enc.Latin1)) - - var m = hMac(their_y, our_dh_pk, x[0], x[1], m1) - var pub = DSA.parsePublic(x[0]) - - var r = HLP.bits2bigInt(x[2].substring(0, 20)) - var s = HLP.bits2bigInt(x[2].substring(20)) - - // verify sign m - if (!DSA.verify(pub, m, r, s)) return ['Cannot verify signature of m.'] + return new Promise( + function(resolve, reject) { + // verify mac + var vmac = HLP.makeMac(aesctr, m2) + if (!HLP.compare(mac, vmac)) { + resolve(['MACs do not match.']) + return + } - return [null, HLP.readLen(x[1]), pub] + // decrypt x + HLP.decryptAesAsync(aesctr.substring(4), c, ctr) + .then(function (x) { + x = HLP.splitype(['PUBKEY', 'INT', 'SIG'], x.toString(CryptoJS.enc.Latin1)) + + var m = hMac(their_y, our_dh_pk, x[0], x[1], m1) + var pub = DSA.parsePublic(x[0]) + + var r = HLP.bits2bigInt(x[2].substring(0, 20)) + var s = HLP.bits2bigInt(x[2].substring(20)) + + // verify sign m + if (!DSA.verify(pub, m, r, s)) { + resolve(['Cannot verify signature of m.']) + return + } + + resolve([null, HLP.readLen(x[1]), pub]) + }) + .catch(function (error) { + reject(error); + }) + }) }, makeM: function (their_y, m1, c, m2) { @@ -111,9 +124,19 @@ msg += BigInt.bigInt2bits(m[0], 20) // pad to 20 bytes msg += BigInt.bigInt2bits(m[1], 20) msg = CryptoJS.enc.Latin1.parse(msg) - var aesctr = HLP.packData(HLP.encryptAes(msg, c, HLP.packCtr(0))) - var mac = HLP.makeMac(aesctr, m2) - return aesctr + mac + + return new Promise( + function(resolve, reject) { + HLP.encryptAesAsync(msg, c, HLP.packCtr(0)) + .then(function (result) { + var aesctr = HLP.packData(result) + var mac = HLP.makeMac(aesctr, m2) + resolve(aesctr + mac) + }) + .catch(function (error) { + reject(error) + }) + }) }, akeSuccess: function (version) { @@ -233,12 +256,20 @@ type = '\x11' send = HLP.packMPI(this.r) - send += this.makeM(this.their_y, this.m1, this.c, this.m2) + this.makeM(this.their_y, this.m1, this.c, this.m2) + .then(function (result) { + send += result + + this.sendMsg(version, type, send) + }.bind(this)) + .catch(function (error) { + console.error('Error calling makeM: ' + error) + }) this.m1 = null this.m2 = null this.c = null - break + return case '\x11': HLP.debug.call(this.otr, 'signature message') @@ -254,55 +285,72 @@ var key = CryptoJS.enc.Hex.parse(BigInt.bigInt2str(this.r, 16)) key = CryptoJS.enc.Latin1.stringify(key) - var gxmpi = HLP.decryptAes(this.encrypted, key, HLP.packCtr(0)) - gxmpi = gxmpi.toString(CryptoJS.enc.Latin1) - - this.their_y = HLP.readMPI(gxmpi) - - // verify hash - var hash = CryptoJS.SHA256(CryptoJS.enc.Latin1.parse(gxmpi)) - - if (!HLP.compare(this.hashed, hash.toString(CryptoJS.enc.Latin1))) - return this.otr.error('Hashed g^x does not match.', true) - - // verify gx is legal 2 <= g^x <= N-2 - if (!HLP.checkGroup(this.their_y, N_MINUS_2)) - return this.otr.error('Illegal g^x.', true) - - this.createKeys(this.their_y) - - vsm = this.verifySignMac( - msg[2] - , msg[1] - , this.m2 - , this.c - , this.their_y - , this.our_dh.publicKey - , this.m1 - , HLP.packCtr(0) - ) - if (vsm[0]) return this.otr.error(vsm[0], true) - - // store their key - this.their_keyid = vsm[1] - this.their_priv_pk = vsm[2] - - send = this.makeM( - this.their_y - , this.m1_prime - , this.c_prime - , this.m2_prime - ) - - this.m1 = null - this.m2 = null - this.m1_prime = null - this.m2_prime = null - this.c = null - this.c_prime = null - - this.sendMsg(version, '\x12', send) - this.akeSuccess(version) + HLP.decryptAesAsync(this.encrypted, key, HLP.packCtr(0)) + .then(function (gxmpi) { + gxmpi = gxmpi.toString(CryptoJS.enc.Latin1) + + this.their_y = HLP.readMPI(gxmpi) + + // verify hash + var hash = CryptoJS.SHA256(CryptoJS.enc.Latin1.parse(gxmpi)) + + if (!HLP.compare(this.hashed, hash.toString(CryptoJS.enc.Latin1))) + return this.otr.error('Hashed g^x does not match.', true) + + // verify gx is legal 2 <= g^x <= N-2 + if (!HLP.checkGroup(this.their_y, N_MINUS_2)) + return this.otr.error('Illegal g^x.', true) + + this.createKeys(this.their_y) + + this.verifySignMac( + msg[2] + , msg[1] + , this.m2 + , this.c + , this.their_y + , this.our_dh.publicKey + , this.m1 + , HLP.packCtr(0) + ).then(function (vsm) { + if (vsm[0]) { + console.error("OTR error: " + vsm[0]); + this.otr.error(vsm[0], true) + return + } + + // store their key + this.their_keyid = vsm[1] + this.their_priv_pk = vsm[2] + + this.makeM( + this.their_y + , this.m1_prime + , this.c_prime + , this.m2_prime + ).then(function (send) { + this.m1 = null + this.m2 = null + this.m1_prime = null + this.m2_prime = null + this.c = null + this.c_prime = null + + this.sendMsg(version, '\x12', send) + this.akeSuccess(version) + }.bind(this)) + .catch(function (error) { + console.error("OTR makeM error: " + error); + }) + + }.bind(this)) + .catch(function (error) { + console.error("OTR verify error: " + error); + }) + }.bind(this)) + .catch(function (error) { + console.error("OTR decrypt error: " + error); + }) return case '\x12': @@ -313,7 +361,7 @@ msg = HLP.splitype(['DATA', 'MAC'], msg.msg) - vsm = this.verifySignMac( + this.verifySignMac( msg[1] , msg[0] , this.m2_prime @@ -322,19 +370,27 @@ , this.our_dh.publicKey , this.m1_prime , HLP.packCtr(0) - ) - if (vsm[0]) return this.otr.error(vsm[0], true) + ).then(function (vsm) { + if (vsm[0]) { + console.error("OTR error: " + vsm[0]); + this.otr.error(vsm[0], true) + return + } - // store their key - this.their_keyid = vsm[1] - this.their_priv_pk = vsm[2] + // store their key + this.their_keyid = vsm[1] + this.their_priv_pk = vsm[2] - this.m1_prime = null - this.m2_prime = null - this.c_prime = null + this.m1_prime = null + this.m2_prime = null + this.c_prime = null - this.transmittedRS = true - this.akeSuccess(version) + this.transmittedRS = true + this.akeSuccess(version) + }.bind(this)) + .catch(function (error) { + console.error("OTR error: " + error); + }) return default: @@ -388,12 +444,12 @@ this.myhashed = CryptoJS.SHA256(gxmpi) this.myhashed = HLP.packData(this.myhashed.toString(CryptoJS.enc.Latin1)) - this.dhcommit = HLP.packData(HLP.encryptAes(gxmpi, key, HLP.packCtr(0))) - this.dhcommit += this.myhashed - - this.sendMsg(version, '\x02', this.dhcommit) + HLP.encryptAesAsync(gxmpi, key, HLP.packCtr(0)).then(function(encrypted){ + this.dhcommit = encrypted + this.dhcommit += this.myhashed + this.sendMsg(version, '\x02', this.dhcommit) + }.bind(this)) } - } }).call(this) \ No newline at end of file diff --git a/lib/helpers.js b/lib/helpers.js index be8c7ad..69e9daf 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -114,6 +114,116 @@ ) } + HLP.convertWordArrayToArrayBuffer = function (wordArray) { + var buffer = new ArrayBuffer(wordArray.sigBytes); + var targetArray = new Uint8Array(buffer); + + for (var i = 0; i < wordArray.sigBytes; ++i) { // copy byte by byte from left as the last word could be incomplete + targetArray[i] = (wordArray.words[Math.floor(i / 4)] >> (3 - (i % 4)) * 8) & 0xff + } + + return buffer; + } + + HLP.convertArrayBufferToWordArray = function (arrayBuffer) { + var sourceArray = new Uint8Array(arrayBuffer); + var targetArray = new CryptoJS.lib.WordArray.init(undefined, arrayBuffer.byteLength); + + for (var i = 0; i < arrayBuffer.byteLength; ++i) { + if (i % 4 == 0) { + targetArray.words[Math.floor(i / 4)] = 0; // init element + } + targetArray.words[Math.floor(i / 4)] += sourceArray[i] << ((3 - (i % 4)) * 8); + } + + return targetArray; + } + + HLP.importAesKey = function(c) { // type of key is string + var keyBuffer = HLP.convertWordArrayToArrayBuffer(CryptoJS.enc.Latin1.parse(c)); + return crypto.subtle.importKey( + "raw", + keyBuffer, + "AES-CTR", + false, //whether the key is extractable (i.e. can be used in exportKey) + ["encrypt", "decrypt"] + ); + } + + HLP.encryptAesAsync = function (msg, c, iv) { // msg is wordarray, c and iv are string + // example: https://github.com/diafygi/webcrypto-examples#aes-ctr---importkey + return new Promise( + function(resolve, reject) { + HLP.importAesKey(c).then(function (key) { + console.log("AsyncCrypto: imported key: " + key); + var msgBuffer = HLP.convertWordArrayToArrayBuffer(msg); + var ivBuffer = HLP.convertWordArrayToArrayBuffer(CryptoJS.enc.Latin1.parse(iv)); + + // start encryption async and notify encapsulating promise -- https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt + crypto.subtle.encrypt( + { + name: "AES-CTR", + counter: ivBuffer, + length: 128, + }, + key, + msgBuffer + ).then(function(encrypted){ + //returns an ArrayBuffer containing the encrypted data + var wordArrayBuffer = HLP.convertArrayBufferToWordArray(encrypted) + var result = CryptoJS.enc.Latin1.stringify(wordArrayBuffer) + //console.log("AsyncCrypto: successfully encrypted: " + wordArrayBuffer + " / " + result.length); + resolve(result); + }).catch(function(err){ + console.error("AsyncCrypto: error encrypting: " + err); + reject(err); + }); + }) + .catch(function (err) { + console.error("AsyncCrypto: error importing key: " + err); + reject(err); + }); + } + ); + } + + HLP.decryptAesAsync = function (msg, c, iv) { // msg, c and iv are of type string + // example: https://github.com/diafygi/webcrypto-examples#aes-ctr---importkey + return new Promise( + function(resolve, reject) { + HLP.importAesKey(c).then(function (key) { + console.log("AsyncCrypto: imported key: " + key); + var msgBuffer = HLP.convertWordArrayToArrayBuffer(CryptoJS.enc.Latin1.parse(msg)); + var ivBuffer = HLP.convertWordArrayToArrayBuffer(CryptoJS.enc.Latin1.parse(iv)); + + // start encryption async and notify encapsulating promise -- https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt + crypto.subtle.decrypt( + { + name: "AES-CTR", + counter: ivBuffer, + length: 128, + }, + key, + msgBuffer + ).then(function(decrypted){ + //returns an ArrayBuffer containing the decrypted data + var wordArrayBuffer = HLP.convertArrayBufferToWordArray(decrypted) + var result = CryptoJS.enc.Latin1.stringify(wordArrayBuffer) + //console.log("AsyncCrypto: successfully decrypted: " + wordArrayBuffer + " / " + result.length); + resolve(result); + }).catch(function(err){ + console.error("AsyncCrypto: error decrypting: " + err); + reject(err); + }); + }) + .catch(function (err) { + console.error("AsyncCrypto: error importing key: " + err); + reject(err); + }); + } + ); + } + HLP.multPowMod = function (a, b, c, d, e) { return BigInt.multMod(BigInt.powMod(a, b, e), BigInt.powMod(c, d, e), e) } diff --git a/lib/otr.js b/lib/otr.js index 1790dce..af48fd2 100644 --- a/lib/otr.js +++ b/lib/otr.js @@ -218,8 +218,9 @@ }) this.sm.on('send', function (ssid, send) { if (self.ssid === ssid) { - send = self.prepareMsg(send) - self.io(send) + self.prepareMsg(send).then(function(msg){ + self.io(msg) + }) } }) } @@ -342,48 +343,55 @@ sessKeys.send_counter += 1 - var ctr = HLP.packCtr(sessKeys.send_counter) + return new Promise( + function(resolve, reject) { - var send = this.ake.otr_version + '\x03' // version and type - var v3 = (this.ake.otr_version === CONST.OTR_VERSION_3) + var ctr = HLP.packCtr(sessKeys.send_counter) - if (v3) { - send += this.our_instance_tag - send += this.their_instance_tag - } - - send += '\x00' // flag - send += HLP.packINT(this.our_keyid - 1) - send += HLP.packINT(this.their_keyid) - send += HLP.packMPI(this.our_dh.publicKey) - send += ctr.substring(0, 8) - - if (Math.ceil(msg.length / 8) >= MAX_UINT) // * 16 / 128 - return this.error('Message is too long.') + var send = this.ake.otr_version + '\x03' // version and type + var v3 = (this.ake.otr_version === CONST.OTR_VERSION_3) - var aes = HLP.encryptAes( - CryptoJS.enc.Latin1.parse(msg) - , sessKeys.sendenc - , ctr - ) - - send += HLP.packData(aes) - send += HLP.make1Mac(send, sessKeys.sendmac) - send += HLP.packData(this.oldMacKeys.splice(0).join('')) + if (v3) { + send += this.our_instance_tag + send += this.their_instance_tag + } - send = HLP.wrapMsg( - send - , this.fragment_size - , v3 - , this.our_instance_tag - , this.their_instance_tag + send += '\x00' // flag + send += HLP.packINT(this.our_keyid - 1) + send += HLP.packINT(this.their_keyid) + send += HLP.packMPI(this.our_dh.publicKey) + send += ctr.substring(0, 8) + + if (Math.ceil(msg.length / 8) >= MAX_UINT) // * 16 / 128 + return this.error('Message is too long.') + + HLP.encryptAesAsync( + CryptoJS.enc.Latin1.parse(msg) + , sessKeys.sendenc + , ctr + ).then(function (encrypted) { + send += HLP.packData(encrypted) + send += HLP.make1Mac(send, sessKeys.sendmac) + send += HLP.packData(this.oldMacKeys.splice(0).join('')) + + send = HLP.wrapMsg( + send + , this.fragment_size + , v3 + , this.our_instance_tag + , this.their_instance_tag + ) + if (send[0]) return this.error(send[0]) + + // emit extra symmetric key + if (esk) this.trigger('file', ['send', sessKeys.extra_symkey, esk]) + + resolve(send[1]) + }.bind(this)).catch(function (error) { + reject(error) //propagate error + }) + }.bind(this) ) - if (send[0]) return this.error(send[0]) - - // emit extra symmetric key - if (esk) this.trigger('file', ['send', sessKeys.extra_symkey, esk]) - - return send[1] } OTR.prototype.handleDataMsg = function (msg) { @@ -398,69 +406,83 @@ // ignore flag var ign = (msg[0] === '\x01') - if (this.msgstate !== CONST.MSGSTATE_ENCRYPTED || msg.length !== 8) { - if (!ign) this.error('Received an unreadable encrypted message.', true) - return - } - - var our_keyid = this.our_keyid - HLP.readLen(msg[2]) - var their_keyid = this.their_keyid - HLP.readLen(msg[1]) + return new Promise( + function(resolve, reject) { - if (our_keyid < 0 || our_keyid > 1) { - if (!ign) this.error('Not of our latest keys.', true) - return - } - - if (their_keyid < 0 || their_keyid > 1) { - if (!ign) this.error('Not of your latest keys.', true) - return - } + if (this.msgstate !== CONST.MSGSTATE_ENCRYPTED || msg.length !== 8) { + if (!ign) this.error('Received an unreadable encrypted message.', true) + reject('Received an unreadable encrypted message.') + return + } - var their_y = their_keyid ? this.their_old_y : this.their_y + var our_keyid = this.our_keyid - HLP.readLen(msg[2]) + var their_keyid = this.their_keyid - HLP.readLen(msg[1]) - if (their_keyid === 1 && !their_y) { - if (!ign) this.error('Do not have that key.') - return - } + if (our_keyid < 0 || our_keyid > 1) { + if (!ign) this.error('Not of our latest keys.', true) + reject('Not of your latest keys.') + return + } - var sessKeys = this.sessKeys[our_keyid][their_keyid] + if (their_keyid < 0 || their_keyid > 1) { + if (!ign) this.error('Not of your latest keys.', true) + reject('Not of your latest keys.') + return + } - var ctr = HLP.unpackCtr(msg[4]) - if (ctr <= sessKeys.rcv_counter) { - if (!ign) this.error('Counter in message is not larger.') - return - } - sessKeys.rcv_counter = ctr + var their_y = their_keyid ? this.their_old_y : this.their_y - // verify mac - vt += msg.slice(0, 6).join('') - var vmac = HLP.make1Mac(vt, sessKeys.rcvmac) + if (their_keyid === 1 && !their_y) { + if (!ign) this.error('Do not have that key.') + reject('Do not have that key.') + return + } - if (!HLP.compare(msg[6], vmac)) { - if (!ign) this.error('MACs do not match.') - return - } - sessKeys.rcvmacused = true + var sessKeys = this.sessKeys[our_keyid][their_keyid] - var out = HLP.decryptAes( - msg[5].substring(4) - , sessKeys.rcvenc - , HLP.padCtr(msg[4]) - ) - out = out.toString(CryptoJS.enc.Latin1) + var ctr = HLP.unpackCtr(msg[4]) + if (ctr <= sessKeys.rcv_counter) { + if (!ign) this.error('Counter in message is not larger.') + reject('Counter in message is not larger.') + return + } + sessKeys.rcv_counter = ctr - if (!our_keyid) this.rotateOurKeys() - if (!their_keyid) this.rotateTheirKeys(HLP.readMPI(msg[3])) + // verify mac + vt += msg.slice(0, 6).join('') + var vmac = HLP.make1Mac(vt, sessKeys.rcvmac) - // parse TLVs - var ind = out.indexOf('\x00') - if (~ind) { - this.handleTLVs(out.substring(ind + 1), sessKeys) - out = out.substring(0, ind) - } + if (!HLP.compare(msg[6], vmac)) { + if (!ign) this.error('MACs do not match.') + reject('MACs do not match.') + return + } + sessKeys.rcvmacused = true + + HLP.decryptAesAsync( + msg[5].substring(4) + , sessKeys.rcvenc + , HLP.padCtr(msg[4]) + ).then(function (out) { + out = out.toString(CryptoJS.enc.Latin1) + + if (!our_keyid) this.rotateOurKeys() + if (!their_keyid) this.rotateTheirKeys(HLP.readMPI(msg[3])) + + // parse TLVs + var ind = out.indexOf('\x00') + if (~ind) { + this.handleTLVs(out.substring(ind + 1), sessKeys) + out = out.substring(0, ind) + } - out = CryptoJS.enc.Latin1.parse(out) - return out.toString(CryptoJS.enc.Utf8) + out = CryptoJS.enc.Latin1.parse(out) + resolve(out.toString(CryptoJS.enc.Utf8)) + }.bind(this)) + .catch(function (error) { + reject(error) + }) + }.bind(this)) } OTR.prototype.handleTLVs = function (tlvs, sessKeys) { @@ -570,8 +592,10 @@ this.error('Message cannot be sent at this time.') return case CONST.MSGSTATE_ENCRYPTED: - msg = this.prepareMsg(msg) - break + this.prepareMsg(msg).then(function(msg){ + this.io(msg, meta) + }.bind(this)) + return default: throw new Error('Unknown message state.') } @@ -600,9 +624,17 @@ if ( msg.version === CONST.OTR_VERSION_3 && this.checkInstanceTags(msg.instance_tags) ) return // ignore - msg.msg = this.handleDataMsg(msg) - msg.encrypted = true - break + this.handleDataMsg(msg) + .then(function (result) { + msg.msg = result + msg.encrypted = true + + if (msg.msg) this.trigger('ui', [msg.msg, !!msg.encrypted]) + }.bind(this)) + .catch(function (error) { + console.error('Error handling data message: ' + error) + }) + return case 'query': if (this.msgstate === CONST.MSGSTATE_ENCRYPTED) this._akeInit() this.doAKE(msg) @@ -665,8 +697,9 @@ OTR.prototype.sendStored = function () { var self = this ;(this.storedMgs.splice(0)).forEach(function (elem) { - var msg = self.prepareMsg(elem.msg) - self.io(msg, elem.meta) + self.prepareMsg(elem.msg).then(function(msg){ + self.io(msg, elem.meta) + }) }) } @@ -691,8 +724,9 @@ msg += '\x00\x00\x00\x01' // four bytes indicating file msg += l1name - msg = this.prepareMsg(msg, filename) - this.io(msg) + this.prepareMsg(msg, filename).then(function (msg) { + this.io(msg) + }.bind(this)) } OTR.prototype.endOtr = function () {