/*! @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.generateKey"; "openpgp.generateKey" -> 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 function SafeChatClient(success, notice, error) { /// Cache Client's Key from local Strorage var k = null; /// Get User Key /** @internal key ist cached in k @return key */ function key() { if (k) return k if (typeof localStorage.key == 'undefined') return null return k = openpgp.key.readArmored(localStorage.key) } /// Get Own User Name /** Get user name as user id of first public key */ function uid() { if (k || key()) return k.pub.keys[0].getUserIds()[0] return null } /// Create New User function createuser(user, email, pwd) { notice("generating keys") openpgp.generateKey({ numBits: 4096, userIds: [{name: user, email: email}], passphrase: pwd }).then(function(keyPair) { success("keys generated") localStorage.key = keyPair.privateKeyArmored k = keyPair.key }).catch(function(e) { console.log(e) error("generating key pairs failed") }) } function password(pwd) { return (k || keys()) && k.keys[0].decrypt(pwd) } /// Encrypt Message function encrypt(targetkeys, message, done, failed) { if (!k) return false openpgp.encrypt({ publicKeys: targets.keys.concat(k.keys), privateKeys: k, data: message, armor: false}) .then(done) .else(failed) return true } /// Decrypt Message function decrypt(message) { if (!k) return false return true } } 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 hostname = window.location.hostname!='localhost'?window.location.hostname:'safechat.ch'; /// Padding for numbers in dates function pad(n) { return n<10 ? '0'+n : n } function uid(name) { return name+' <'+name+'@'+hostname+'>'; } /// Convert number of bytes to readable text function size(num) { if (num>0.6*1024) { if (num>0.6*1024*1024) { if (num>0.6*1024*1024*1024) { if (num>0.6*1024*1024*1024*1024) { return Math.round(num/1024/1024/1024/1024)+"TB"; } else { return Math.round(num/1024/1024/1024)+"GB"; } } else { return Math.round(num/1024/1024)+"MB"; } } else { return Math.round(num/1024)+"kB"; } } else { return num+"B"; } } 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").hide(); $("#main").children(":not(#"+id+")").hide(); $("#main #"+id).show(); $("#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(); } function htmlenc(html) { return $('
').text(html).html(); } function htmldec(data) { return $('
').html(data).text(); } /// Alert user /** Alert user, e.g. that a new message has arrived. */ function beep(user) { if (user) success("message from "+htmlenc(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'; var now = new Date(); download.download = pad(now.getFullYear())+pad(now.getMonth()+1)+pad(now.getDate())+ "-"+userid()+"@"+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 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 …"); 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", uid(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.generateKey({ numBits: 4096, userIds: [{name: user, email: user+'@'+hostname}], passphrase: pwd }).then(function(keyPair) { success("keys generated"); localStorage.pubkey = keyPair.publicKeyArmored; localStorage.privkey = keyPair.privateKeyArmored; login(); }).catch(function(e) { console.log(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() { $("#message").prop(":disabled", true); filecontent = new Array(); $('#preview').empty(); $("#msg").val(""); $("#message").prop(":disabled", false); } function guessfilename(mimetype, user, date) { if (!user) user = userid(); if (!date) date = new Date(); var ext = mimetype.replace(/.*\/(x-)?/i, ""); return pad(date.getFullYear())+pad(date.getMonth()+1)+pad(date.getDate()) +"-"+ext+"-"+user+"@"+hostname+'.'+ext; } /// Display Image Attachments function attachments(files, id, from, date) { if (files) files.forEach(function(file) { console.log(file); if (!file.name) file.name = guessfilename(file.type, from, date); var a = document.createElement('a'); a.href = file.content; a.download = file.name; a.target = '_blank'; if (file.type.match('^image/')) { var img = document.createElement('img'); img.title = file.name; img.src = file.content; a.appendChild(img); } else if (file.type.match('^video/')) { var video = document.createElement('video'); video.controls = true; video.title = file.name; video.src = file.content; a.appendChild(video); } else { var img = document.createElement('img'); img.title = file.name; img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg"; a.appendChild(img); } $(id).append(a); }); } var recorder; function done() { if (recorder) { recorder.stop(); recorder.recording(function(data) { previewfile(data, "video/webm"); abort(); }); } } function abort() { if (recorder) { $("#videorecorder").hide(); recorder.release(); delete recorder; recorder = null; } } /// Record Video from builtin camera function recordvideo() { try { abort(); $("#videorecorder").show(); recorder = new MediaStreamRecorder({ video: { width: {ideal: 180}, height: {ideal: 160} }, audio: true }); recorder.on("ready", function() { $("#videorecorder video").attr("src", recorder.preview()); $("#videorecorder video").css("width", 180); $("#videorecorder video").css("height", 160); $("#videorecorder video").attr("width", 180); $("#videorecorder video").attr("height", 160); recorder.start(); }); } catch (e) { console.log(e); error("cannot access camera", true); } } function previewfile(content, type, name) { if (!name) name = guessfilename(type); if (type.match('^image/')) { var img = document.createElement("img"); img.onload = function() { // resize image to maximum 400px 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({name: name, type: type, content: img.src}); $("#preview").append(img); success('image is ready to be sent'); } img.src = canvas.toDataURL(file.type); img.title = name+"\n"+size(img.src.length); } img.src=content; } else if (type.match('^video/')) { filecontent.push({name: name, type: type, content: content}); var video = document.createElement("video"); video.setAttribute("controls", "controls"); video.setAttribute("loop", "loop"); video.setAttribute("src", content); video.setAttribute("title", name+"\n"+size(content.length)); $("#preview").append(video); } else { filecontent.push({name: name, type: type, content: content}); var img = document.createElement("img"); img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg"; img.title = name+"\n"+size(content.length); $("#preview").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 …"); previewfile(evt.target.result, file.type, file.name); } 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(); $("#allusers").empty(); userlist.forEach(function(usr) { userMap[usr.name] = usr.pubkey; $("#allusers").append('