Fully end to end encrypted anonymous chat program. Server only stores public key lookup for users and the encrypted messages. No credentials are transfered to the server, but kept in local browser storage. This allows 100% safe chatting.
https://safechat.ch
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1262 lines
37 KiB
1262 lines
37 KiB
/*! @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 log() { |
|
//[].push.call(arguments, (new Error()).stack) |
|
console.log.apply(null, arguments) |
|
} |
|
|
|
function SafeChat() { |
|
|
|
/// Create UID from a name by appending an E-Mail |
|
function uid(name) { |
|
return name+' <'+mail(name)+'>' |
|
} |
|
|
|
function mail(name) { |
|
var hostname = window.location.hostname!='localhost'?window.location.hostname:'safechat.ch' |
|
return name+'@'+hostname |
|
} |
|
|
|
//============================================================================== |
|
/// @class Crypto cryptographic functions |
|
/** @param view is of class SafeChat.View */ |
|
function Crypto(controller) { |
|
|
|
/// cache client's key from local strorage |
|
var k = null |
|
|
|
/// detect hostname, default to safechat.ch |
|
/// get user key |
|
/** @internal key ist cached in k |
|
@return key */ |
|
this.key = function() { |
|
if (k) return k // cached key |
|
if (typeof localStorage.privkey === 'undefined') return null |
|
return k = openpgp.key.readArmored(localStorage.privkey) |
|
} |
|
|
|
/// get own user name |
|
/** get user name as user id of first public key */ |
|
this.user = function() { |
|
if (k || key()) return k.pub.keys[0].getUserIds()[0] |
|
return null |
|
} |
|
|
|
/// open private key with password |
|
/** @return @c true if password matches */ |
|
function password(pwd) { |
|
return (k || keys()) && k.keys[0].decrypt(pwd) |
|
} |
|
|
|
/// Encrypt Message |
|
function encrypt(message, targetkeys, done, failed) { |
|
if (!k) return false |
|
openpgp.encrypt({ |
|
publicKeys: targetkeys.keys.concat(k.keys), |
|
privateKeys: k, |
|
data: message, |
|
armor: false |
|
}).then(done).else(failed) |
|
return true |
|
} |
|
|
|
/// Decrypt Message |
|
function decrypt(message, sourcekeys, done, failed) { |
|
if (!k) return false |
|
openpgp.decrypt({ |
|
privateKeys: k.keys, |
|
publicKeys: sourcekeys.keys, |
|
message: message |
|
}).then(done).else(failed) |
|
return true |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
if (openpgp.initWorker("openpgp.worker.min.js")) |
|
log("asynchronous openpgp enabled") |
|
else |
|
log("asynchronous openpgp failed") |
|
|
|
} |
|
|
|
//============================================================================== |
|
/// database that stores in indexed db |
|
function DataBase() { |
|
|
|
function user(name, key) { |
|
} |
|
|
|
} |
|
|
|
//============================================================================== |
|
/// manage local copy of users |
|
function Users() { |
|
|
|
var users = new Map() |
|
|
|
function add(usr) { |
|
if (!users[usr.name]) |
|
users[usr.name].valid = true |
|
else |
|
users[usr.name].valid = users[usr.name].valid && users[usr.name].key == usr.key |
|
users[usr.name].key = usr.key |
|
users[usr.name].online = usr.online |
|
} |
|
|
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
/// manage local copy of messages |
|
function Messages() { |
|
|
|
var messages = {}; |
|
|
|
function add() { |
|
} |
|
|
|
} |
|
|
|
//============================================================================== |
|
/// @class Communication client socket communication |
|
/** @param view is of class SafeChat.View */ |
|
function Communication(controller) { |
|
|
|
var socket = io.connect() |
|
|
|
function broadcast(signal, data) { |
|
log(' function broadcast(signal, data)') |
|
log("<=snd "+signal) |
|
socket.broadcast.emit(signal, data) |
|
} |
|
|
|
function emit(signal, data, next) { |
|
log(' function emit(signal, data, next)') |
|
log("<-snd "+signal) |
|
socket.emit(signal, data, next) |
|
} |
|
|
|
this.lookup = function(usr, next) { |
|
log(' this.lookup = function(usr, next)') |
|
emit('user', usr, next) |
|
} |
|
|
|
socket |
|
.on("login", controller.loggedin) |
|
.on("fail", controller.fail) |
|
.on("user", controller.user) |
|
.on("users", controller.users) |
|
.on("message", controller.message) |
|
.on("messages", controller.messages) |
|
.io |
|
.on("connect", controller.connected) |
|
.on("reconnect", controller.connected) |
|
.on("disconnect", controller.disconnected) |
|
.on("error", controller.disconnected) |
|
|
|
} |
|
|
|
//============================================================================== |
|
/// @class View provides the glue to the GUI in the index.ejs file |
|
/** View provides the following callbacks: |
|
- status updates: |
|
- @c notice(msg) to display information |
|
- @c warning(msg) |
|
- @c error(msg) |
|
- @c fatal(msg) */ |
|
function View() { |
|
|
|
var nexttimer = null |
|
|
|
/// Padding for numbers in dates |
|
function pad(n) { |
|
log(' function pad(n)') |
|
return n<10 ? '0'+n : n |
|
} |
|
|
|
/// escape text to show in html @see htmldec |
|
function htmlenc(html) { |
|
log(' function htmlenc(html)') |
|
return $('<div/>').text(html).html() |
|
} |
|
|
|
/// decode html encoded text @see htmlenc |
|
function htmldec(data) { |
|
log(' function htmldec(data)') |
|
return $('<div/>').html(data).text() |
|
} |
|
|
|
/// alert user accoustically or by vibration |
|
/** alert user, e.g. that a new message has arrived. */ |
|
function beep() { |
|
log(' function beep()') |
|
if (navigator.vibrate) navigator.vibrate(1000) |
|
(new Audio("sounds/beep.mp3")).play() |
|
} |
|
|
|
/// show fatal error |
|
/** something completely failed, abort |
|
@param msg */ |
|
function fatal(msg) { |
|
log(' function fatal(msg)') |
|
if (nexttimer) clearTimeout(nexttimer) |
|
if (msg) { |
|
error(msg) |
|
$('#fatal-msg').html(msg) |
|
} |
|
show('fatal') |
|
} |
|
|
|
/// show error messsage |
|
/** shows 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 next (optional) next function to call */ |
|
function error(data, next) { |
|
log(' function error(data, next)') |
|
if (nexttimer) clearTimeout(nexttimer) |
|
$("#status").hide() |
|
$("#status").addClass("error") |
|
$("#status").removeClass("notice") |
|
$("#status").removeClass("success") |
|
if (data) { |
|
if (typeof data == 'string') { |
|
$("#status").html(data) |
|
log("error: "+data) |
|
} else { |
|
$("#status").html('error') |
|
log("error: "+JSON.stringify(data)) |
|
} |
|
} else { |
|
$("#status").html('error') |
|
log("error") |
|
} |
|
$("#status").show() |
|
if (next) nexttimer = setTimeout(function() { |
|
nexttimer = null |
|
next() |
|
}, 5000) |
|
} |
|
|
|
/// show notice messsage |
|
/** shows an notice message and logs to console. |
|
@param text (optional) The data is a string. */ |
|
function notice(text) { |
|
log(' function notice(text)') |
|
$("#status").hide() |
|
$("#status").addClass("notice") |
|
$("#status").removeClass("error") |
|
$("#status").removeClass("success") |
|
if (text) { |
|
$("#status").html(text) |
|
log("notice: "+text) |
|
} else { |
|
$("#status").html('') |
|
log("notice") |
|
} |
|
$("#status").show() |
|
} |
|
|
|
/// show success messsage |
|
/** shows an success message and logs to console. |
|
@param text (optional) The data is a string. */ |
|
function success(text) { |
|
log(' function success(text)') |
|
$("#status").hide() |
|
$("#status").addClass("success") |
|
$("#status").removeClass("error") |
|
$("#status").removeClass("notice") |
|
if (text) { |
|
$("#status").html(text) |
|
log("success: "+text) |
|
} else { |
|
$("#status").html('') |
|
log("success") |
|
} |
|
$("#status").show() |
|
} |
|
|
|
/// show a specific screen given the element id |
|
/** @param id html id to be shown. |
|
@param msg (optional) the success message text */ |
|
function show(id, msg) { |
|
log(' function show(id, msg)') |
|
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() |
|
} |
|
|
|
/// show server connected status |
|
function connected() { |
|
log(' function connected()') |
|
log("server connected") |
|
$("#connectionstatus #bad").hide() |
|
$("#connectionstatus #good").show() |
|
success("server connected") |
|
} |
|
|
|
/// show server disconnected status |
|
function disconnected() { |
|
log(' function disconnected()') |
|
log("server disconnected") |
|
$("#connectionstatus #good").hide() |
|
$("#connectionstatus #bad").show() |
|
error("server disconnected", true) |
|
} |
|
|
|
/// toggle menu display |
|
this.togglemenu = function() { |
|
log(' function togglemenu()') |
|
$("#menu").toggle() |
|
} |
|
|
|
function checkFeature(id, query) { |
|
log(' function checkFeature(id, query)') |
|
if (query) $('#'+id) |
|
.css('color', 'green') |
|
.prepend('<span>✔</span>') |
|
else $('#'+id) |
|
.css('color', 'red') |
|
.prepend('<span>✘</span>') |
|
} |
|
|
|
this.checkFeatures = function() { |
|
log(' this.checkFeatures = function()') |
|
$('ul.features').css('list-style-type', 'none') |
|
checkFeature("localstorage", Storage) |
|
checkFeature("indexeddb", window.indexedDB) |
|
checkFeature("randomvalues", window.crypto.getRandomValues) |
|
checkFeature("vibrate", navigator.vibrate) |
|
checkFeature("filereader", window.FileReader) |
|
} |
|
|
|
/// @name create new user |
|
/// @{ |
|
|
|
this.newuser = function() { |
|
log(' this.newuser = function()') |
|
show('newuser') |
|
} |
|
|
|
var user = null |
|
var pwd = false |
|
|
|
function invalid(usr) { |
|
log(' function invalid(usr)') |
|
return !user || !user.exists && user.name.length<3 |
|
} |
|
|
|
this.available = function(usr) { |
|
log(' this.available = function(usr)') |
|
user = usr |
|
log("props:", invalid(user) || !pwd) |
|
$("#createuser").prop(":disabled", invalid(user) || !pwd) |
|
if (user.length==0) |
|
notice("please chose a user name") |
|
else if (user.length<3) |
|
notice("please chose a longer user name") |
|
else if (user.exists) |
|
notice("user name is already in use") |
|
else if (!pwd) |
|
notice("please chose a password") |
|
else |
|
success("user is ready to be created") |
|
} |
|
|
|
this.passwords = function(pwd1, pwd2) { |
|
log(' this.passwords = function(pwd1, pwd2)') |
|
return |
|
pwd = pwd1==pwd2 && pwd1.length>5 |
|
log("props:", invalid(user) || !pwd) |
|
$("#createuser").prop(":disabled", invalid(user) || !pwd) |
|
if (pwd1.length==0) |
|
notice('please chose a password') |
|
else if (pwd1.length<6) |
|
notice('please chose a longer password') |
|
else if (pwd1 != pwd2) |
|
notice("passwords don't match") |
|
else if (invalid(user)) |
|
notice("please chose a user name") |
|
else |
|
success("user is ready to be created") |
|
} |
|
|
|
/// @} |
|
|
|
function DataTransfer() { |
|
log(' function DataTransfer()') |
|
|
|
var reboottimer = null |
|
var data = new DataTransfer() |
|
|
|
/// 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) { |
|
log(' function restore(evt)') |
|
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") |
|
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") |
|
log("reboot after restore in 2s") |
|
if (!reboottimer && reboot) reboottimer = setTimeout(function() { |
|
reboottimer = null |
|
}, 2000) |
|
} |
|
reader.readAsText(file) |
|
} |
|
} |
|
|
|
if (!window.FileReader) { |
|
$('restore-menu-item').hide() |
|
error("your browser does not support file upload") |
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
//============================================================================== |
|
/// @class Controller defines the programm flow |
|
function Controller(view) { |
|
log(' function Controller(view)') |
|
|
|
var db = new DataBase() |
|
var crypto = new Crypto(this) |
|
var communication = new Communication(this) |
|
var users = new Users() |
|
|
|
/// @name error handling |
|
/// @{ |
|
|
|
this.fatal = view.fatal |
|
this.error = view.error |
|
this.warning = view.warning |
|
this.notice = view.notice |
|
|
|
/// @} |
|
|
|
/// @name access to view |
|
/// @{ |
|
|
|
this.togglemenu = view.togglemenu |
|
|
|
/// @} |
|
|
|
/// @name signals from server |
|
/// @{ |
|
|
|
function fail(msg) { |
|
log(' function fail(msg)') |
|
log('rcv-> fail('+msg+')') |
|
error(msg) |
|
} |
|
|
|
function loggedin() { |
|
log(' function loggedin()') |
|
log("rcv-> login") |
|
success("login successful") |
|
chat() |
|
} |
|
|
|
function user(usr) { |
|
log(' function user(usr)') |
|
log("rcv-> user") |
|
if (usr.exits) users.add(usr) |
|
} |
|
|
|
function users() { |
|
log(' function users()') |
|
log("rcv-> users") |
|
} |
|
|
|
function message(msg) { |
|
log(' function message(msg)') |
|
log("rcv-> message") |
|
} |
|
|
|
function messages(msgs) { |
|
log(' function messages(msgs)') |
|
log("rcv-> messages") |
|
} |
|
|
|
this.connected = view.connected |
|
this.reconnect = view.connected |
|
this.disconnect = view.disconnected |
|
this.error = view.disconnected |
|
|
|
// @} |
|
|
|
/// @name signals from view |
|
/// @{ |
|
|
|
/// @name new user registration |
|
/// @{ |
|
|
|
this.lookup = function(usr) { |
|
log(' this.lookup = function(usr)') |
|
return |
|
if (usr.length > 2) communication.lookup(uid(usr), function(res) { |
|
view.available(res) |
|
}) |
|
} |
|
|
|
this.checkpasswords = view.passwords |
|
|
|
this.createuser = function(name, pwd) { |
|
log(' this.createuser = function(name, pwd)') |
|
crypto.createuser(name, name+'@'+hostname, pwd).then(function() { |
|
if (!crypto.password(pwd)) |
|
fatal("private key decryption failed") |
|
else |
|
chat() |
|
}) |
|
} |
|
|
|
/// @} |
|
|
|
/// @} |
|
|
|
function initBrowser() { |
|
log(' function initBrowser()') |
|
log('A') |
|
//window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB |
|
log('B') |
|
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction |
|
log('C') |
|
window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange |
|
log('D') |
|
navigator.vibrate = navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate |
|
log('E') |
|
log(' end of function initBrowser()') |
|
return window.indexedDB && window.crypto.getRandomValues && Storage |
|
} |
|
|
|
var newuser = view.newuser |
|
|
|
function chat() { |
|
log(' function chat()') |
|
} |
|
|
|
function password() { |
|
log(' function password()') |
|
} |
|
|
|
function login() { |
|
log(' function login()') |
|
if (!crypto.key()) newuser(); else password(); |
|
} |
|
|
|
this.run = function() { |
|
log(' this.run = function()') |
|
login() |
|
} |
|
|
|
this.start = function() { |
|
log(' this.start = function()') |
|
view.reboot = this.run |
|
var compatible = initBrowser() |
|
view.checkFeatures() |
|
if (!compatible) { |
|
log('incompatible') |
|
view.fatal("your browser is not supported") |
|
} else { |
|
log('incompatible') |
|
this.run() |
|
} |
|
} |
|
|
|
} |
|
|
|
//============================================================================== |
|
return new Controller(new View()) |
|
|
|
} |
|
|
|
//============================================================================== |
|
//------------------------------------------------------------------------------ |
|
|
|
var filecontent = new Array() ///< temporary storage for attachments |
|
var reboottimer = null |
|
|
|
function connectionstatus() { |
|
log('function connectionstatus()') |
|
if (socket.connected) connected(); else disconnected(); |
|
} |
|
|
|
/// Configure local groups |
|
/** … */ |
|
function groups() { |
|
log('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) { |
|
log('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) { |
|
log('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) { |
|
log('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) { |
|
log(e) |
|
error("generating key pairs failed") |
|
}) |
|
} |
|
|
|
/// Get Own Public Key |
|
/** @return public key object */ |
|
function publicKey() { |
|
log('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() { |
|
log('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() { |
|
log('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() { |
|
log('function clearmessage()') |
|
$("#message").prop(":disabled", true) |
|
filecontent = new Array() |
|
$('#preview').empty() |
|
$("#msg").val("") |
|
$("#message").prop(":disabled", false) |
|
} |
|
|
|
function guessfilename(mimetype, user, date) { |
|
log('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) { |
|
log('function attachments(files, id, from, date)') |
|
if (files) files.forEach(function(file) { |
|
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() { |
|
log('function done()') |
|
if (recorder) { |
|
recorder.stop() |
|
recorder.recording(function(data) { |
|
previewfile(data, "video/webm") |
|
abort() |
|
}) |
|
} |
|
} |
|
|
|
function abort() { |
|
log('function abort()') |
|
if (recorder) { |
|
$("#videorecorder").hide() |
|
recorder.release() |
|
delete recorder |
|
recorder = null |
|
} |
|
} |
|
|
|
/// Record Video from builtin camera |
|
function recordvideo() { |
|
log('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) { |
|
log(e) |
|
error("cannot access camera", true) |
|
} |
|
} |
|
|
|
function previewfile(content, type, name) { |
|
log('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) { |
|
log('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) { |
|
log('function setreceiver(name)') |
|
$("#recv").val(name) |
|
checkpartner(name) |
|
$("#msg").focus() |
|
} |
|
|
|
var userMap = null |
|
function users(userlist) { |
|
log('function users(userlist)') |
|
log("rcv-> users") |
|
userMap = new Array() |
|
$("#allusers").empty() |
|
userlist.forEach(function(usr) { |
|
userMap[usr.name] = usr.pubkey |
|
$("#allusers").append('<option value="'+htmlenc(usr.name)+'">') |
|
$("#allusers").hide() |
|
log(" user: "+usr.name) |
|
}) |
|
localStorage.userMap = JSON.stringify(userMap) |
|
} |
|
|
|
function user(usr) { |
|
log('function user(usr)') |
|
if (usr.exists) log("rcv-> user("+usr.name+")") |
|
else log("rcv-> user("+usr.name+"): name is available") |
|
if ($("#newuser").is(":visible") && usr.name==uid($('#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==uid($("#recv").val())) { // same username as in receiver |
|
$('#send').prop("disabled", !usr.exists) |
|
$("label[for=send] img").css("opacity", usr.exists?"1.0":"0.4") |
|
$("label[for=send] img").css("filter", usr.exists?"alpha(opacity=100)":"alpha(opacity=40)") |
|
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 |
|
$("#allusers").append('option value="'+htmlenc(usr.name)+'"') |
|
localStorage.userMap = JSON.stringify(userMap) |
|
} |
|
} |
|
|
|
function queryuser(usr) { |
|
log('function queryuser(usr)') |
|
log("query user: "+uid(usr)) |
|
socket.emit("user", uid(usr)) |
|
} |
|
|
|
/// Get a user's public key. |
|
/** The first time, gets it from the server, later from the cache. */ |
|
function getPublicKey(user) { |
|
log('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(msgs) { |
|
log('function messages(msgs)') |
|
log("rcv-> messages("+msgs.length+")") |
|
if (!password || !privateKey()) |
|
return setTimeout(function() {emit("messages")}, 1000) // try again later |
|
show("allmessages") |
|
notice("load messages, please wait …") |
|
msgs.forEach(function(msg) {message(msg, true)}) |
|
show("chat") |
|
} |
|
|
|
/// Received a message from server |
|
function message(m, internal) { |
|
log('function message(m, internal)') |
|
if (!internal) 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.decrypt({ |
|
privateKeys: privkey, |
|
publicKeys: key.keys, |
|
message: message |
|
}).then(function(msg) { // decryption succeded |
|
openpgp.decrypt({ |
|
privateKeys: privkey, |
|
publicKeys: key.keys, |
|
message: 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('<div id="id'+(m.id)+'" class="msg '+ |
|
(m.user==userid()?"me":"other")+ |
|
'"><div class="header">'+ |
|
'<span class="date">'+ |
|
(new Date(m.time)).toLocaleString()+ |
|
'</span><span class="sender">'+ |
|
'<a href="javascript:void(0)" '+ |
|
'onclick="setreceiver(this.innerHTML)">'+ |
|
htmlenc(m.user)+ |
|
'</a>'+(message.receiver?' → <a href="javascript:void(0)" '+ |
|
'onclick="setreceiver(this.innerHTML)">' |
|
+htmlenc(message.receiver)+'</a>':"")+ |
|
'</span></div>'+ |
|
'<div class="text">'+ |
|
htmlenc(message.text)+ |
|
'</div></div><div class="clear"/>') |
|
// show attachments |
|
attachments(message.files, '#id'+m.id+' .text', m.user, new Date(m.time)) |
|
// calculate and show emoticons |
|
$('#id'+m.id).emoticonize() |
|
if (!internal) 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) { |
|
log('function sendmessage(recv, txt)') |
|
notice("1/3 preparing message …") |
|
$("#message").prop(":disabled", true) |
|
getPublicKey(recv) // get receiver's public key |
|
.done(function(pk) { |
|
var key=openpgp.key.readArmored(pk) |
|
if (!pk||key.err) { |
|
$("#message").prop(":disabled", false) |
|
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.encrypt({publicKeys: key.keys.concat(publicKey().keys), |
|
privateKeys: privkey, |
|
data: message, |
|
armor: false}) |
|
.then(function(msg) { // message is encrypted |
|
notice("3/3 sending message …") |
|
emit("message", {user: userid(), content: msg}) |
|
clearmessage() |
|
}) |
|
.catch(function(e) { |
|
$("#message").prop(":disabled", false) |
|
error("encryption of message failed", true) |
|
}) |
|
}) |
|
.fail(function(e) { |
|
$("#message").prop(":disabled", false) |
|
error("user not found", true) |
|
}) |
|
} |
|
|
|
/// 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) { |
|
log('function setpw(pwd)') |
|
return |
|
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() { |
|
log('function getpwd()') |
|
if (password) return |
|
$("#removeKey").show() |
|
show("getpwd") |
|
} |
|
|
|
function deleteUser() { |
|
log('function deleteUser()') |
|
var uid = userid() |
|
localStorage.removeItem(pubkey) |
|
localStorage.removeItem(privkey) |
|
error("user "+uid+" permanentely lost") |
|
} |
|
|
|
function removeKey() { |
|
log('function removeKey()') |
|
togglemenu() |
|
$("#removeKey").hide() |
|
show('forgotpassword') |
|
} |
|
|
|
/// Main Chat Window |
|
/** Gets chat widgets from server and displays them. Starts timer for |
|
get() which polls for new messages. */ |
|
var firsttime = true |
|
function chat() { |
|
log('function chat()') |
|
if (!password) return getpwd() |
|
show("chat") |
|
if (firsttime && $('#msgs').is(':empty')) { |
|
firsttime = false |
|
notice("getting previous messages, please wait …") |
|
emit("messages") |
|
} |
|
} |
|
|
|
/// 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() { |
|
log('function login()') |
|
$("#username").html(userid()+"@"+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() { |
|
log('function newuser()') |
|
show("newuser") |
|
} |
|
|
|
/// Check if local storage is available |
|
function checkLocalStorage() { |
|
log('function checkLocalStorage()') |
|
var test = 'test' |
|
try { |
|
localStorage.setItem(test, test) |
|
localStorage.removeItem(test) |
|
return true |
|
} catch(e) { |
|
show("nolocalstorage") |
|
error("local storage not available") |
|
} |
|
return false |
|
} |
|
|
|
/// Initial Function: Startup |
|
/** Decide whether to login or to create a new user */ |
|
function start() { |
|
log('function start()') |
|
$("#menu").hide() |
|
//show("startup") |
|
if (checkLocalStorage()) |
|
try { |
|
if (!userid()) { |
|
newuser() |
|
} else { |
|
login() |
|
} |
|
} catch (m) { |
|
log(m.stack) |
|
error(m) |
|
} |
|
} |
|
|
|
var safechat = new SafeChat() |
|
|
|
function init() { |
|
log('function init()') |
|
safechat.start() |
|
} |
|
|
|
function old() { |
|
log('function old()') |
|
/// 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) |
|
connectionstatus() |
|
if (openpgp.initWorker("openpgp.worker.min.js")) |
|
log("asynchronous openpgp enabled") |
|
else |
|
log("asynchronous openpgp failed") |
|
emit('users') |
|
start() |
|
} |
|
|
|
/// Start Main Loop |
|
$(init)
|
|
|