/ * ! @ f i l e
@ id $Id$
This is the main application as it is fully run in the user ' s browser .
@ dot
digraph X {
start [ URL = "\ref start()" ] ;
newuser [ URL = "\ref newuser()" ] ;
login [ URL = "\ref login()" ] ;
createkeypair [ URL = "\ref createkeypair()" ] ;
chat [ URL = "\ref chat()" ] ;
getpwd [ URL = "\ref getpwd()" ] ;
setpw [ URL = "\ref setpw()" ] ;
get [ URL = "\ref get()" ] ;
sendmessage [ URL = "\ref sendmessage()" ] ;
start - > newuser [ label = "if no keys exist" ] ;
start - > login [ label = "if keys exist" ] ;
newuser - > createkeypair [ label = "on submit" ] ;
createkeypair - > "openpgp.generateKeyPair" ;
"openpgp.generateKeyPair" - > login [ label = "keys generated in local store" ] ;
login - > chat [ label = "user is valid on server" ] ;
chat - > getpwd [ label = "password not yet entered" ] ;
getpwd - > setpw [ label = "on input" ] ;
setpw - > chat [ label = "password is valid" ] ;
chat - > chat [ label = "remain in chat" ] ;
chat - > get [ label = "start timer" ] ;
get - > get [ label = "restart timer" ] ;
chat - > sendmessage [ label = "on submit" ] ;
sendmessage - > chat [ label = "remain in chat" ] ;
}
@ enddot
* /
// 1 2 3 4 5 6 7 8
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
var password = null ; ///< password, only stored temporary, until reload
var username = null ; ///< username, only used during registration
var filecontent = new Array ( ) ; ///< temporary storage for attachments
/// Show error messsage
/ * * F a d e s i n a n e r r o r m e s s a g e a n d l o g s t o c o n s o l e .
@ param data ( optional ) The error can be a string or any structure .
Strings are shown to the user , structures are logged only .
@ param stay ( optional ) If not given as @ c true , reloads page after 5 s . * /
function error ( data , stay ) {
$ ( "#status" ) . fadeOut ( "slow" , function ( ) {
$ ( "#status" ) . addClass ( "error" )
$ ( "#status" ) . removeClass ( "notice" )
$ ( "#status" ) . removeClass ( "success" )
if ( data ) {
if ( typeof data == 'string' ) {
$ ( "#status" ) . html ( data ) ;
console . log ( "error: " + data ) ;
} else {
$ ( "#status" ) . html ( 'error' ) ;
console . log ( "error: " + JSON . stringify ( data ) ) ;
}
} else {
$ ( "#status" ) . html ( 'error' ) ;
console . log ( "error" ) ;
}
$ ( "#status" ) . fadeIn ( "slow" ) ;
if ( ! stay ) setTimeout ( start , 5000 ) ;
} ) ;
}
/// Show notice messsage
/ * * F a d e s i n a n n o t i c e m e s s a g e a n d l o g s t o c o n s o l e .
@ param text ( optional ) The data is a string . * /
function notice ( text ) {
$ ( "#status" ) . fadeOut ( "slow" , function ( ) {
$ ( "#status" ) . addClass ( "notice" )
$ ( "#status" ) . removeClass ( "error" )
$ ( "#status" ) . removeClass ( "success" )
if ( text ) {
$ ( "#status" ) . html ( text ) ;
console . log ( "notice: " + text ) ;
} else {
$ ( "#status" ) . html ( '' ) ;
console . log ( "notice" ) ;
}
$ ( "#status" ) . fadeIn ( "slow" ) ;
} ) ;
}
/// Show notice messsage
/ * * F a d e s i n a n s u c c e s s m e s s a g e a n d l o g s t o c o n s o l e .
@ param text ( optional ) The data is a string . * /
function success ( text ) {
$ ( "#status" ) . fadeOut ( "slow" , function ( ) {
$ ( "#status" ) . addClass ( "success" )
$ ( "#status" ) . removeClass ( "error" )
$ ( "#status" ) . removeClass ( "notice" )
if ( text ) {
$ ( "#status" ) . html ( text ) ;
console . log ( "success: " + text ) ;
} else {
$ ( "#status" ) . html ( '' ) ;
console . log ( "success" ) ;
}
$ ( "#status" ) . fadeIn ( "slow" ) ;
} ) ;
}
/// Show status message in the main screen area
/ * * @ p a r a m t e x t T e x t i s a m e s s a g e o r s o m e c o m p l e x H T M L f r o m t h e s e r v e r .
@ param msg The success message text * /
function status ( text , msg ) {
$ ( "#main" ) . fadeOut ( "slow" , function ( ) {
$ ( "#main" ) . html ( text ) ;
success ( msg ) ;
$ ( "#main" ) . fadeIn ( "slow" , function ( ) {
$ ( "form input:first-child" ) . focus ( ) ;
} )
} ) ;
}
var getLoopTimeout = null ; ///< store get timeout to make sure only one is running
/// Set timeout for next get request
/** @param time timeout time in ms, defaults to 10000 */
function getLoop ( time ) {
if ( ! time ) time = 10000 ;
getLoopStop ( ) ;
getLoopTimeout = setTimeout ( get , time ) ;
}
/// Stop get loop if it is running
function getLoopStop ( ) {
if ( getLoopTimeout ) clearTimeout ( getLoopTimeout ) ;
getLoopTimeout = null ;
}
/// Alert user
/ * * A l e r t u s e r , e . g . t h a t a n e w m e s s a g e h a s a r r i v e d .
@ param * /
function alert ( ) {
navigator . vibrate =
navigator . vibrate || navigator . webkitVibrate || navigator . mozVibrate || navigator . msVibrate ;
if ( navigator . vibrate ) {
// vibration API supported
navigator . vibrate ( 1000 ) ;
}
( new Audio ( "A-Tone-His_Self-1266414414.mp3" ) ) . play ( ) ;
}
/// Toggle Menu Display
function togglemenu ( ) {
$ ( "#menu" ) . toggle ( ) ;
}
/// Download Profile Backup
function backup ( ) {
getLoopStop ( ) ;
status ( "<p>Starting backup download ...</p>" ) ;
var download = document . createElement ( 'a' ) ;
download . href = 'data:attachment/text,' + encodeURI ( JSON . stringify ( localStorage ) ) ;
download . target = '_blank' ;
function pad ( n ) { return n < 10 ? '0' + n : n }
var now = new Date ( ) ;
download . download =
pad ( now . getFullYear ( ) ) + pad ( now . getMonth ( ) + 1 ) + pad ( now . getDate ( ) ) +
"-safechat.bak" ;
var clickEvent = new MouseEvent ( "click" , {
"view" : window ,
"bubbles" : true ,
"cancelable" : false
} ) ;
download . dispatchEvent ( clickEvent ) ;
togglemenu ( ) ;
setTimeout ( start , 2000 ) ;
}
/// Upload Profile Backup
function restore ( evt ) {
getLoopStop ( ) ;
status ( "<p>Starting backup restore ...</p>" ) ;
if ( ! window . FileReader )
return error ( "your browser dows not support file upload" , true ) ;
for ( var i = 0 , f ; f = evt . target . files [ i ] ; ++ i ) {
var file = f ;
var reader = new FileReader ( ) ;
reader . onload = function ( evt ) {
if ( evt . target . error ) return error ( "error reading file" , true ) ;
if ( evt . target . readyState == 0 ) return notice ( "waiting for data ..." ) ;
if ( evt . target . readyState == 1 ) return notice ( "loading data ..." ) ;
var parsed = JSON . parse ( evt . target . result ) ;
togglemenu ( ) ;
localStorage . pubKey = parsed . pubKey ;
localStorage . privKey = parsed . privKey ;
setTimeout ( start , 2000 ) ;
}
reader . readAsText ( file ) ;
}
}
/// Configure local groups
/** ... */
function groups ( ) {
}
/// Check if user name is available
/ * * 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 d i s p l a y s a n e r r o r , i f t h e
user name is already in use . This function is used when creating a
new user . It immediately gives the user a feedback , whether the
chosen user name is available or not .
Called when user edits the user name fields .
Sets @ ref username and checks @ ref password - if both are well
defined , enables the submit button .
@ param user User name to check . * /
function checkuser ( user ) {
$ ( "#register" ) . submit ( function ( event ) {
return false ;
} ) ;
$ . post ( "checknewuser.php" , { user : user } )
. done ( function ( res ) {
username = JSON . parse ( res ) ;
if ( ! username || username . length < 1 ) username = null ;
$ ( "#createuser" ) . prop ( "disabled" , ! ( username && password ) ) ;
if ( username ) {
if ( password ) success ( "user is ready to be created" ) ;
else notice ( "user name is available, please set password" ) ;
} else notice ( "user name is not available" ) ;
} ) . fail ( function ( res ) {
username = null ;
$ ( "#createuser" ) . prop ( "disabled" , ! ( username && password ) ) ;
error ( "offline" ) ;
} ) ;
}
/// 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 .
Called when user edits the password fields .
Sets @ ref username and checks @ ref password - if both are well
defined , enables the submit button .
@ param pwd The password .
@ param pwd2 The repeated password . * /
function checkpwd ( pwd , pwd2 ) {
$ ( "#register" ) . submit ( function ( event ) {
return false ;
} ) ;
if ( pwd == pwd2 ) password = pwd ;
else password = null ;
if ( ! password || password . length < 1 ) password = null ;
$ ( "#createuser" ) . prop ( "disabled" , ! ( username && password ) ) ;
if ( password ) {
if ( username ) success ( "user is ready to be created" ) ;
else notice ( "password matches, please chose a valid user name" ) ;
} else notice ( "passwords don't match" ) ;
}
/// 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
button if the receiver of the message exists on the server . * /
function checkpartner ( user ) {
$ ( "#chat" ) . submit ( function ( event ) {
return false ;
} ) ;
$ . post ( "checknewuser.php" , { user : user } )
. done ( function ( res ) {
if ( JSON . parse ( res ) ) {
notice ( "receiver does not exist" ) ;
$ ( "#send" ) . prop ( "disabled" , true ) ;
return ;
}
$ ( "#send" ) . prop ( "disabled" , false ) ;
success ( "receiver exists" ) ;
} ) . fail ( function ( res ) {
error ( "offline" , true ) ;
$ ( "#send" ) . prop ( "disabled" , true ) ;
} ) ;
}
/// Create Local Public-/Private-Key Pair
/** Called if user has not yet his keys, just generates a new key pair. */
function createkeypair ( user , pwd ) {
status ( "generate keys" ) ;
openpgp . generateKeyPair ( {
numBits : 1024 ,
userId : user ,
passphrase : pwd
} ) . then ( function ( keyPair ) {
success ( "keys generated" ) ;
localStorage [ "pubKey" ] = keyPair . publicKeyArmored ;
localStorage [ "privKey" ] = keyPair . privateKeyArmored ;
login ( ) ;
} ) . catch ( function ( e ) {
error ( e ) ;
} ) ;
}
/// Get Own Public Key
/** @return public key object */
function publicKey ( ) {
if ( typeof localStorage [ "pubKey" ] == 'undefined' ) return null ;
return openpgp . key . readArmored ( localStorage [ "pubKey" ] ) ;
}
/// Get Own Private Key
/** @return private key object */
function privateKey ( ) {
if ( typeof localStorage [ "privKey" ] == 'undefined' ) return null ;
return openpgp . key . readArmored ( localStorage [ "privKey" ] ) ;
}
/// Get Own User Name
/** Get user name as user id of first public key */
function userid ( ) {
if ( ! publicKey ( ) ||
publicKey ( ) . keys . length < 1 ||
publicKey ( ) . keys [ 0 ] . getUserIds ( ) . length < 1 ) return null
return publicKey ( ) . keys [ 0 ] . getUserIds ( ) [ 0 ] ;
}
/// Clear Message Text And Attachments
/** Does not remove the receiver's name */
function clearmessage ( ) {
filecontent = new Array ( ) ;
$ ( '#preview' ) . empty ( ) ;
$ ( "#msg" ) . val ( "" ) ;
notice ( "message cleared" ) ;
}
/// Display Image Attachments
function attachments ( files , id ) {
if ( files ) files . forEach ( function ( file ) {
if ( file . content . length < 100000 ) {
var img = document . createElement ( 'img' ) ;
img . src = file . content ;
$ ( id ) . append ( img ) ;
}
} ) ;
}
/// 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
an image , it resizes the image to 400 px on the lager side .
By now , only images are supported .
Stores data in global variable @ ref filecontent . * /
function fileupload ( evt ) {
if ( ! window . FileReader )
return error ( "your browser dows not support file upload" , true ) ;
for ( var i = 0 , f ; f = evt . target . files [ i ] ; ++ i ) {
var file = f ;
var reader = new FileReader ( ) ;
reader . onload = function ( evt ) {
if ( evt . target . error ) return error ( "error reading file" , true ) ;
if ( evt . target . readyState == 0 ) return notice ( "waiting for data ..." ) ;
if ( evt . target . readyState == 1 ) return notice ( "loading data ..." ) ;
if ( ! file . type . match ( '^image/' ) )
return error ( file . name + ": not an image" , true ) ;
var img = document . createElement ( "img" ) ;
img . onload = function ( ) {
var MAX = 400 ;
var width = img . width ;
var height = img . height ;
if ( width > MAX ) {
height *= MAX / width ;
width = MAX ;
}
if ( height > MAX ) {
width *= MAX / height ;
height = MAX ;
}
var canvas = document . createElement ( "canvas" ) ;
canvas . width = width ;
canvas . height = height ;
var ctx = canvas . getContext ( "2d" ) ;
ctx . drawImage ( img , 0 , 0 , width , height ) ;
img . onload = function ( ) {
filecontent . push ( { type : file . type , content : img . src } ) ;
$ ( "#preview" ) . append ( img ) ;
success ( 'image of type ' + file . type + ' is ready to be sent' ) ;
}
img . src = canvas . toDataURL ( file . type ) ;
}
img . src = evt . target . result ;
}
reader . readAsDataURL ( file ) ;
}
}
/// Sets Receiver's Name
/ * * 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 .
@ param name The receiver ' s name . * /
function setreceiver ( name ) {
$ ( "#recv" ) . val ( name ) ;
checkpartner ( name ) ;
$ ( "#msg" ) . focus ( ) ;
}
var startmsg = 0 ; ///< number of last downloaded message
/// Poll For New Messages, Get And Show Them
/ * * T h e g l o b a l v a r i a b l e @ r e f s t a r t m s g s t o r e s t h e i d o f t h e l a s t
downloaded message . This function is called by timer in regulary
periods . It calls get on server and passes @ ref startmsg . The
server returns all newer messages . They are then decrypted . If
decryption is successful , then the message is shown , including
attachments . If decryption fails , the message is sent to someone
else , so failure is simply ignored . Beeps a sound once , if new
messages have been displayed . * /
function get ( ) {
var beeped = false ; // beep only once
$ . post ( "get.php" , { start : startmsg } )
. done ( function ( res ) { // new messages from server received
var msgs = JSON . parse ( res ) ;
if ( msgs ) {
msgs . forEach ( function ( e ) { // one single message
if ( startmsg < Number ( e . id ) ) startmsg = Number ( e . id ) ;
$ . post ( "pubkey.php" , { user : e . user } ) // get sender's key
. done ( function ( pk ) {
var res = JSON . parse ( pk ) ;
var key = openpgp . key . readArmored ( res ) ;
if ( ! res || key . err ) {
getLoop ( ) ;
return error ( "key of receiver not found" , true ) ;
}
var message = openpgp . message . readArmored ( e . msg ) ;
var privkey = privateKey ( ) . keys [ 0 ] ;
if ( privkey . decrypt ( password ) ) // prepare own key
openpgp . decryptAndVerifyMessage ( privkey , key . keys , message )
. then ( function ( msg ) { // decryption succeded
// prepend message to list of messages
var message = JSON . parse ( msg . text ) ;
$ ( "#msgs" ) // todo: check msg.signatures[0].valid
. prepend ( '<div id="id' + ( e . id ) + '" class="msg ' +
( e . user == userid ( ) ? "me" : "other" ) +
'"><div class="header">' +
'<span class="date">' +
( new Date ( 1000 * Number ( e . time ) ) ) . toLocaleString ( ) +
'</span><span class="sender">' +
'<a href="javascript:void(0)" ' +
'onclick="setreceiver(this.innerHTML)">' +
e . user +
'</a>' + ( message . receiver ? " → " + message . receiver : "" ) +
'</span></div>' +
'<div class="text">' +
message . text +
'</div></div><div class="clear"/>' ) ;
// show attachments
attachments ( message . files , '#id' + e . id + ' .text' ) ;
// calculate and show emoticons
$ ( '#id' + e . id ) . emoticonize ( ) ;
// beep for the first new message
if ( ! beeped ) alert ( )
beeped = true ;
success ( ) ;
} )
. catch ( function ( e ) {
// not for me
success ( ) ;
} ) ;
} ) . fail ( function ( e ) {
error ( "offline" ) ;
} ) ;
} ) ;
}
} ) . fail ( function ( e ) {
error ( "offline" )
} ) ;
getLoop ( ) ; // repeat every 10 seconds
}
/// 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
the receiver ' s public key , then send it to the server . * /
function sendmessage ( recv , txt ) {
notice ( "1/3 preparing message ..." ) ;
$ ( "#message" ) . fadeOut ( "slow" ) ;
$ . post ( "pubkey.php" , { user : recv } ) // get receiver's public key
. done ( function ( pk ) {
var res = JSON . parse ( pk ) ;
var key = openpgp . key . readArmored ( res ) ;
if ( ! res || key . err ) {
$ ( "#message" ) . fadeIn ( "slow" ) ;
error ( "receiver's key not found" , true ) ;
return ;
}
var privkey = privateKey ( ) . keys [ 0 ] ;
privkey . decrypt ( password ) ; // get own private key ready
var message = JSON . stringify ( { receiver : recv , text : txt , files : filecontent } ) ;
notice ( "2/3 encrypting message ..." ) ;
openpgp . signAndEncryptMessage ( key . keys . concat ( publicKey ( ) . keys ) , privkey , message )
. then ( function ( msg ) { // message is encrypted
notice ( "3/3 sending message ..." ) ;
$ . post ( "send.php" , { user : userid ( ) , msg : msg } )
. done ( function ( res ) { // message has been sent to server
var st = JSON . parse ( res ) ;
if ( st . success ) {
$ ( "#message" ) . fadeIn ( "slow" ) ;
clearmessage ( ) ;
success ( st . txt ) ;
} else {
$ ( "#message" ) . fadeIn ( "slow" ) ;
error ( st . txt , true ) ;
}
} )
. fail ( function ( ) {
error ( "offline" , true ) ;
} ) ;
} )
. catch ( function ( e ) {
$ ( "#message" ) . fadeIn ( "slow" ) ;
error ( "encryption of message failed" , true ) ;
} ) ;
} )
. fail ( function ( e ) {
$ ( "#message" ) . fadeIn ( "slow" ) ;
error ( "offline" , true ) ;
} ) ;
$ ( "#message" ) . fadeIn ( "slow" ) ;
}
/// 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 ,
store it in global temporary variable @ ref password and start the
chat . The password matches , when the private key can be decrypted .
@ param pwd The password to check . * /
function setpw ( pwd ) {
if ( privateKey ( ) . keys [ 0 ] . decrypt ( pwd ) ) {
password = pwd ;
chat ( ) ;
}
}
/// 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
permanentely checked in setpw ( ) . As soon as the password matches ,
setpw ( ) continues automatically . No submit is required by the
user . * /
function getpwd ( ) {
status ( '<form>' +
' <input placeholder="password for ' + userid ( ) +
'" id="pwd" oninput="setpw(this.value)" type="password" />' +
'</form>' ) ;
}
/// 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
get ( ) which polls for new messages . * /
function chat ( ) {
if ( ! password ) return getpwd ( ) ;
$ ( "#username" ) . html ( userid ( ) + "@safechat.ch" ) ;
$ . ajax ( { url : "chat.html" , success : function ( res ) {
status ( res ) ;
getLoop ( 2000 ) ;
} } ) . fail ( function ( ) {
error ( "offline" )
} ) ;
}
/// 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 .
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 ( ) {
status ( "login ..." ) ;
$ . post ( "login.php" , { user : userid ( ) ,
pubkey : localStorage . pubKey } ,
function ( res ) {
var st = JSON . parse ( res ) ;
if ( st . success ) {
status ( "logged in ..." , st . txt ) ;
chat ( ) ;
} else {
error ( st . txt ) ;
}
} )
. fail ( function ( e ) {
error ( "offline" ) ;
} ) ;
}
/// 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
createkeypair ( ) , then login ( ) creates the user . * /
function newuser ( ) {
status ( "new user ..." ) ;
$ . ajax ( { url : "newuser.html" , success : function ( res ) {
status ( res ) ;
} } ) . fail ( function ( ) {
error ( "offline" ) ;
} ) ;
}
/// Initial Function: Startup
/** Decide whether to login or to create a new user */
function start ( ) {
try {
status ( "Starting up ..." ) ;
if ( ! userid ( ) ) {
newuser ( ) ;
} else {
login ( ) ;
}
} catch ( m ) {
error ( m ) ;
}
}
/// On Load, Call @ref start
$ ( start ) ;