about to be able to create user

master
Marc Wäckerlin 8 years ago
parent d2a6a0f518
commit d6beeefe74
  1. 1123
      nodejs/public/javascripts/safechat.js
  2. 2
      nodejs/public/stylesheets/safechat.css
  3. 17
      nodejs/sockets/index.js
  4. 23
      nodejs/views/index.ejs

@ -41,9 +41,15 @@ function SafeChat() {
/// Create UID from a name by appending an E-Mail /// Create UID from a name by appending an E-Mail
function uid(name) { function uid(name) {
return name+' <'+name+'@'+hostname+'>' 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 /// @class Crypto cryptographic functions
/** @param view is of class SafeChat.View */ /** @param view is of class SafeChat.View */
function Crypto(controller) { function Crypto(controller) {
@ -51,42 +57,23 @@ function SafeChat() {
/// cache client's key from local strorage /// cache client's key from local strorage
var k = null var k = null
/// detect hosstname, default to safechat.ch /// detect hostname, default to safechat.ch
var hostname = window.location.hostname!='localhost'?window.location.hostname:'safechat.ch'
/// get user key /// get user key
/** @internal key ist cached in k /** @internal key ist cached in k
@return key */ @return key */
function key() { this.key = function() {
if (k) return k if (k) return k // cached key
if (typeof localStorage.key == 'undefined') return null if (typeof localStorage.privkey === 'undefined') return null
return k = openpgp.key.readArmored(localStorage.key) return k = openpgp.key.readArmored(localStorage.privkey)
} }
/// get own user name /// get own user name
/** get user name as user id of first public key */ /** get user name as user id of first public key */
function user() { this.user = function() {
if (k || key()) return k.pub.keys[0].getUserIds()[0] if (k || key()) return k.pub.keys[0].getUserIds()[0]
return null return null
} }
/// create New User
function createuser(user, email, pwd) {
controller.notice("generating keys")
openpgp.generateKey({
numBits: 4096,
userIds: [{name: user, email: email}],
passphrase: pwd
}).then(function(keyPair) {
controller.success("keys generated")
localStorage.key = keyPair.privateKeyArmored
k = keyPair.key
}).catch(function(e) {
console.log(e)
controller.fatal("generating key pairs failed")
})
}
/// open private key with password /// open private key with password
/** @return @c true if password matches */ /** @return @c true if password matches */
function password(pwd) { function password(pwd) {
@ -116,8 +103,15 @@ function SafeChat() {
return true return true
} }
//------------------------------------------------------------------------------
if (openpgp.initWorker("openpgp.worker.min.js"))
console.log("asynchronous openpgp enabled")
else
console.log("asynchronous openpgp failed")
} }
//==============================================================================
/// database that stores in indexed db /// database that stores in indexed db
function DataBase() { function DataBase() {
@ -126,6 +120,7 @@ function SafeChat() {
} }
//==============================================================================
/// manage local copy of users /// manage local copy of users
function Users() { function Users() {
@ -142,6 +137,7 @@ function SafeChat() {
} }
//------------------------------------------------------------------------------
/// manage local copy of messages /// manage local copy of messages
function Messages() { function Messages() {
@ -152,6 +148,7 @@ function SafeChat() {
} }
//==============================================================================
/// @class Communication client socket communication /// @class Communication client socket communication
/** @param view is of class SafeChat.View */ /** @param view is of class SafeChat.View */
function Communication(controller) { function Communication(controller) {
@ -163,9 +160,13 @@ function SafeChat() {
socket.broadcast.emit(signal, data) socket.broadcast.emit(signal, data)
} }
function emit(signal, data) { function emit(signal, data, next) {
console.log("<-snd "+signal) console.log("<-snd "+signal)
socket.emit(signal, data) socket.emit(signal, data, next)
}
this.lookup = function(usr, next) {
emit('user', usr, next)
} }
socket socket
@ -183,6 +184,7 @@ function SafeChat() {
} }
//==============================================================================
/// @class View provides the glue to the GUI in the index.ejs file /// @class View provides the glue to the GUI in the index.ejs file
/** View provides the following callbacks: /** View provides the following callbacks:
- status updates: - status updates:
@ -299,7 +301,7 @@ function SafeChat() {
@param msg (optional) the success message text */ @param msg (optional) the success message text */
function show(id, msg) { function show(id, msg) {
console.log("state: "+id) console.log("state: "+id)
if (msg) success(msg) else $("#status").hide() if (msg) success(msg); else $("#status").hide();
$("#main").children(":not(#"+id+")").hide() $("#main").children(":not(#"+id+")").hide()
$("#main #"+id).show() $("#main #"+id).show()
$("#main #"+id+" form input:first-child").focus() $("#main #"+id+" form input:first-child").focus()
@ -327,21 +329,15 @@ function SafeChat() {
} }
function checkFeature(id, query) { function checkFeature(id, query) {
if (query) $('#'+id+':before')
.css('color', 'green')
.css('content', '&#x2714;')
else $('#'+id+':before')
.css('color', 'red')
.css('content', '&#x2718;')
if (query) $('#'+id) if (query) $('#'+id)
.css('color', 'green') .css('color', 'green')
.css('text-decoration', 'line-through') .prepend('<span>&#x2714;</span>')
else $('#'+id) else $('#'+id)
.css('color', 'red') .css('color', 'red')
.css('text-decoration', 'none') .prepend('<span>&#x2718;</span>')
} }
function checkFeatures() { this.checkFeatures = function() {
$('ul.features').css('list-style-type', 'none') $('ul.features').css('list-style-type', 'none')
checkFeature("localstorage", Storage) checkFeature("localstorage", Storage)
checkFeature("indexeddb", window.indexedDB) checkFeature("indexeddb", window.indexedDB)
@ -350,6 +346,54 @@ function SafeChat() {
checkFeature("filereader", window.FileReader) checkFeature("filereader", window.FileReader)
} }
/// @name create new user
/// @{
this.newuser = function() {
show('newuser')
}
var user = null
var pwd = false
function invalid(usr) {
return !user || !user.exists && user.name.length<3
}
this.available = function(usr) {
user = usr
console.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) {
pwd = pwd1==pwd2 && pwd1.length>5
console.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() { function DataTransfer() {
var reboottimer = null var reboottimer = null
@ -405,10 +449,11 @@ function SafeChat() {
} }
//==============================================================================
/// @class Controller defines the programm flow /// @class Controller defines the programm flow
function Controller(view) { function Controller(view) {
var db = new Database() var db = new DataBase()
var crypto = new Crypto(this) var crypto = new Crypto(this)
var communication = new Communication(this) var communication = new Communication(this)
var users = new Users() var users = new Users()
@ -461,6 +506,33 @@ function SafeChat() {
// @} // @}
/// @name signals from view
/// @{
/// @name new user registration
/// @{
this.lookup = function(usr) {
if (usr.length > 2) communication.lookup(uid(usr), function(res) {
view.available(res)
})
}
this.checkpasswords = view.passwords
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() { function initBrowser() {
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction
@ -469,242 +541,248 @@ function SafeChat() {
return window.indexedDB && window.crypto.getRandomValues && Storage return window.indexedDB && window.crypto.getRandomValues && Storage
} }
function register() { var newuser = view.newuser
function chat() {
}
function password() {
} }
function login() { function login() {
if (!crypto.key()) register() if (!crypto.key()) newuser(); else password();
else password()
} }
function run() { this.run = function() {
login() login()
} }
function start() { this.start = function() {
view.reboot = run view.reboot = this.run
var compatible = initBrowser() var compatible = initBrowser()
view.checkFeatures() view.checkFeatures()
if (!compatible) if (!compatible)
view.fatal("your browser is not supported") view.fatal("your browser is not supported")
else else
run() this.run()
} }
} }
//============================================================================== //==============================================================================
//------------------------------------------------------------------------------
return new Controller(new View()) return new Controller(new View())
} }
var filecontent = new Array() ///< temporary storage for attachments
var reboottimer = null
function connectionstatus() {
if (socket.connected) connected() else disconnected()
}
/// 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.
var filecontent = new Array() ///< temporary storage for attachments
var reboottimer = null
Called when user edits the password fields. function connectionstatus() {
if (socket.connected) connected(); else disconnected();
}
Sets @ref username and checks @ref password - if both are well /// Configure local groups
defined, enables the submit button. /** … */
function groups() {
}
@param pwd The password. /// Check if password is set and matches the repeated password
@param pwd2 The repeated password. */ /** Checks if both passwords are identical and valid and gives
function checkpwd(pwd, pwd2) { feedback to the user.
$("#register").submit(function(event) {
return false Called when user edits the password fields.
})
if (pwd==pwd2) password=pwd Sets @ref username and checks @ref password - if both are well
else password=null defined, enables the submit button.
if (!password||password.length<1) password=null
$("#createuser").prop("disabled", !(username && password)) @param pwd The password.
if (password) { @param pwd2 The repeated password. */
if (username) success("user is ready to be created") 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 notice("password matches, please chose a valid user name")
} else { } else {
if (username) notice("passwords don't match") if (username) notice("passwords don't match")
else if ($('#user').val()) notice("user name is already in use") else if ($('#user').val()) notice("user name is already in use")
else notice("please chose a user name") else notice("please chose a user name")
}
} }
}
/// Checks if the receiver of a message exists on server. /// Checks if the receiver of a message exists on server.
/** Calls checknewuser.php on server and enables the message submit /** Calls checknewuser.php on server and enables the message submit
button if the receiver of the message exists on the server. */ button if the receiver of the message exists on the server. */
function checkpartner(user) { function checkpartner(user) {
$("#chat").submit(function(event) { $("#chat").submit(function(event) {
return false return false
}) })
emit("user", uid(user)) emit("user", uid(user))
} }
/// Create Local Public-/Private-Key Pair /// Create Local Public-/Private-Key Pair
/** Called if user has not yet his keys, just generates a new key pair. */ /** Called if user has not yet his keys, just generates a new key pair. */
function createkeypair(user, pwd) { function createkeypair(user, pwd) {
notice("generating keys") notice("generating keys")
openpgp.generateKey({ openpgp.generateKey({
numBits: 4096, numBits: 4096,
userIds: [{name: user, email: user+'@'+hostname}], userIds: [{name: user, email: user+'@'+hostname}],
passphrase: pwd passphrase: pwd
}).then(function(keyPair) { }).then(function(keyPair) {
success("keys generated") success("keys generated")
localStorage.pubkey = keyPair.publicKeyArmored localStorage.pubkey = keyPair.publicKeyArmored
localStorage.privkey = keyPair.privateKeyArmored localStorage.privkey = keyPair.privateKeyArmored
login() login()
}).catch(function(e) { }).catch(function(e) {
console.log(e) console.log(e)
error("generating key pairs failed") error("generating key pairs failed")
}) })
} }
/// Get Own Public Key /// Get Own Public Key
/** @return public key object */ /** @return public key object */
function publicKey() { function publicKey() {
if (typeof localStorage.pubkey == 'undefined') { if (typeof localStorage.pubkey == 'undefined') {
if (typeof localStorage.pubKey == 'undefined') { if (typeof localStorage.pubKey == 'undefined') {
return null return null
} else { } else {
localStorage.pubkey = localStorage.pubKey localStorage.pubkey = localStorage.pubKey
localStorage.removeItem(pubKey) localStorage.removeItem(pubKey)
}
} }
return openpgp.key.readArmored(localStorage.pubkey)
} }
return openpgp.key.readArmored(localStorage.pubkey)
}
/// Get Own Private Key /// Get Own Private Key
/** @return private key object */ /** @return private key object */
function privateKey() { function privateKey() {
if (typeof localStorage.privkey == 'undefined') { if (typeof localStorage.privkey == 'undefined') {
if (typeof localStorage.privKey == 'undefined') { if (typeof localStorage.privKey == 'undefined') {
return null return null
} else { } else {
localStorage.privkey = localStorage.privKey localStorage.privkey = localStorage.privKey
localStorage.removeItem(privKey) localStorage.removeItem(privKey)
}
} }
return openpgp.key.readArmored(localStorage.privkey)
} }
return openpgp.key.readArmored(localStorage.privkey)
}
/// Get Own User Name /// Get Own User Name
/** Get user name as user id of first public key */ /** Get user name as user id of first public key */
function userid() { function userid() {
if (!publicKey() || if (!publicKey() ||
publicKey().keys.length < 1 || publicKey().keys.length < 1 ||
publicKey().keys[0].getUserIds().length < 1) return null publicKey().keys[0].getUserIds().length < 1) return null
return publicKey().keys[0].getUserIds()[0] return publicKey().keys[0].getUserIds()[0]
} }
/// 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) $("#message").prop(":disabled", true)
filecontent = new Array() filecontent = new Array()
$('#preview').empty() $('#preview').empty()
$("#msg").val("") $("#msg").val("")
$("#message").prop(":disabled", false) $("#message").prop(":disabled", false)
} }
function guessfilename(mimetype, user, date) { function guessfilename(mimetype, user, date) {
if (!user) user = userid() if (!user) user = userid()
if (!date) date = new Date() if (!date) date = new Date()
var ext = mimetype.replace(/.*\/(x-)?/i, "") var ext = mimetype.replace(/.*\/(x-)?/i, "")
return pad(date.getFullYear())+pad(date.getMonth()+1)+pad(date.getDate()) return pad(date.getFullYear())+pad(date.getMonth()+1)+pad(date.getDate())
+"-"+ext+"-"+user+"@"+hostname+'.'+ext +"-"+ext+"-"+user+"@"+hostname+'.'+ext
} }
/// Display Image Attachments /// Display Image Attachments
function attachments(files, id, from, date) { function attachments(files, id, from, date) {
if (files) files.forEach(function(file) { if (files) files.forEach(function(file) {
console.log(file) console.log(file)
if (!file.name) file.name = guessfilename(file.type, from, date) if (!file.name) file.name = guessfilename(file.type, from, date)
var a = document.createElement('a') var a = document.createElement('a')
a.href = file.content a.href = file.content
a.download = file.name a.download = file.name
a.target = '_blank' a.target = '_blank'
if (file.type.match('^image/')) { if (file.type.match('^image/')) {
var img = document.createElement('img') var img = document.createElement('img')
img.title = file.name img.title = file.name
img.src = file.content img.src = file.content
a.appendChild(img) a.appendChild(img)
} else if (file.type.match('^video/')) { } else if (file.type.match('^video/')) {
var video = document.createElement('video') var video = document.createElement('video')
video.controls = true video.controls = true
video.title = file.name video.title = file.name
video.src = file.content video.src = file.content
a.appendChild(video) a.appendChild(video)
} else { } else {
var img = document.createElement('img') var img = document.createElement('img')
img.title = file.name img.title = file.name
img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg" img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg"
a.appendChild(img) a.appendChild(img)
} }
$(id).append(a) $(id).append(a)
}) })
} }
var recorder var recorder
function done() { function done() {
if (recorder) { if (recorder) {
recorder.stop() recorder.stop()
recorder.recording(function(data) { recorder.recording(function(data) {
previewfile(data, "video/webm") previewfile(data, "video/webm")
abort() abort()
}) })
}
} }
}
function abort() { function abort() {
if (recorder) { if (recorder) {
$("#videorecorder").hide() $("#videorecorder").hide()
recorder.release() recorder.release()
delete recorder recorder = null delete recorder
} recorder = null
} }
}
/// Record Video from builtin camera /// Record Video from builtin camera
function recordvideo() { function recordvideo() {
try { try {
abort() abort()
$("#videorecorder").show() $("#videorecorder").show()
recorder = new MediaStreamRecorder({ recorder = new MediaStreamRecorder({
video: { video: {
width: {ideal: 180}, width: {ideal: 180},
height: {ideal: 160} height: {ideal: 160}
}, },
audio: true audio: true
}) })
recorder.on("ready", function() { recorder.on("ready", function() {
$("#videorecorder video").attr("src", recorder.preview()) $("#videorecorder video").attr("src", recorder.preview())
$("#videorecorder video").css("width", 180) $("#videorecorder video").css("width", 180)
$("#videorecorder video").css("height", 160) $("#videorecorder video").css("height", 160)
$("#videorecorder video").attr("width", 180) $("#videorecorder video").attr("width", 180)
$("#videorecorder video").attr("height", 160) $("#videorecorder video").attr("height", 160)
recorder.start() recorder.start()
}) })
} catch (e) { } catch (e) {
console.log(e) console.log(e)
error("cannot access camera", true) error("cannot access camera", true)
}
} }
}
function previewfile(content, type, name) { function previewfile(content, type, name) {
if (!name) name = guessfilename(type) if (!name) name = guessfilename(type)
if (type.match('^image/')) { if (type.match('^image/')) {
var img = document.createElement("img") var img = document.createElement("img")
img.onload = function() { // resize image to maximum 400px img.onload = function() { // resize image to maximum 400px
@ -748,334 +826,341 @@ function SafeChat() {
img.title = name+"\n"+size(content.length) img.title = name+"\n"+size(content.length)
$("#preview").append(img) $("#preview").append(img)
} }
} }
/// Upload Attachment /// Upload Attachment
/** Prepares attachment to be sent in a message. If the attachment is /** Prepares attachment to be sent in a message. If the attachment is
an image, it resizes the image to 400px on the lager side. an image, it resizes the image to 400px on the lager side.
By now, only images are supported. By now, only images are supported.
Stores data in global variable @ref filecontent. */ Stores data in global variable @ref filecontent. */
function fileupload(evt) { function fileupload(evt) {
if (!window.FileReader) if (!window.FileReader)
return error("your browser does not support file upload", true) return error("your browser does not support file upload", true)
for (var i=0, f; f=evt.target.files[i]; ++i) { for (var i=0, f; f=evt.target.files[i]; ++i) {
var file = f var file = f
var reader = new FileReader() var reader = new FileReader()
reader.onload = function(evt) { reader.onload = function(evt) {
if (evt.target.error) return error("error reading file", true) if (evt.target.error) return error("error reading file", true)
if (evt.target.readyState==0) return notice("waiting for data …") if (evt.target.readyState==0) return notice("waiting for data …")
if (evt.target.readyState==1) return notice("loading data …") if (evt.target.readyState==1) return notice("loading data …")
previewfile(evt.target.result, file.type, file.name) previewfile(evt.target.result, file.type, file.name)
}
reader.readAsDataURL(file)
} }
reader.readAsDataURL(file)
} }
}
/// Sets Receiver's Name /// Sets Receiver's Name
/** Called when clicked on a receiver's name. Sets focus to the /** Called when clicked on a receiver's name. Sets focus to the
message text field. message text field.
@param name The receiver's name. */ @param name The receiver's name. */
function setreceiver(name) { function setreceiver(name) {
$("#recv").val(name) $("#recv").val(name)
checkpartner(name) checkpartner(name)
$("#msg").focus() $("#msg").focus()
} }
var userMap = null 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() $("#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").append('<option value="'+htmlenc(usr.name)+'">')
$("#allusers").hide() $("#allusers").hide()
console.log(" user: "+usr.name) console.log(" user: "+usr.name)
}) })
localStorage.userMap = JSON.stringify(userMap) localStorage.userMap = JSON.stringify(userMap)
} }
function user(usr) { function user(usr) {
if (usr.exists) console.log("rcv-> user("+usr.name+")") if (usr.exists) console.log("rcv-> user("+usr.name+")")
else console.log("rcv-> user("+usr.name+"): name is available") else console.log("rcv-> user("+usr.name+"): name is available")
if ($("#newuser").is(":visible") && usr.name==uid($('#user').val())) { if ($("#newuser").is(":visible") && usr.name==uid($('#user').val())) {
// same username as in the create user form // same username as in the create user form
$("#createuser").prop("disabled", usr.exists) // todo: check password $("#createuser").prop("disabled", usr.exists) // todo: check password
if (!usr.exists) { if (!usr.exists) {
username = usr.name username = usr.name
success("user name "+usr.name+" is available") success("user name "+usr.name+" is available")
} else { } else {
username = null username = null
error("user name "+usr.name+" is in use", true) 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) if ($("#chat").is(":visible") && usr.name==uid($("#recv").val())) { // same username as in receiver
$("label[for=send] img").css("opacity", usr.exists?"1.0":"0.4") $('#send').prop("disabled", !usr.exists)
$("label[for=send] img").css("filter", usr.exists?"alpha(opacity=100)":"alpha(opacity=40)") $("label[for=send] img").css("opacity", usr.exists?"1.0":"0.4")
if (usr.exists) success("recipient exists") $("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) else error("unknown recipient", true)
} }
if (userMap == null) { if (userMap == null) {
if (localStorage.userMap) { if (localStorage.userMap) {
userMap = JSON.parse(localStorage.userMap) userMap = JSON.parse(localStorage.userMap)
} else { } else {
userMap = new Array() 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)
} }
} }
if (usr.exists && usr.pubkey && userMap[usr.name] != usr.pubkey) {
function queryuser(usr) { userMap[usr.name] = usr.pubkey
console.log("query user: "+uid(usr)) $("#allusers").append('option value="'+htmlenc(usr.name)+'"')
socket.emit("user", uid(usr)) localStorage.userMap = JSON.stringify(userMap)
} }
}
function queryuser(usr) {
console.log("query user: "+uid(usr))
socket.emit("user", uid(usr))
}
/// Get a user's public key. /// Get a user's public key.
/** The first time, gets it from the server, later from the cache. */ /** The first time, gets it from the server, later from the cache. */
function getPublicKey(user) { function getPublicKey(user) {
var deferredObject = $.Deferred() var deferredObject = $.Deferred()
if (userMap && userMap[user]) deferredObject.resolve(userMap[user]) if (userMap && userMap[user]) deferredObject.resolve(userMap[user])
else deferredObject.reject("unknown user") else deferredObject.reject("unknown user")
return deferredObject.promise() return deferredObject.promise()
} }
/// Received a list of messages from server /// Received a list of messages from server
function messages(msgs) { function messages(msgs) {
console.log("rcv-> messages("+msgs.length+")") console.log("rcv-> messages("+msgs.length+")")
if (!password || !privateKey()) if (!password || !privateKey())
return setTimeout(function() {emit("messages")}, 1000) // try again later return setTimeout(function() {emit("messages")}, 1000) // try again later
status("allmessages") show("allmessages")
notice("load messages, please wait …") notice("load messages, please wait …")
msgs.forEach(function(msg) {message(msg, true)}) msgs.forEach(function(msg) {message(msg, true)})
status("chat") show("chat")
} }
/// Received a message from server /// Received a message from server
function message(m, internal) { function message(m, internal) {
if (!internal) console.log("rcv-> message("+m.user+")") if (!internal) console.log("rcv-> message("+m.user+")")
if (!password || !privateKey()) return if (!password || !privateKey()) return
var key=openpgp.key.readArmored(m.pubkey) var key=openpgp.key.readArmored(m.pubkey)
if (key.err) return error("key of sender unreadable", true) if (key.err) return error("key of sender unreadable", true)
var message = openpgp.message.readArmored(m.msg) var message = openpgp.message.readArmored(m.msg)
var privkey = privateKey().keys[0] var privkey = privateKey().keys[0]
if (privkey.decrypt(password)) // prepare own key if (privkey.decrypt(password)) // prepare own key
openpgp.decrypt({
privateKeys: privkey,
publicKeys: key.keys,
message: message
}).then(function(msg) { // decryption succeded
openpgp.decrypt({ openpgp.decrypt({
privateKeys: privkey, privateKeys: privkey,
publicKeys: key.keys, publicKeys: key.keys,
message: message message: message
}).then(function(msg) { // decryption succeded }).then(function(msg) { // decryption succeded
openpgp.decrypt({ // prepend message to list of messages
privateKeys: privkey, var message = JSON.parse(msg.text)
publicKeys: key.keys, $("#msgs") // todo: check msg.signatures[0].valid
message: message .prepend('<div id="id'+(m.id)+'" class="msg '+
}).then(function(msg) { // decryption succeded (m.user==userid()?"me":"other")+
// prepend message to list of messages '"><div class="header">'+
var message = JSON.parse(msg.text) '<span class="date">'+
$("#msgs") // todo: check msg.signatures[0].valid (new Date(m.time)).toLocaleString()+
.prepend('<div id="id'+(m.id)+'" class="msg '+ '</span><span class="sender">'+
(m.user==userid()?"me":"other")+ '<a href="javascript:void(0)" '+
'"><div class="header">'+ 'onclick="setreceiver(this.innerHTML)">'+
'<span class="date">'+ htmlenc(m.user)+
(new Date(m.time)).toLocaleString()+ '</a>'+(message.receiver?' → <a href="javascript:void(0)" '+
'</span><span class="sender">'+ 'onclick="setreceiver(this.innerHTML)">'
'<a href="javascript:void(0)" '+ +htmlenc(message.receiver)+'</a>':"")+
'onclick="setreceiver(this.innerHTML)">'+ '</span></div>'+
htmlenc(m.user)+ '<div class="text">'+
'</a>'+(message.receiver?' → <a href="javascript:void(0)" '+ htmlenc(message.text)+
'onclick="setreceiver(this.innerHTML)">' '</div></div><div class="clear"/>')
+htmlenc(message.receiver)+'</a>':"")+ // show attachments
'</span></div>'+ attachments(message.files, '#id'+m.id+' .text', m.user, new Date(m.time))
'<div class="text">'+ // calculate and show emoticons
htmlenc(message.text)+ $('#id'+m.id).emoticonize()
'</div></div><div class="clear"/>') if (!internal) beep(m.user)
// show attachments }).catch(function(e) {
attachments(message.files, '#id'+m.id+' .text', m.user, new Date(m.time)) // not for me
// calculate and show emoticons success()
$('#id'+m.id).emoticonize() })
if (!internal) beep(m.user) })
}).catch(function(e) { }
// not for me
success()
})
}
/// Send Message To Server /// Send Message To Server
/** User wants to send a message. Encrypt message with own private and /** User wants to send a message. Encrypt message with own private and
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").prop(":disabled", true) $("#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").prop(":disabled", false) $("#message").prop(":disabled", false)
error("receiver's key not found", true) error("receiver's key not found", true)
return return
} }
var privkey = privateKey().keys[0] var privkey = privateKey().keys[0]
privkey.decrypt(password) // get own private key ready privkey.decrypt(password) // get own private key ready
var message = JSON.stringify({receiver: recv, text: txt, files: filecontent}) var message = JSON.stringify({receiver: recv, text: txt, files: filecontent})
notice("2/3 encrypting message …") notice("2/3 encrypting message …")
openpgp.encrypt({publicKeys: key.keys.concat(publicKey().keys), openpgp.encrypt({publicKeys: key.keys.concat(publicKey().keys),
privateKeys: privkey, privateKeys: privkey,
data: message, data: message,
armor: false}) armor: false})
.then(function(msg) { // message is encrypted .then(function(msg) { // message is encrypted
notice("3/3 sending message …") notice("3/3 sending message …")
emit("message", {user: userid(), content: msg}) emit("message", {user: userid(), content: msg})
clearmessage() clearmessage()
}) })
.catch(function(e) { .catch(function(e) {
$("#message").prop(":disabled", false) $("#message").prop(":disabled", false)
error("encryption of message failed", true) error("encryption of message failed", true)
}) })
}) })
.fail(function(e) { .fail(function(e) {
$("#message").prop(":disabled", false) $("#message").prop(":disabled", false)
error("user not found", true) error("user not found", true)
}) })
} }
/// Check And Set Password /// Check And Set Password
/** Check if given password matches to decrypt the private key. If so, /** Check if given password matches to decrypt the private key. If so,
store it in global temporary variable @ref password and start the store it in global temporary variable @ref password and start the
chat. The password matches, when the private key can be decrypted. chat. The password matches, when the private key can be decrypted.
@param pwd The password to check. */ @param pwd The password to check. */
function setpw(pwd) { function setpw(pwd) {
if (privateKey().keys[0].decrypt(pwd)) { if (privateKey().keys[0].decrypt(pwd)) {
success("password matches") success("password matches")
$("#removeKey").hide() $("#removeKey").hide()
password = pwd password = pwd
chat() chat()
} else { } else {
notice("password does not match") notice("password does not match")
} }
} }
/// Create Password Entry Field /// Create Password Entry Field
/** Asks user for password. When user starts to enter it, it is /** Asks user for password. When user starts to enter it, it is
permanentely checked in setpw(). As soon as the password matches, permanentely checked in setpw(). As soon as the password matches,
setpw() continues automatically. No submit is required by the setpw() continues automatically. No submit is required by the
user. */ user. */
function getpwd() { function getpwd() {
if (password) return if (password) return
$("#removeKey").show() $("#removeKey").show()
status("getpwd") show("getpwd")
} }
function deleteUser() { function deleteUser() {
var uid = userid() var uid = userid()
localStorage.removeItem(pubkey) localStorage.removeItem(pubkey)
localStorage.removeItem(privkey) localStorage.removeItem(privkey)
error("user "+uid+" permanentely lost") error("user "+uid+" permanentely lost")
} }
function removeKey() { function removeKey() {
togglemenu() togglemenu()
$("#removeKey").hide() $("#removeKey").hide()
status('forgotpassword') show('forgotpassword')
} }
/// 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 var firsttime = true
function chat() { function chat() {
if (!password) return getpwd() if (!password) return getpwd()
status("chat") show("chat")
if (firsttime && $('#msgs').is(':empty')) { if (firsttime && $('#msgs').is(':empty')) {
firsttime = false firsttime = false
notice("getting previous messages, please wait …") notice("getting previous messages, please wait …")
emit("messages") emit("messages")
} }
} }
/// Login User /// Login User
/** This is not really a login, it is just some kind of validation. /** 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 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 interesting to the client to make sure, everything is fine. User
is logged in the following way: User name and public key are sent 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 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 public key is the same, the user is considered logged in, his
credentials seem to be valid. If user does not yet exits on credentials seem to be valid. If user does not yet exits on
server, it is created now. If user exists, but public key is server, it is created now. If user exists, but public key is
different, then this is a complete failure, something went different, then this is a complete failure, something went
terribly wrong. */ terribly wrong. */
function login() { function login() {
$("#username").html(userid()+"@"+hostname) $("#username").html(userid()+"@"+hostname)
emit("login", {name: userid(), emit("login", {name: userid(),
pubkey: localStorage.pubkey}) pubkey: localStorage.pubkey})
success("login sent to server") success("login sent to server")
} }
/// Get And Display Form To Create New User /// Get And Display Form To Create New User
/** Shows user creation form. On submit, a private key is generated in /** Shows user creation form. On submit, a private key is generated in
createkeypair(), then login() creates the user. */ createkeypair(), then login() creates the user. */
function newuser() { function newuser() {
status("newuser") show("newuser")
} }
/// Check if local storage is available /// Check if local storage is available
function checkLocalStorage() { function checkLocalStorage() {
var test = 'test' var test = 'test'
try { try {
localStorage.setItem(test, test) localStorage.setItem(test, test)
localStorage.removeItem(test) localStorage.removeItem(test)
return true return true
} catch(e) { } catch(e) {
status("nolocalstorage") show("nolocalstorage")
error("local storage not available") error("local storage not available")
} }
return false return false
} }
/// Initial Function: Startup /// Initial Function: Startup
/** Decide whether to login or to create a new user */ /** Decide whether to login or to create a new user */
function start() { function start() {
$("#menu").hide() $("#menu").hide()
//status("startup") //show("startup")
if (checkLocalStorage()) if (checkLocalStorage())
try { try {
if (!userid()) { if (!userid()) {
newuser() newuser()
} else { } else {
login() login()
} }
} catch (m) { } catch (m) {
console.log(m.stack) console.log(m.stack)
error(m) error(m)
} }
} }
function init() { var safechat = new SafeChat()
/// On Load, Call @ref start
$(window.onbeforeunload = function() { function init() {
return "Are you sure you want to navigate away?" safechat.start()
}) }
/// Allow Running in Background on Android
document.addEventListener('deviceready', function () { function old() {
if (cordova && cordova.plugins.backgroundMode) { /// On Load, Call @ref start
cordova.plugins.backgroundMode.enable() $(window.onbeforeunload = function() {
} return "Are you sure you want to navigate away?"
}, false) })
connectionstatus() /// Allow Running in Background on Android
if (openpgp.initWorker("openpgp.worker.min.js")) document.addEventListener('deviceready', function () {
console.log("asynchronous openpgp enabled") if (cordova && cordova.plugins.backgroundMode) {
else cordova.plugins.backgroundMode.enable()
console.log("asynchronous openpgp failed") }
emit('users') }, false)
start() connectionstatus()
} if (openpgp.initWorker("openpgp.worker.min.js"))
console.log("asynchronous openpgp enabled")
else
console.log("asynchronous openpgp failed")
emit('users')
start()
}
/// Start Main Loop /// Start Main Loop
$(init) $(init)

@ -58,7 +58,7 @@ form input#msg {
height: 1.5em; height: 1.5em;
} }
.toolbutton input { .toolbutton input {
display:none; display: none;
} }
.toolbutton.bad:first-line, .toolbutton.bad:first-line,
.toolbutton.good:first-line { .toolbutton.good:first-line {

@ -79,7 +79,7 @@ module.exports = function(sql) {
}); });
}); });
socket.on("user", function(name) { socket.on("user", function(name, fn) {
console.log("-> signal: user("+name+")"); console.log("-> signal: user("+name+")");
var result = {name: name, exists: false, pubkey: null}; var result = {name: name, exists: false, pubkey: null};
sql.query("select pubkey from user where name = ?", [name], function(err, res, flds) { sql.query("select pubkey from user where name = ?", [name], function(err, res, flds) {
@ -87,10 +87,23 @@ module.exports = function(sql) {
result.exists = true; result.exists = true;
result.pubkey = res[0].pubkey; result.pubkey = res[0].pubkey;
} }
emit('user', result); fn(result);
}); });
}); });
socket.on("lookup", function(name, fn) {
console.log("-> signal: lookup("+name+")")
var result = false
sql.query("select pubkey from user where name = ?",
[name],
function(err, res, flds) {
if (!err && res && res.length) {
result = true;
}
fn(result)
});
});
socket.on("users", function(name) { socket.on("users", function(name) {
console.log("-> signal: users"); console.log("-> signal: users");
sql.query("select name, pubkey from user", [name], function(err, res, flds) { sql.query("select name, pubkey from user", [name], function(err, res, flds) {

@ -51,21 +51,12 @@
<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>
<form id="register" autocomplete="off"> <form id="register" autocomplete="off" onsubmit="safechat.createuser(document.getElementById('user').value, document.getElementById('pwd').value)">
<input placeholder="username" autocomplete="off" type="text" id="user"/> <input placeholder="username" autocomplete="off" type="text" id="user" oninput="safechat.lookup(this.value)" />
<input placeholder="password" autocomplete="off" type="password" id="pwd" oninput="checkpwd(this.value, document.getElementById('pwd2').value)"/> <input placeholder="password" autocomplete="off" type="password" id="pwd" oninput="safechat.checkpasswords(this.value, document.getElementById('pwd2').value)"/>
<input placeholder="repeat password" autocomplete="off" type="password" id="pwd2" oninput="checkpwd(document.getElementById('pwd').value, this.value)"/> <input placeholder="repeat password" autocomplete="off" type="password" id="pwd2" oninput="safechat.checkpasswords(document.getElementById('pwd').value, this.value)"/>
<button id="createuser" disabled>register</button> <input id="createuser" type="submit" value="register" disabled />
</form> </form>
<script>
$("#user").on("input", function() {
queryuser($('#user').val());
});
$("#createuser").on("click", function(event) {
createkeypair($('#user').val(), $('#pwd').val());
return false;
});
</script>
<p>Please chose any username, e.g. a pseudonym, your e-mail, <p>Please chose any username, e.g. a pseudonym, your e-mail,
your phone number, your real name, and chose a safe your phone number, your real name, and chose a safe
password.</p> password.</p>
@ -184,9 +175,9 @@
<!-- Fatal: Abort --> <!-- Fatal: Abort -->
<div id="fatal"> <div id="fatal" style="display: none">
<h2 id="fatal-msg">Failure</h2> <h2 id="fatal-msg">Failure</h2>
<p>The SafeChat has been aborted due to a fatal error.</p> <p>SafeChat has been aborted due to a fatal error.</p>
<p>There is a problem in your browser. Please try to reload. If the problem persists, please update your web browser or try SafeChat in another browser.</p> <p>There is a problem in your browser. Please try to reload. If the problem persists, please update your web browser or try SafeChat in another browser.</p>
<p>The following java script features are required:</p> <p>The following java script features are required:</p>
<ul class="features"> <ul class="features">

Loading…
Cancel
Save