some fixes; asynchronous message decryption to keep browser responsive

version-1
Marc Wäckerlin 9 years ago
parent 2c92d69a6d
commit 3bd00f09fd
  1. 25
      nodejs/public/javascripts/openpgp.js
  2. 193
      nodejs/public/javascripts/openpgp.worker.js
  3. 57
      nodejs/public/javascripts/safechat.js
  4. 5
      nodejs/public/stylesheets/safechat.css
  5. 2
      nodejs/sockets/index.js
  6. 11
      nodejs/views/index.ejs

@ -995,7 +995,7 @@ module.exports = {
show_version: true, show_version: true,
show_comment: true, show_comment: true,
versionstring: "OpenPGP.js v1.2.0", versionstring: "OpenPGP.js v1.3.0",
commentstring: "http://openpgpjs.org", commentstring: "http://openpgpjs.org",
keyserver: "keyserver.linux.it", // "pgp.mit.edu:11371" keyserver: "keyserver.linux.it", // "pgp.mit.edu:11371"
@ -5255,7 +5255,6 @@ function ii(a, b, c, d, x, s, t) {
} }
function md51(s) { function md51(s) {
txt = '';
var n = s.length, var n = s.length,
state = [1732584193, -271733879, -1732584194, 271733878], state = [1732584193, -271733879, -1732584194, 271733878],
i; i;
@ -15922,6 +15921,10 @@ S2K.prototype.write = function () {
bytes += this.salt; bytes += this.salt;
bytes += String.fromCharCode(this.c); bytes += String.fromCharCode(this.c);
break; break;
case 'gnu':
throw new Error("GNU s2k type not supported.");
default:
throw new Error("Unknown s2k type.");
} }
return bytes; return bytes;
@ -15950,8 +15953,8 @@ S2K.prototype.produce_key = function (passphrase, numBytes) {
case 'iterated': case 'iterated':
var isp = [], var isp = [],
count = s2k.get_count(); count = s2k.get_count(),
data = s2k.salt + passphrase; data = s2k.salt + passphrase;
while (isp.length * data.length < count) while (isp.length * data.length < count)
isp.push(data); isp.push(data);
@ -15962,6 +15965,12 @@ S2K.prototype.produce_key = function (passphrase, numBytes) {
isp = isp.substr(0, count); isp = isp.substr(0, count);
return crypto.hash.digest(algorithm, prefix + isp); return crypto.hash.digest(algorithm, prefix + isp);
case 'gnu':
throw new Error("GNU s2k type not supported.");
default:
throw new Error("Unknown s2k type.");
} }
} }
@ -16655,8 +16664,8 @@ AsyncProxy.prototype.decryptKey = function(privateKey, password) {
}); });
self.tasks.push({ resolve:function(data) { self.tasks.push({ resolve:function(data) {
var packetlist = packet.List.fromStructuredClone(data); var packetlist = packet.List.fromStructuredClone(data),
data = new key.Key(packetlist); data = new key.Key(packetlist);
resolve(data); resolve(data);
}, reject:reject }); }, reject:reject });
}); });
@ -16683,8 +16692,8 @@ AsyncProxy.prototype.decryptKeyPacket = function(privateKey, keyIds, password) {
}); });
self.tasks.push({ resolve:function(data) { self.tasks.push({ resolve:function(data) {
var packetlist = packet.List.fromStructuredClone(data); var packetlist = packet.List.fromStructuredClone(data),
data = new key.Key(packetlist); data = new key.Key(packetlist);
resolve(data); resolve(data);
}, reject:reject }); }, reject:reject });
}); });

@ -0,0 +1,193 @@
;(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
// GPG4Browsers - An OpenPGP implementation in javascript
// Copyright (C) 2011 Recurity Labs GmbH
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 3.0 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
window = {}; // to make UMD bundles work
// Mozilla bind polyfill because phantomjs is stupid
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
FNOP = function() {},
fBound = function() {
return fToBind.apply(this instanceof FNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));
};
FNOP.prototype = this.prototype;
fBound.prototype = new FNOP();
return fBound;
};
}
importScripts('openpgp.js');
var MIN_SIZE_RANDOM_BUFFER = 40000;
var MAX_SIZE_RANDOM_BUFFER = 60000;
window.openpgp.crypto.random.randomBuffer.init(MAX_SIZE_RANDOM_BUFFER);
self.onmessage = function (event) {
var data = null,
err = null,
msg = event.data,
correct = false;
switch (msg.event) {
case 'configure':
for (var i in msg.config) {
window.openpgp.config[i] = msg.config[i];
}
break;
case 'seed-random':
if (!(msg.buf instanceof Uint8Array)) {
msg.buf = new Uint8Array(msg.buf);
}
window.openpgp.crypto.random.randomBuffer.set(msg.buf);
break;
case 'encrypt-message':
if (!msg.keys.length) {
msg.keys = [msg.keys];
}
msg.keys = msg.keys.map(packetlistCloneToKey);
window.openpgp.encryptMessage(msg.keys, msg.text).then(function(data) {
response({event: 'method-return', data: data});
}).catch(function(e) {
response({event: 'method-return', err: e.message});
});
break;
case 'sign-and-encrypt-message':
if (!msg.publicKeys.length) {
msg.publicKeys = [msg.publicKeys];
}
msg.publicKeys = msg.publicKeys.map(packetlistCloneToKey);
msg.privateKey = packetlistCloneToKey(msg.privateKey);
window.openpgp.signAndEncryptMessage(msg.publicKeys, msg.privateKey, msg.text).then(function(data) {
response({event: 'method-return', data: data});
}).catch(function(e) {
response({event: 'method-return', err: e.message});
});
break;
case 'decrypt-message':
msg.privateKey = packetlistCloneToKey(msg.privateKey);
msg.message = packetlistCloneToMessage(msg.message.packets);
window.openpgp.decryptMessage(msg.privateKey, msg.message).then(function(data) {
response({event: 'method-return', data: data});
}).catch(function(e) {
response({event: 'method-return', err: e.message});
});
break;
case 'decrypt-and-verify-message':
msg.privateKey = packetlistCloneToKey(msg.privateKey);
if (!msg.publicKeys.length) {
msg.publicKeys = [msg.publicKeys];
}
msg.publicKeys = msg.publicKeys.map(packetlistCloneToKey);
msg.message = packetlistCloneToMessage(msg.message.packets);
window.openpgp.decryptAndVerifyMessage(msg.privateKey, msg.publicKeys, msg.message).then(function(data) {
response({event: 'method-return', data: data});
}).catch(function(e) {
response({event: 'method-return', err: e.message});
});
break;
case 'sign-clear-message':
msg.privateKeys = msg.privateKeys.map(packetlistCloneToKey);
window.openpgp.signClearMessage(msg.privateKeys, msg.text).then(function(data) {
response({event: 'method-return', data: data});
}).catch(function(e) {
response({event: 'method-return', err: e.message});
});
break;
case 'verify-clear-signed-message':
if (!msg.publicKeys.length) {
msg.publicKeys = [msg.publicKeys];
}
msg.publicKeys = msg.publicKeys.map(packetlistCloneToKey);
var packetlist = window.openpgp.packet.List.fromStructuredClone(msg.message.packets);
msg.message = new window.openpgp.cleartext.CleartextMessage(msg.message.text, packetlist);
window.openpgp.verifyClearSignedMessage(msg.publicKeys, msg.message).then(function(data) {
response({event: 'method-return', data: data});
}).catch(function(e) {
response({event: 'method-return', err: e.message});
});
break;
case 'generate-key-pair':
window.openpgp.generateKeyPair(msg.options).then(function(data) {
data.key = data.key.toPacketlist();
response({event: 'method-return', data: data});
}).catch(function(e) {
response({event: 'method-return', err: e.message});
});
break;
case 'decrypt-key':
try {
msg.privateKey = packetlistCloneToKey(msg.privateKey);
correct = msg.privateKey.decrypt(msg.password);
if (correct) {
data = msg.privateKey.toPacketlist();
} else {
err = 'Wrong password';
}
} catch (e) {
err = e.message;
}
response({event: 'method-return', data: data, err: err});
break;
case 'decrypt-key-packet':
try {
msg.privateKey = packetlistCloneToKey(msg.privateKey);
msg.keyIds = msg.keyIds.map(window.openpgp.Keyid.fromClone);
correct = msg.privateKey.decryptKeyPacket(msg.keyIds, msg.password);
if (correct) {
data = msg.privateKey.toPacketlist();
} else {
err = 'Wrong password';
}
} catch (e) {
err = e.message;
}
response({event: 'method-return', data: data, err: err});
break;
default:
throw new Error('Unknown Worker Event.');
}
};
function response(event) {
if (window.openpgp.crypto.random.randomBuffer.size < MIN_SIZE_RANDOM_BUFFER) {
postMessage({event: 'request-seed'});
}
postMessage(event);
}
function packetlistCloneToKey(packetlistClone) {
var packetlist = window.openpgp.packet.List.fromStructuredClone(packetlistClone);
return new window.openpgp.key.Key(packetlist);
}
function packetlistCloneToMessage(packetlistClone) {
var packetlist = window.openpgp.packet.List.fromStructuredClone(packetlistClone);
return new window.openpgp.message.Message(packetlist);
}
},{}]},{},[1])
;

@ -117,11 +117,10 @@ function success(text) {
@param msg The success message text */ @param msg The success message text */
function status(id, msg) { function status(id, msg) {
console.log("state: "+id); console.log("state: "+id);
if (msg) success(msg); else $("#status").fadeOut('slow'); if (msg) success(msg); else $("#status").hide();
$("#main").children(":not(#"+id+")").hide(); $("#main").children(":not(#"+id+")").hide();
$("#main #"+id).fadeIn("slow", function() { $("#main #"+id).show();
$("#main #"+id+" form input:first-child").focus(); $("#main #"+id+" form input:first-child").focus();
});
} }
function emit(signal, data) { function emit(signal, data) {
@ -147,10 +146,18 @@ function connectionstatus() {
if (socket.connected) connected(); else disconnected(); if (socket.connected) connected(); else disconnected();
} }
function htmlenc(html) {
return $('<div/>').text(html).html();
}
function htmldec(data) {
return $('<div/>').html(data).text();
}
/// Alert user /// Alert user
/** Alert user, e.g. that a new message has arrived. */ /** Alert user, e.g. that a new message has arrived. */
function beep(user) { function beep(user) {
success("message from "+user+" received"); if (user) success("message from "+htmlenc(user)+" received");
navigator.vibrate = navigator.vibrate =
navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate; navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate;
if (navigator.vibrate) { if (navigator.vibrate) {
@ -313,9 +320,11 @@ function userid() {
/// Clear Message Text And Attachments /// Clear Message Text And Attachments
/** Does not remove the receiver's name */ /** Does not remove the receiver's name */
function clearmessage() { function clearmessage() {
$("#message").prop(":disabled", true);
filecontent = new Array(); filecontent = new Array();
$('#preview').empty(); $('#preview').empty();
$("#msg").val(""); $("#msg").val("");
$("#message").prop(":disabled", false);
} }
/// Display Image Attachments /// Display Image Attachments
@ -392,8 +401,11 @@ var userMap = null;
function users(userlist) { function users(userlist) {
console.log("rcv-> users"); console.log("rcv-> users");
userMap = new Array(); userMap = new Array();
$("#allusers").empty();
userlist.forEach(function(usr) { userlist.forEach(function(usr) {
userMap[usr.name] = usr.pubkey; userMap[usr.name] = usr.pubkey;
$("#allusers").append('<option value="'+htmlenc(usr.name)+'">')
$("#allusers").hide();
console.log(" user: "+usr.name); console.log(" user: "+usr.name);
}); });
localStorage.userMap = JSON.stringify(userMap); localStorage.userMap = JSON.stringify(userMap);
@ -440,6 +452,7 @@ function user(usr) {
} }
if (usr.exists && usr.pubkey && userMap[usr.name] != usr.pubkey) { if (usr.exists && usr.pubkey && userMap[usr.name] != usr.pubkey) {
userMap[usr.name] = usr.pubkey; userMap[usr.name] = usr.pubkey;
$("#allusers").append('option value="'+htmlenc(usr.name)+'"')
localStorage.userMap = JSON.stringify(userMap); localStorage.userMap = JSON.stringify(userMap);
} }
} }
@ -454,13 +467,13 @@ function getPublicKey(user) {
} }
/// Received a list of messages from server /// Received a list of messages from server
function messages(ms) { function messages(msgs) {
console.log("rcv-> messages"); console.log("rcv-> messages("+msgs.length+")");
if (!password || !privateKey())
return setTimeout(function() {emit("messages");}, 1000); // try again later
status("allmessages");
notice("load messages, please wait …"); notice("load messages, please wait …");
ms.forEach(function(msg) { msgs.forEach(function(msg) {message(msg, false);});
message(msg, false);
});
success("ready");
status("chat"); status("chat");
} }
@ -509,12 +522,12 @@ function message(m, signaling=true) {
the receiver's public key, then send it to the server. */ the receiver's public key, then send it to the server. */
function sendmessage(recv, txt) { function sendmessage(recv, txt) {
notice("1/3 preparing message …"); notice("1/3 preparing message …");
$("#message").fadeOut("slow"); $("#message").prop(":disabled", true);
getPublicKey(recv) // get receiver's public key getPublicKey(recv) // get receiver's public key
.done(function(pk) { .done(function(pk) {
var key=openpgp.key.readArmored(pk); var key=openpgp.key.readArmored(pk);
if (!pk||key.err) { if (!pk||key.err) {
$("#message").fadeIn("slow"); $("#message").prop(":disabled", false);
error("receiver's key not found", true); error("receiver's key not found", true);
return; return;
} }
@ -529,15 +542,14 @@ function sendmessage(recv, txt) {
clearmessage(); clearmessage();
}) })
.catch(function(e) { .catch(function(e) {
$("#message").fadeIn("slow"); $("#message").prop(":disabled", false);
error("encryption of message failed", true); error("encryption of message failed", true);
}); });
}) })
.fail(function(e) { .fail(function(e) {
$("#message").fadeIn("slow"); $("#message").prop(":disabled", false);
error("user not found", true); error("user not found", true);
}); });
$("#message").fadeIn("slow");
} }
/// Check And Set Password /// Check And Set Password
@ -583,12 +595,15 @@ function removeKey() {
/// Main Chat Window /// Main Chat Window
/** Gets chat widgets from server and displays them. Starts timer for /** Gets chat widgets from server and displays them. Starts timer for
get() which polls for new messages. */ get() which polls for new messages. */
var firsttime = true;
function chat() { function chat() {
if (!password) return getpwd(); if (!password) return getpwd();
if ($('#msgs').is(':empty')) status("chat");
if (firsttime && $('#msgs').is(':empty')) {
firsttime = false;
notice("getting previous messages, please wait …");
emit("messages"); emit("messages");
else }
status("chat");
} }
/// Login User /// Login User
@ -670,6 +685,10 @@ function init() {
socket.on("message", message); socket.on("message", message);
socket.on("messages", messages); socket.on("messages", messages);
connectionstatus(); connectionstatus();
if (openpgp.initWorker("javascripts/openpgp.worker.js"))
console.log("asynchronous openpgp enabled");
else
console.log("asynchronous openpgp failed");
emit('users'); emit('users');
start(); start();
} }

@ -188,6 +188,11 @@ td:last-child {
.clear { .clear {
clear: both; clear: both;
} }
:disabled {
opacity: 0.4;
filter: alpha(opacity=40); /* MSIE */
background-color: lightgray;
}
label[for=send] img { label[for=send] img {
opacity: 0.4; opacity: 0.4;
filter: alpha(opacity=40); /* MSIE */ filter: alpha(opacity=40); /* MSIE */

@ -43,7 +43,7 @@ module.exports = function(sql) {
socket.on("messages", function(msg) { socket.on("messages", function(msg) {
console.log("-> signal: messages"); console.log("-> signal: messages");
sql.query("select * from message, user where message.user = user.name", [], sql.query("select * from message, user where message.user = user.name order by message.id", [],
function(err, res, flds) { function(err, res, flds) {
emit('messages', res); emit('messages', res);
}); });

@ -46,6 +46,7 @@
<div id="main"> <div id="main">
<!-- Register New User -->
<div id="newuser" style="display: none"> <div id="newuser" style="display: none">
<h2>Register User</h2> <h2>Register User</h2>
<p>All you need to start is a username and a password:</p> <p>All you need to start is a username and a password:</p>
@ -99,6 +100,7 @@
up your keys.</p> up your keys.</p>
</div> </div>
<!-- Enter Password -->
<div id="getpwd" style="display: none"> <div id="getpwd" style="display: none">
<form> <form>
<input placeholder="please enter password" id="pwd" oninput="setpw(this.value)" <input placeholder="please enter password" id="pwd" oninput="setpw(this.value)"
@ -106,10 +108,12 @@
</form> </form>
</div> </div>
<!-- Chat Screen -->
<div id="chat" style="display: none"> <div id="chat" style="display: none">
<div id="message"> <div id="message">
<form id="chat" autocomplete="off" onsubmit="sendmessage(this.elements['recv'].value, this.elements['msg'].value)"> <form id="chat" autocomplete="off" onsubmit="sendmessage(this.elements['recv'].value, this.elements['msg'].value)">
<input placeholder="receiver" autocomplete="off" type="text" id="recv" oninput="checkpartner(this.value)" /> <input placeholder="receiver" autocomplete="off" type="text" list="allusers" id="recv" oninput="checkpartner(this.value)" />
<datalist id="allusers"></datalist>
<input placeholder="message" autocomplete="off" type="text" id="msg"/> <input placeholder="message" autocomplete="off" type="text" id="msg"/>
<div class="buttongroup"> <div class="buttongroup">
<span class="toolbutton"> <span class="toolbutton">
@ -149,6 +153,7 @@
</script> </script>
</div> </div>
<!-- Password Forgotten -->
<div id="forgotpassword" style="display: none"> <div id="forgotpassword" style="display: none">
<h2>Password Forgotten</h2> <h2>Password Forgotten</h2>
<div class="warning"><strong>Warning!</strong> <div class="warning"><strong>Warning!</strong>
@ -174,6 +179,7 @@
</div> </div>
</div> </div>
<!-- Error: Missing JavaScript -->
<noscript> <noscript>
<h2>JavasScript Required!</h2> <h2>JavasScript Required!</h2>
<p>This is a secure and encryptet chat application, that runs <p>This is a secure and encryptet chat application, that runs
@ -184,15 +190,18 @@
<p><a href="<%= projecturl %>" target="_blank">more information</a></p> <p><a href="<%= projecturl %>" target="_blank">more information</a></p>
</noscript> </noscript>
<!-- Error: Missing LocalStorage -->
<div id="nolocalstorage" style="display: none"> <div id="nolocalstorage" style="display: none">
<p>No access to local storage. Please allow access to local <p>No access to local storage. Please allow access to local
storage, i.e. do not block cookies.<p> storage, i.e. do not block cookies.<p>
</div> </div>
<!-- Notice: Setup Messages -->
<div id="allmessages" style="display: none"> <div id="allmessages" style="display: none">
<p>Setting up all previous messages, please wait …</p> <p>Setting up all previous messages, please wait …</p>
</div> </div>
<!-- Notice: Startup -->
<div id="startup"> <div id="startup">
<p>Starting up …</p> <p>Starting up …</p>
</div> </div>

Loading…
Cancel
Save