/*! @file @id $Id$ This is the main application as it is fully run in the user's browser. @dot digraph X { start [URL="\ref start()"]; newuser [URL="\ref newuser()"]; login [URL="\ref login()"]; createkeypair [URL="\ref createkeypair()"]; chat [URL="\ref chat()"]; getpwd [URL="\ref getpwd()"]; setpw [URL="\ref setpw()"]; get [URL="\ref get()"]; sendmessage [URL="\ref sendmessage()"]; start -> newuser [label="if no keys exist"]; start -> login [label="if keys exist"]; newuser -> createkeypair [label="on submit"]; createkeypair -> "openpgp.generateKeyPair"; "openpgp.generateKeyPair" -> login [label="keys generated in local store"]; login -> chat [label="user is valid on server"]; chat -> getpwd [label="password not yet entered"]; getpwd -> setpw [label="on input"]; setpw -> chat [label="password is valid"]; chat -> chat [label="remain in chat"]; chat -> get [label="start timer"]; get -> get [label="restart timer"]; chat -> sendmessage [label="on submit"]; sendmessage -> chat [label="remain in chat"]; } @enddot */ // 1 2 3 4 5 6 7 8 // 45678901234567890123456789012345678901234567890123456789012345678901234567890 var password = null; ///< password, only stored temporary, until reload var username = null; ///< username, only used during registration var filecontent = new Array(); ///< temporary storage for attachments var socket = io.connect(); var reboottimer = null; /// Show error messsage /** Fades in an error message and logs to console. @param data (optional) The error can be a string or any structure. Strings are shown to the user, structures are logged only. @param stay (optional) If not given as @c true, reloads page after 5s. */ function error(data, stay) { $("#status").hide(); $("#status").addClass("error") $("#status").removeClass("notice") $("#status").removeClass("success") if (data) { if (typeof data == 'string') { $("#status").html(data); console.log("error: "+data); } else { $("#status").html('unknown error: '+JSON.stringify(data)); console.log("error: "+JSON.stringify(data)); } } else { $("#status").html('error'); console.log("error"); } $("#status").show(); if (!stay) { console.log("reboot in 5s"); console.log((new Error('stacktrace')).stack); if (!reboottimer) reboottimer = setTimeout(function() { reboottimer = null; start(); }, 5000); } } /// Show notice messsage /** Fades in an notice message and logs to console. @param text (optional) The data is a string. */ function notice(text) { $("#status").hide() $("#status").addClass("notice") $("#status").removeClass("error") $("#status").removeClass("success") if (text) { $("#status").html(text); console.log("notice: "+text); } else { $("#status").html(''); console.log("notice"); } $("#status").show(); } /// Show notice messsage /** Fades in an success message and logs to console. @param text (optional) The data is a string. */ function success(text) { $("#status").hide(); $("#status").addClass("success") $("#status").removeClass("error") $("#status").removeClass("notice") if (text) { $("#status").html(text); console.log("success: "+text); } else { $("#status").html(''); console.log("success"); } $("#status").show(); } /// Show status message in the main screen area /** @param id HTML id to be shown. @param msg The success message text */ function status(id, msg) { console.log("state: "+id); if (msg) success(msg); else $("#status").fadeOut('slow'); $("#main").children(":not(#"+id+")").hide(); $("#main #"+id).fadeIn("slow", function() { $("#main #"+id+" form input:first-child").focus(); }); } function emit(signal, data) { console.log("<-snd "+signal); socket.emit(signal, data); } function connected() { console.log("server connected"); $("#connectionstatus #bad").hide(); $("#connectionstatus #good").show(); success("server connected"); } function disconnected() { console.log("server disconnected"); $("#connectionstatus #good").hide(); $("#connectionstatus #bad").show(); error("server disconnected", true); } function connectionstatus() { if (socket.connected) connected(); else disconnected(); } /// Alert user /** Alert user, e.g. that a new message has arrived. */ function beep(user) { success("message from "+user+" received"); navigator.vibrate = navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate; if (navigator.vibrate) { // vibration API supported navigator.vibrate(1000); } (new Audio("sounds/A-Tone-His_Self-1266414414.mp3")).play(); } /// Toggle Menu Display function togglemenu() { $("#menu").toggle(); } /// Download Profile Backup function backup() { var download = document.createElement('a'); download.href = 'data:attachment/text,'+encodeURI(JSON.stringify(localStorage)); download.target = '_blank'; function pad(n) {return n<10 ? '0'+n : n} var now = new Date(); download.download = pad(now.getFullYear())+pad(now.getMonth()+1)+pad(now.getDate())+ "-"+userid()+"@"+window.location.hostname+".bak"; var clickEvent = new MouseEvent("click", { "view": window, "bubbles": true, "cancelable": false }); download.dispatchEvent(clickEvent); togglemenu(); } /// Upload Profile Backup function restore(evt) { if (!window.FileReader) return error("your browser dows not support file upload", true); for (var i=0, f; f=evt.target.files[i]; ++i) { var file = f; var reader = new FileReader(); reader.onload = function(evt) { if (evt.target.error) return error("error reading file", true); if (evt.target.readyState==0) return notice("waiting for data …"); if (evt.target.readyState==1) return notice("loading data …"); var parsed=JSON.parse(evt.target.result); togglemenu(); localStorage.pubkey = parsed.pubkey; localStorage.privkey = parsed.privkey; success("backup is restored"); console.log("reboot after restore in 2s"); if (!reboottimer) reboottimer = setTimeout(function() { reboottimer = null; start(); }, 2000); } reader.readAsText(file); } } /// Configure local groups /** … */ function groups() { } /// Check if password is set and matches the repeated password /** Checks if both passwords are identical and valid and gives feedback to the user. Called when user edits the password fields. Sets @ref username and checks @ref password - if both are well defined, enables the submit button. @param pwd The password. @param pwd2 The repeated password. */ function checkpwd(pwd, pwd2) { $("#register").submit(function(event) { return false; }); if (pwd==pwd2) password=pwd; else password=null; if (!password||password.length<1) password=null; $("#createuser").prop("disabled", !(username && password)); if (password) { if (username) success("user is ready to be created"); else notice("password matches, please chose a valid user name"); } else { if (username) notice("passwords don't match"); else if ($('#user').val()) notice("user name is already in use"); else notice("please chose a user name"); } } /// Checks if the receiver of a message exists on server. /** Calls checknewuser.php on server and enables the message submit button if the receiver of the message exists on the server. */ function checkpartner(user) { $("#chat").submit(function(event) { return false; }); emit("user", user); } /// Create Local Public-/Private-Key Pair /** Called if user has not yet his keys, just generates a new key pair. */ function createkeypair(user, pwd) { notice("generating keys"); openpgp.generateKeyPair({ numBits: 4096, userId: user, passphrase: pwd }).then(function(keyPair) { success("keys generated"); localStorage.pubkey = keyPair.publicKeyArmored; localStorage.privkey = keyPair.privateKeyArmored; login(); }).catch(function(e) { error("generating key pairs failed"); }); } /// Get Own Public Key /** @return public key object */ function publicKey() { if (typeof localStorage.pubkey == 'undefined') { if (typeof localStorage.pubKey == 'undefined') { return null; } else { localStorage.pubkey = localStorage.pubKey; localStorage.removeItem(pubKey); } } return openpgp.key.readArmored(localStorage.pubkey); } /// Get Own Private Key /** @return private key object */ function privateKey() { if (typeof localStorage.privkey == 'undefined') { if (typeof localStorage.privKey == 'undefined') { return null; } else { localStorage.privkey = localStorage.privKey; localStorage.removeItem(privKey); } } return openpgp.key.readArmored(localStorage.privkey); } /// Get Own User Name /** Get user name as user id of first public key */ function userid() { if (!publicKey() || publicKey().keys.length < 1 || publicKey().keys[0].getUserIds().length < 1) return null return publicKey().keys[0].getUserIds()[0]; } /// Clear Message Text And Attachments /** Does not remove the receiver's name */ function clearmessage() { filecontent = new Array(); $('#preview').empty(); $("#msg").val(""); } /// Display Image Attachments function attachments(files, id) { if (files) files.forEach(function(file) { var img = document.createElement('img'); img.src = file.content; $(id).append(img); }); } /// Upload Attachment /** Prepares attachment to be sent in a message. If the attachment is an image, it resizes the image to 400px on the lager side. By now, only images are supported. Stores data in global variable @ref filecontent. */ function fileupload(evt) { if (!window.FileReader) return error("your browser does not support file upload", true); for (var i=0, f; f=evt.target.files[i]; ++i) { var file = f; var reader = new FileReader(); reader.onload = function(evt) { if (evt.target.error) return error("error reading file", true); if (evt.target.readyState==0) return notice("waiting for data …"); if (evt.target.readyState==1) return notice("loading data …"); if (!file.type.match('^image/')) return error(file.name+": not an image", true); var img = document.createElement("img"); img.onload = function() { var MAX = 400; var width = img.width; var height = img.height; if (width > MAX) { height *= MAX / width; width = MAX; } if (height > MAX) { width *= MAX / height; height = MAX; } var canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); img.onload = function() { filecontent.push({type: file.type, content: img.src}); $("#preview").append(img); success('image of type '+file.type+' is ready to be sent'); } img.src = canvas.toDataURL(file.type); } img.src=evt.target.result; } reader.readAsDataURL(file); } } /// Sets Receiver's Name /** Called when clicked on a receiver's name. Sets focus to the message text field. @param name The receiver's name. */ function setreceiver(name) { $("#recv").val(name); checkpartner(name); $("#msg").focus(); } var userMap = null; function users(userlist) { console.log("rcv-> users"); userMap = new Array(); userlist.forEach(function(usr) { userMap[usr.name] = usr.pubkey; console.log(" user: "+usr.name); }); localStorage.userMap = JSON.stringify(userMap); } function fail(msg) { console.log("rcv-> fail"); error(msg); } function loggedin() { console.log("rcv-> login"); success("login successful"); chat(); } function user(usr) { if (usr.exists) console.log("rcv-> user("+usr.name+")"); else console.log("rcv-> user("+usr.name+"): name is available"); if ($("#newuser").is(":visible") && usr.name==$('#user').val()) { // same username as in the create user form $("#createuser").prop("disabled", usr.exists); // todo: check password if (!usr.exists) { username = usr.name; success("user name "+usr.name+" is available"); } else { username = null; error("user name "+usr.name+" is in use", true); } } if ($("#chat").is(":visible") && usr.name==$("#recv").val()) { // same username as in receiver $("#send").prop("disabled", !usr.exists); if (usr.exists) success("recipient exists"); else error("unknown recipient", true); } if (userMap == null) { if (localStorage.userMap) { userMap = JSON.parse(localStorage.userMap); } else { userMap = new Array(); } } if (usr.exists && usr.pubkey && userMap[usr.name] != usr.pubkey) { userMap[usr.name] = usr.pubkey; localStorage.userMap = JSON.stringify(userMap); } } /// Get a user's public key. /** The first time, gets it from the server, later from the cache. */ function getPublicKey(user) { var deferredObject = $.Deferred(); if (userMap && userMap[user]) deferredObject.resolve(userMap[user]); else deferredObject.reject("unknown user"); return deferredObject.promise(); } /// Received a list of messages from server function messages(ms) { console.log("rcv-> messages"); notice("load messages, please wait …"); ms.forEach(function(msg) { message(msg, false); }); success("ready"); status("chat"); } /// Received a message from server function message(m, signaling=true) { if (signaling) console.log("rcv-> message("+m.user+")"); if (!password || !privateKey()) return; var key=openpgp.key.readArmored(m.pubkey); if (key.err) return error("key of sender unreadable", true); var message = openpgp.message.readArmored(m.msg); var privkey = privateKey().keys[0]; if (privkey.decrypt(password)) // prepare own key openpgp.decryptAndVerifyMessage(privkey, key.keys, message) .then(function(msg) { // decryption succeded // prepend message to list of messages var message = JSON.parse(msg.text); $("#msgs") // todo: check msg.signatures[0].valid .prepend('
'+ ''+ (new Date(m.time)).toLocaleString()+ ''+ ''+ m.user+ ''+(message.receiver?" → "+message.receiver:"")+ '
'+ '
'+ message.text+ '
'); // show attachments attachments(message.files, '#id'+m.id+' .text'); // calculate and show emoticons $('#id'+m.id).emoticonize(); if (signaling) beep(m.user); }) .catch(function(e) { // not for me success(); }); } /// Send Message To Server /** User wants to send a message. Encrypt message with own private and the receiver's public key, then send it to the server. */ function sendmessage(recv, txt) { notice("1/3 preparing message …"); $("#message").fadeOut("slow"); getPublicKey(recv) // get receiver's public key .done(function(pk) { var key=openpgp.key.readArmored(pk); if (!pk||key.err) { $("#message").fadeIn("slow"); error("receiver's key not found", true); return; } var privkey = privateKey().keys[0]; privkey.decrypt(password); // get own private key ready var message = JSON.stringify({receiver: recv, text: txt, files: filecontent}); notice("2/3 encrypting message …"); openpgp.signAndEncryptMessage(key.keys.concat(publicKey().keys), privkey, message) .then(function(msg) { // message is encrypted notice("3/3 sending message …"); emit("message", {user: userid(), content: msg}); clearmessage(); }) .catch(function(e) { $("#message").fadeIn("slow"); error("encryption of message failed", true); }); }) .fail(function(e) { $("#message").fadeIn("slow"); error("user not found", true); }); $("#message").fadeIn("slow"); } /// Check And Set Password /** Check if given password matches to decrypt the private key. If so, store it in global temporary variable @ref password and start the chat. The password matches, when the private key can be decrypted. @param pwd The password to check. */ function setpw(pwd) { if (privateKey().keys[0].decrypt(pwd)) { success("password matches"); $("#removeKey").hide(); password = pwd; chat(); } else { notice("password does not match"); } } /// Create Password Entry Field /** Asks user for password. When user starts to enter it, it is permanentely checked in setpw(). As soon as the password matches, setpw() continues automatically. No submit is required by the user. */ function getpwd() { $("#removeKey").show(); status("getpwd"); } function deleteUser() { var uid = userid(); localStorage.pubkey = null; localStorage.privkey = null; error("user "+uid+" permanentely lost"); } function removeKey() { togglemenu(); $("#removeKey").hide(); status('forgotpassword'); } /// Main Chat Window /** Gets chat widgets from server and displays them. Starts timer for get() which polls for new messages. */ function chat() { if (!password) return getpwd(); if ($('#msgs').is(':empty')) emit("messages"); else status("chat"); } /// Login User /** This is not really a login, it is just some kind of validation. The server does not care if a user is online or not, it is only interesting to the client to make sure, everything is fine. User is logged in the following way: User name and public key are sent to the server. If the user name exists on the server and the public key is the same, the user is considered logged in, his credentials seem to be valid. If user does not yet exits on server, it is created now. If user exists, but public key is different, then this is a complete failure, something went terribly wrong. */ function login() { $("#username").html(userid()+"@"+window.location.hostname); emit("login", {name: userid(), pubkey: localStorage.pubkey}); success("login sent to server"); } /// Get And Display Form To Create New User /** Shows user creation form. On submit, a private key is generated in createkeypair(), then login() creates the user. */ function newuser() { status("newuser"); } /// Check if local storage is available function checkLocalStorage() { var test = 'test'; try { localStorage.setItem(test, test); localStorage.removeItem(test); return true; } catch(e) { status("nolocalstorage"); error("local storage not available"); } return false; } /// Initial Function: Startup /** Decide whether to login or to create a new user */ function start() { $("#menu").hide(); //status("startup"); if (checkLocalStorage()) try { if (!userid()) { newuser(); } else { login(); } } catch (m) { console.log(m.stack); error(m); } } function init() { /// On Load, Call @ref start $(window.onbeforeunload = function() { return "Are you sure you want to navigate away?"; }); /// Allow Running in Background on Android document.addEventListener('deviceready', function () { if (cordova && cordova.plugins.backgroundMode) { cordova.plugins.backgroundMode.enable(); } }, false); socket.io.on("connect", connected()); socket.io.on("reconnect", connected()); socket.io.on("disconnect", disconnected()); socket.io.on("error", disconnected()); socket.on("login", loggedin); socket.on("fail", fail); socket.on("user", user); socket.on("users", users); socket.on("message", message); socket.on("messages", messages); connectionstatus(); emit('users'); start(); } /// Start Main Loop $(init);