/* Javascript parts of the group demo. To use, load group_demo.html in your * browser. */ function buttonElement(buttonLabel, clickHandler) { var button = document.createElement("button"); button.appendChild(document.createTextNode(buttonLabel)); button.addEventListener("click", clickHandler, false); return button; } function buttonsAndText(textContent, buttonLabelToHandlerMap) { var el = document.createElement("div"); for (var label in buttonLabelToHandlerMap) { if (!buttonLabelToHandlerMap.hasOwnProperty(label)) { continue; } var handler = buttonLabelToHandlerMap[label]; var button = buttonElement(label, handler); el.appendChild(button); } var message_element = document.createElement("tt"); el.appendChild(message_element); var content = document.createTextNode(textContent); message_element.appendChild(content); return el; } function buttonAndTextElement(buttonLabel, textContent, clickHandler) { var buttonMap = {}; buttonMap[buttonLabel] = clickHandler; return buttonsAndText(textContent, buttonMap); } function DemoUser(name) { this.name = name; this.olmAccount = new Olm.Account(); this.olmAccount.create(); this.idKey = this.getIdKeys()["curve25519"]; /* the people in our chat, indexed by their Curve25519 identity key. */ this.peers = {}; /* the Ed25519 signing key for each peer, indexed by their Curve25519 id key */ this.peerSigningKeys = {}; /* for each peer, a one-to-one session - indexed by id key and created on * demand */ this.peerSessions = {}; /* for each peer, info on their sender session - indexed by id key and * session id */ this.peerGroupSessions = {}; /* our outbound group session */ this.groupSession = undefined; /* a list of pending tasks */ this.tasks = []; this.taskWorker = undefined; /* the operations our peers are allowed to do on us */ var publicOps = [ "getIdKeys", "getOneTimeKey", "receiveOneToOne", "receiveGroup", ]; this.remoteOps = {}; for (var i=0; i<publicOps.length; i++) { var op = publicOps[i]; this.remoteOps[op] = this[op].bind(this); } } DemoUser.prototype._progress = function(message) { var progress = this.progressElement; var message_element = document.createElement("pre"); var start_content = document.createTextNode(message + "..."); function start() { message_element.appendChild(start_content); progress.appendChild(message_element); } function done(res) { var done_content = document.createTextNode(message + "..." + res); message_element.replaceChild(done_content, start_content); } return {start:start, done:done}; }; DemoUser.prototype._do_tasks = function() { var self = this; var task = self.tasks.shift(); var desc = task[0]; var func = task[1]; var callback = task[2]; var p = self._progress(desc); p.start(); function done() { p.done("Done"); if (callback) { try { callback.apply(undefined, arguments) } catch (e) { console.error("Uncaught exception in callback", e.stack || e); } } start_tasks(); } // sleep 50ms before actually doing the task self.taskWorker = window.setTimeout(function() { try { task[1](done); } catch (e) { console.error("Uncaught exception in task", e.stack || e); p.done("Failed: "+e); start_tasks(); } }, 50); function start_tasks() { if (self.tasks.length == 0) { self.taskWorker = undefined; return; } self.taskWorker = window.setTimeout(self._do_tasks.bind(self), 50); } } /** * add a function "task" to this user's queue of things to do. * * task is called with a single argument 'done' which is a function to call * once the task is complete. * * 'callback' is called once the task is complete, with any arguments that * were passed to 'done'. */ DemoUser.prototype.addTask = function(description, task, callback) { this.tasks.push([description, task, callback]); if(!this.taskWorker) { this._do_tasks(); } }; DemoUser.prototype.addPeer = function(peerOps) { var keys = peerOps.getIdKeys(); var id = keys["curve25519"]; this.peers[id] = peerOps; this.peerSigningKeys[id] = keys["ed25519"]; }; DemoUser.prototype.getIdKeys = function() { return JSON.parse(this.olmAccount.identity_keys()); }; DemoUser.prototype.generateKeys = function(callback) { var self = this; this.addTask("generate one time key", function(done) { self.olmAccount.generate_one_time_keys(1); done(); }, callback); }; DemoUser.prototype.getOneTimeKey = function() { var self = this; var keys = JSON.parse(self.olmAccount.one_time_keys()) .curve25519; for (key_id in keys) { if (keys.hasOwnProperty(key_id)) { return keys[key_id]; } } throw new Error("No one-time-keys generated"); }; /* ************************************************************************ * * one-to-one messaging */ /** * retrieve, or initiate, a one-to-one session to a given peer */ DemoUser.prototype.getPeerSession = function(peerId, callback) { var self = this; if (this.peerSessions[peerId]) { callback(this.peerSessions[peerId]); return; } var peer = this.peers[peerId]; this.addTask("get peer keys", function(done) { key = peer.getOneTimeKey(); done(key); }, function(ot_key) { self.addTask("create peer session", function(done) { var session = new Olm.Session(); session.create_outbound(self.olmAccount, peerId, ot_key); self.peerSessions[peerId] = session; done(session); }, callback); }); }; /** * encrypt a one-to-one message and prepare it for sending to a peer */ DemoUser.prototype.sendToPeer = function(peerId, message, callback) { var self = this; this.getPeerSession(peerId, function(session) { self.addTask("encrypt one-to-one message", function(done) { var encrypted = session.encrypt(message); var packet = { sender_key: self.idKey, ciphertext: encrypted, }; var json = JSON.stringify(packet); var el = buttonAndTextElement("send", json, function(ev) { self.peers[peerId].receiveOneToOne(json); }); self.cipherOutputDiv.appendChild(el); done(); }, callback); }); }; /** * handler for receiving a one-to-one message */ DemoUser.prototype.receiveOneToOne = function(jsonpacket) { var self = this; var el = buttonAndTextElement("decrypt", jsonpacket, function(ev) { var sender = JSON.parse(jsonpacket).sender_key; self.decryptOneToOne(jsonpacket, function(result) { var el2 = document.createElement("tt"); el.appendChild(el2); var content = document.createTextNode(" -> "+result); el2.appendChild(content); var body = JSON.parse(result); // create a new inbound session if we don't yet have one if (!self.peerGroupSessions[sender] || !self.peerGroupSessions[sender][body.session_id]) { self.createInboundSession( sender, body.session_id, body.session_key ); } }); }); this.cipherInputDiv.appendChild(el); }; /** * add a task to decrypt a one-to-one message. Calls the callback with the * decrypted plaintext */ DemoUser.prototype.decryptOneToOne = function(jsonpacket, callback) { var self = this; self.addTask("decrypt one-to-one message", function(done) { var packet = JSON.parse(jsonpacket); var peerId = packet.sender_key; var session = self.peerSessions[peerId]; var plaintext; if (session) { plaintext = session.decrypt(packet.ciphertext.type, packet.ciphertext.body); done(plaintext); return; } if (packet.ciphertext.type != 0) { throw new Error("Unknown one-to-one session"); } session = new Olm.Session(); session.create_inbound(self.olmAccount, packet.ciphertext.body); self.peerSessions[peerId] = session; plaintext = session.decrypt(packet.ciphertext.type, packet.ciphertext.body); done(plaintext); }, callback) }; /* ************************************************************************ * * group messaging */ /** * retrieve, or initiate, an outbound group session */ DemoUser.prototype.getGroupSession = function() { if (this.groupSession) { return this.groupSession; } this.groupSession = new Olm.OutboundGroupSession(); this.groupSession.create(); var keymsg = { "session_id": this.groupSession.session_id(), "session_key": this.groupSession.session_key(), "message_index": this.groupSession.message_index(), }; var jsonmsg = JSON.stringify(keymsg); for (var peer in this.peers) { if (!this.peers.hasOwnProperty(peer)) { continue; } this.sendToPeer(peer, jsonmsg); } return this.groupSession; }; /** * add a task to create an inbound group session */ DemoUser.prototype.createInboundSession = function( peer_id, session_id, session_key, callback ) { var self = this; this.addTask("init inbound session", function(done) { session = new Olm.InboundGroupSession(); session.create(session_key); if (!self.peerGroupSessions[peer_id]) { self.peerGroupSessions[peer_id] = {}; } if (session_id != session.session_id()) { throw new Error("Mismatched session_ids"); } self.peerGroupSessions[peer_id][session_id] = session; done(session); }, callback); }; /** * handler for receiving a group message */ DemoUser.prototype.receiveGroup = function(jsonpacket) { var self = this; var el = buttonAndTextElement("decrypt", jsonpacket, function(ev) { self.decryptGroup(jsonpacket, function(result) { var el2 = document.createElement("tt"); el.appendChild(el2); var content = document.createTextNode(" -> "+result); el2.appendChild(content); }); }); this.groupInputDiv.appendChild(el); }; /** * add a task to decrypt a received group message. Calls the callback with the * decrypted plaintext */ DemoUser.prototype.decryptGroup = function(jsonpacket, callback) { var self = this; this.addTask("decrypt group message", function(done) { var packet = JSON.parse(jsonpacket); var sender = packet.sender_key; var session_id = packet.session_id; var sender_signing_key = self.peerSigningKeys[sender]; if (!sender_signing_key) { throw new Error("No known signing key for sender "+sender); } var olmUtility = new Olm.Utility(); olmUtility.ed25519_verify( sender_signing_key, packet.body, packet.signature ); var peer_sessions = self.peerGroupSessions[sender]; if (!peer_sessions) { throw new Error("No sessions for sender "+sender); } var session = peer_sessions[session_id]; if (!session) { throw new Error("Unknown session id " + session_id); } var plaintext = session.decrypt(packet.body); done(plaintext); }, callback); }; /** * add a task to encrypt, and prepare for sending, a group message. * * Will create a group session if necessary */ DemoUser.prototype.encrypt = function(message) { var self = this; var session = this.getGroupSession(); function sendJsonToPeers(json) { for (var peer in self.peers) { if (!self.peers.hasOwnProperty(peer)) { continue; } self.peers[peer].receiveGroup(json); } } self.addTask("encrypt group message", function(done) { var encrypted = session.encrypt(message); var signature = self.olmAccount.sign(encrypted); var packet = { sender_key: self.idKey, session_id: session.session_id(), body: encrypted, signature: signature, }; var json = JSON.stringify(packet); var el = buttonsAndText(json, { send: function(ev) { sendJsonToPeers(json); }, "send corrupted": function(ev) { var p = JSON.parse(json); p.body += " "; sendJsonToPeers(JSON.stringify(p)); }, }); self.groupOutputDiv.appendChild(el); done(); }); }; /* ************************************************************************** */ function initUserDiv(demoUser, div) { demoUser.progressElement = div.getElementsByClassName("user_progress")[0]; demoUser.cipherOutputDiv = div.getElementsByClassName("user_cipher_output")[0]; demoUser.cipherInputDiv = div.getElementsByClassName("user_cipher_input")[0]; demoUser.groupOutputDiv = div.getElementsByClassName("group_output")[0]; demoUser.groupInputDiv = div.getElementsByClassName("group_input")[0]; var plain_input = div.getElementsByClassName("user_plain_input")[0]; var encrypt = div.getElementsByClassName("user_encrypt")[0]; encrypt.addEventListener("click", function() { demoUser.encrypt(plain_input.value); }, false); } function startDemo() { var user1 = new DemoUser(); initUserDiv(user1, document.getElementById("user1")); user1.generateKeys(); var user2 = new DemoUser(); initUserDiv(user2, document.getElementById("user2")); user2.generateKeys(); user1.addPeer(user2.remoteOps); user2.addPeer(user1.remoteOps); } document.addEventListener("DOMContentLoaded", startDemo, false);