@ -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
/ * * @ i n t e r n a l k e y i s t c a c h e d i n k
/ * * @ i n t e r n a l k e y i s t c a c h e d i n 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 . priv key = == 'undefined' ) return null
return k = openpgp . key . readArmored ( localStorage . key )
return k = openpgp . key . readArmored ( localStorage . priv key)
}
}
/// 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
/ * * V i e w p r o v i d e s t h e f o l l o w i n g c a l l b a c k s :
/ * * V i e w p r o v i d e s t h e f o l l o w i n g c a l l b a c k s :
- 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' , '✔' )
else $ ( '#' + id + ':before' )
. css ( 'color' , 'red' )
. css ( 'content' , '✘' )
if ( query ) $ ( '#' + id )
if ( query ) $ ( '#' + id )
. css ( 'color' , 'green' )
. css ( 'color' , 'green' )
. css ( 'text-decoration' , 'line-through ')
. prepend ( '<span>✔</span>' )
else $ ( '#' + id )
else $ ( '#' + id )
. css ( 'color' , 'red' )
. css ( 'color' , 'red' )
. css ( 'text-decoration' , 'none ')
. prepend ( '<span>✘</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 Datab ase ( )
var db = new DataB ase ( )
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 ru n( ) {
this . run = functio n( ) {
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
//==============================================================================
/ * * C h e c k s i f b o t h p a s s w o r d s a r e i d e n t i c a l a n d v a l i d a n d g i v e s
//------------------------------------------------------------------------------
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 . * /
/ * * C h e c k s i f b o t h p a s s w o r d s a r e i d e n t i c a l a n d v a l i d a n d g i v e s
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.
/ * * C a l l s c h e c k n e w u s e r . p h p o n s e r v e r a n d e n a b l e s t h e m e s s a g e s u b m i t
/ * * C a l l s c h e c k n e w u s e r . p h p o n s e r v e r a n d e n a b l e s t h e m e s s a g e s u b m i t
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
/ * * P r e p a r e s a t t a c h m e n t t o b e s e n t i n a m e s s a g e . I f t h e a t t a c h m e n t i s
/ * * P r e p a r e s a t t a c h m e n t t o b e s e n t i n a m e s s a g e . I f t h e a t t a c h m e n t i s
an image , it resizes the image to 400 px on the lager side .
an image , it resizes the image to 400 px 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
/ * * C a l l e d w h e n c l i c k e d o n a r e c e i v e r ' s n a m e . S e t s f o c u s t o t h e
/ * * C a l l e d w h e n c l i c k e d o n a r e c e i v e r ' s n a m e . S e t s f o c u s t o t h e
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
/ * * U s e r w a n t s t o s e n d a m e s s a g e . E n c r y p t m e s s a g e w i t h o w n p r i v a t e a n d
/ * * U s e r w a n t s t o s e n d a m e s s a g e . E n c r y p t m e s s a g e w i t h o w n p r i v a t e a n d
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
/ * * C h e c k i f g i v e n p a s s w o r d m a t c h e s t o d e c r y p t t h e p r i v a t e k e y . I f s o ,
/ * * C h e c k i f g i v e n p a s s w o r d m a t c h e s t o d e c r y p t t h e p r i v a t e k e y . I f s o ,
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
/ * * A s k s u s e r f o r p a s s w o r d . W h e n u s e r s t a r t s t o e n t e r i t , i t i s
/ * * A s k s u s e r f o r p a s s w o r d . W h e n u s e r s t a r t s t o e n t e r i t , i t i s
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
/ * * G e t s c h a t w i d g e t s f r o m s e r v e r a n d d i s p l a y s t h e m . S t a r t s t i m e r f o r
/ * * G e t s c h a t w i d g e t s f r o m s e r v e r a n d d i s p l a y s t h e m . S t a r t s t i m e r f o r
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
/ * * T h i s i s n o t r e a l l y a l o g i n , i t i s j u s t s o m e k i n d o f v a l i d a t i o n .
/ * * T h i s i s n o t r e a l l y a l o g i n , i t i s j u s t s o m e k i n d o f v a l i d a t i o n .
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
/ * * S h o w s u s e r c r e a t i o n f o r m . O n s u b m i t , a p r i v a t e k e y i s g e n e r a t e d i n
/ * * S h o w s u s e r c r e a t i o n f o r m . O n s u b m i t , a p r i v a t e k e y i s g e n e r a t e d i n
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 )