added some comments and dokus

This commit is contained in:
Marc Wäckerlin
2015-07-15 13:54:52 +00:00
parent 6ac39269ac
commit 28a943f4eb
15 changed files with 2888 additions and 195 deletions

View File

@@ -1,4 +1,20 @@
<?php
/*! @file
API-call checknewuser.php
Check if a user exists in the server's user table.
@param user user name to check
@return json encoded value:
- 'user name as string', if user does exist
- null, if user does not exist or in case of error
@id $Id$
*/
// 1 2 3 4 5 6 7 8
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
require_once("usertable.php");
try {
$user = $db->real_escape_string($_REQUEST['user']);

54
html/documentation.dox Normal file
View File

@@ -0,0 +1,54 @@
/*! @file
@id $Id$
*/
// 1 2 3 4 5 6 7 8
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
/** @page protocol SafeChat Protocol
@tableofcontents
@section security Security Concept
Neither the password nor the private key are sent to the
server. They remain under the user's control and in the user's
property. Only the user name and the public key are sent to the
server.
- The password is only kept in the browser's transient memory.
- The private key is kept in encrypted form in the browser's
persistent local storage.
- The public key is stored on server, so that other users can
lookup for a user's public key.
There are two secret security tokens: The password, that is in the
user's mind and the private key, which is in the user's device, in
the local storage of his browser. Messages can only be sent or
read with access to both security tokens.
@section newuser Create New User
If no credentials exist in the browser's local storage, the
browser asks the user for a user name and a password and creates a
private key that is encrypted with the password.
In the login(), the browser sends the user's name and public key
to the server. The server creates a new user, if the user does not
exist yet. Then the server returns, whether user name and public
key match to what he has in his table.
@msc
user, browser, server;
user -> browser [label="https://safechat.ch"];
browser -> server [label="index.html"];
browser <- server [label="safechat.js",URL="\ref safechat.js"];
user <- browser [label="register new user"];
user -> browser [label="username / password"];
browser -> browser [label="create openpgp-public/private keys"];
browser -> server [label="login.php(username, public-key)"];
server -> server [label="if user name does not exist:\nstore username/public-key"];
server -> browser [label="success"];
@endmsc
*/

View File

@@ -1,10 +1,34 @@
<?php
/// Send Error To Client
/** @return error message from server to client
Function calls exit to terminate.
Message format is json:
@code
{
success: false,
txt: 'error message string';
}
@endcode */
function error($txt) {
echo json_encode(array('success' => false, 'txt' => $txt));
exit;
}
/// Send Success To Client
/** @return success message from server to client
Function calls exit to terminate.
Message format is json:
@code
{
success: true,
txt: 'success message string';
}
@endcode */
function success($txt) {
echo json_encode(array('success' => true, 'txt' => $txt));
exit;

View File

@@ -6,15 +6,19 @@
EXTRA_DIST = ${www_DATA}
wwwdir = ${pkgdatadir}/html
www_DATA = index.html chat.html newuser.html safechat.js jquery.js \
openpgp.js jquery.cssemoticons.js safechat.css \
jquery.cssemoticons.css checknewuser.php get.php login.php \
messagetable.php pubkey.php send.php usertable.php \
abort.svg A-Tone-His_Self-1266414414.mp3 attachment.svg \
audio.svg chat-rodrigo-angleton.svg \
Checkout-Scanner-Beep-SoundBible.com-593325210-by-Mike-Koenig.mp3 \
envelope.svg functions.php menu.svg pfeil.svg photo.png \
photo.svg safechat-rodrigo-angleton.svg safe-mimooh.svg \
send.svg update-messages.js video.png video.svg
dist_www_DATA = index.html chat.html newuser.html safechat.js \
jquery.js openpgp.js jquery.cssemoticons.js \
safechat.css jquery.cssemoticons.css checknewuser.php \
get.php login.php messagetable.php pubkey.php \
send.php usertable.php abort.svg \
A-Tone-His_Self-1266414414.mp3 attachment.svg \
audio.svg chat-rodrigo-angleton.svg \
Checkout-Scanner-Beep-SoundBible.com-593325210-by-Mike-Koenig.mp3 \
envelope.svg functions.php menu.svg pfeil.svg \
photo.png photo.svg safechat-rodrigo-angleton.svg \
safe-mimooh.svg send.svg update-messages.js video.png \
video.svg
EXTRA_DIST = documentation.dox
MAINTAINERCLEANFILES = makefile.in

View File

@@ -1,5 +1,5 @@
<h2>Register User (Step 1 of 1)</h2>
<form id="register" onsubmit="createNewUser(this.elements['user'].value, this.elements['pwd'].value)">
<form id="register" onsubmit="createkeypair(this.elements['user'].value, this.elements['pwd'].value)">
<input placeholder="user name" type="text" id="user" oninput="checkuser(this.value)"/>
<input placeholder="password" type="password" id="pwd" oninput="checkpwd(this.value, document.getElementById('pwd2').value)"/>
<input placeholder="repeat password" type="password" id="pwd2" oninput="checkpwd(document.getElementById('pwd').value, this.value)"/>

View File

@@ -1,7 +1,51 @@
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
/*! @file
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
@id $Id$
*/
// 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
/** Fades in an error message and logs to console.
@param data (optional) The error can be a string or any structure.
Strings are shown to the user, structures are logged only.
@param stay (optional) If not given as @c true, reloads page after 5s. */
function error(data, stay) {
$("#status").fadeOut("slow", function() {
$("#status").addClass("error")
@@ -24,6 +68,9 @@ function error(data, stay) {
});
}
/// Show notice messsage
/** Fades in an notice message and logs to console.
@param data (optional) The data is a string. */
function notice(text) {
$("#status").fadeOut("slow", function() {
$("#status").addClass("notice")
@@ -40,6 +87,9 @@ function notice(text) {
});
}
/// Show notice messsage
/** Fades in an success message and logs to console.
@param data (optional) The data is a string. */
function success(text) {
$("#status").fadeOut("slow", function() {
$("#status").addClass("success")
@@ -56,6 +106,9 @@ function success(text) {
});
}
/// Show status message in the main screen area
/** @param text Text is a message or some complex HTML from the server.
@param msg The success message text */
function status(text, msg) {
$("#main").fadeOut("slow", function() {
$("#main").html(text);
@@ -66,6 +119,18 @@ function status(text, msg) {
});
}
/// Check if user name is available
/** Calls checknewuser.php on server and displays an error, if the
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;
@@ -86,6 +151,17 @@ function checkuser(user) {
});
}
/// Check if password is set and matches the repeated password
/** Checks if both passwords are identical and valid and gives
feedback to the user.
Called when user edits the password fields.
Sets @ref username and checks @ref password - if both are well
defined, enables the submit button.
@param pwd The password.
@param pwd2 The repeated password. */
function checkpwd(pwd, pwd2) {
$("#register").submit(function(event) {
return false;
@@ -100,6 +176,9 @@ function checkpwd(pwd, pwd2) {
} else notice("passwords don't match");
}
/// Checks if the receiver of a message exists on server.
/** Calls checknewuser.php on server and enables the message submit
button if the receiver of the message exists on the server. */
function checkpartner(user) {
$("#chat").submit(function(event) {
return false;
@@ -119,7 +198,9 @@ function checkpartner(user) {
});
}
function createNewUser(user, pwd) {
/// 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,
@@ -135,16 +216,22 @@ function createNewUser(user, pwd) {
});
}
/// 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 ||
@@ -152,6 +239,8 @@ function userid() {
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();
@@ -159,6 +248,7 @@ function clearmessage() {
notice("message cleared");
}
/// Display Image Attachments
function attachments(files, id) {
if (files) files.forEach(function(file) {
if (file.content.length<100000) {
@@ -169,8 +259,16 @@ function attachments(files, id) {
});
}
/// Upload Attachment
/** Prepares attachment to be sent in a message. If the attachment is
an image, it resizes the image to 400px on the lager side.
By now, only images are supported.
Stores data in global variable @ref filecontent. */
function fileupload(evt) {
if (!window.FileReader) return error("your browser dows not support file upload", true);
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();
@@ -178,7 +276,8 @@ function fileupload(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);
if (!file.type.match('^image/'))
return error(file.name+": not an image", true);
var img = document.createElement("img");
img.onload = function() {
var MAX = 400;
@@ -210,49 +309,36 @@ function fileupload(evt) {
}
}
/*
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 ...");
var base64 = btoa(evt.target.result);
filecontent.push({type: file.type, content: base64});
if (file.type.match('^image/')) {
var img = document.createElement('img');
img.src = 'data:'+file.type+';base64,' + base64;
$("#preview").append(img);
success('image of type '+file.type+' is ready to be sent');
} else {
success('file of type '+file.type+' is ready to be sent');
}
}
reader.readAsBinaryString(file);
}
}
*/
/// Sets Receiver's Name
/** Called when clicked on a receiver's name. Sets focus to the
message text field.
@param name The receiver's name. */
function setreceiver(name) {
$("#recv").val(name);
checkpartner(name);
$("#msg").focus();
}
var startmsg = 0; // number of last downloaded message
var startmsg = 0; ///< number of last downloaded message
/// Poll For New Messages, Get And Show Them
/** The global variable @ref startmsg stores the id of the last
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;
var beeped = false; // beep only once
$.post("get.php", {start: startmsg})
.done(function(res) {
.done(function(res) { // new messages from server received
var msgs = JSON.parse(res);
if (msgs) {
msgs.forEach(function(e) {
msgs.forEach(function(e) { // one single message
if (startmsg<Number(e.id)) startmsg = Number(e.id);
$.post("pubkey.php", {user: e.user})
$.post("pubkey.php", {user: e.user}) // get sender's key
.done(function(pk) {
var res=JSON.parse(pk);
var key=openpgp.key.readArmored(res);
@@ -262,9 +348,10 @@ function get() {
}
var message = openpgp.message.readArmored(e.msg);
var privkey = privateKey().keys[0];
if (privkey.decrypt(password))
if (privkey.decrypt(password)) // prepare own key
openpgp.decryptAndVerifyMessage(privkey, key.keys, message)
.then(function(msg) {
.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 '+
@@ -280,15 +367,20 @@ function get() {
'<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)
(new Audio("A-Tone-His_Self-1266414414.mp3"))
.play();
beeped = true;
success();
})
.catch(function(e) {
// not for me
success();
});
}).fail(function(e) {
error("offline", true);
@@ -298,30 +390,33 @@ function get() {
}).fail(function(e) {
error("offline", true)
});
setTimeout(get, 10000);
setTimeout(get, 10000); // repeat every 10 seconds
}
/// Send Message To Server
/** User wants to send a message. Encrypt message with own private and
the receiver's public key, then send it to the server. */
function sendmessage(recv, txt) {
notice("1/3 preparing message ...");
$("#message").fadeOut("slow");
$.post("pubkey.php", {user: recv})
$.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("key of receiver not found", true);
error("receiver's key not found", true);
return;
}
var privkey = privateKey().keys[0];
privkey.decrypt(password);
privkey.decrypt(password); // get own private key ready
var message = JSON.stringify({text: txt, files: filecontent});
notice("2/3 encrypting message ...");
openpgp.signAndEncryptMessage(key.keys.concat(publicKey().keys), privkey, message)
.then(function(msg) {
.then(function(msg) { // message is encrypted
notice("3/3 sending message ...");
$.post("send.php", {user: userid(), msg: msg})
.done(function(res) {
.done(function(res) { // message has been sent to server
var st = JSON.parse(res);
if (st.success) {
$("#message").fadeIn("slow");
@@ -348,6 +443,12 @@ function sendmessage(recv, txt) {
$("#message").fadeIn("slow");
}
/// Check And Set Password
/** Check if given password matches to decrypt the private key. If so,
store it in global temporary variable @ref password and start the
chat. The password matches, when the private key can be decrypted.
@param pwd The password to check. */
function setpw(pwd) {
if (privateKey().keys[0].decrypt(pwd)) {
password = pwd;
@@ -355,6 +456,11 @@ function setpw(pwd) {
}
}
/// Create Password Entry Field
/** Asks user for password. When user starts to enter it, it is
permanentely checked in setpw(). As soon as the password matches,
setpw() continues automatically. No submit is required by the
user. */
function getpwd() {
status('<form>'+
' <input placeholder="password for '+userid()+
@@ -362,6 +468,9 @@ function getpwd() {
'</form>');
}
/// Main Chat Window
/** Gets chat widgets from server and displays them. Starts timer for
get() which polls for new messages. */
function chat() {
if (!password) return getpwd();
$.ajax({url: "chat.html", success: function(res) {
@@ -372,6 +481,17 @@ function chat() {
});
}
/// Login User
/** This is not really a login, it is just some kind of validation.
The server does not care if a user is online or not, it is only
interesting to the client to make sure, everything is fine. User
is logged in the following way: User name and public key are sent
to the server. If the user name exists on the server and the
public key is the same, the user is considered logged in, his
credentials seem to be valid. If user does not yet exits on
server, it is created now. If user exists, but public key is
different, then this is a complete failure, something went
terribly wrong. */
function login() {
status("login ...");
$.post("login.php", {user: userid(),
@@ -390,6 +510,9 @@ function login() {
});
}
/// Get And Display Form To Create New User
/** Shows user creation form. On submit, a private key is generated in
createkeypair(), then login() creates the user. */
function newuser() {
status("new user ...");
$.ajax({url: "newuser.html", success: function(res) {
@@ -399,6 +522,8 @@ function newuser() {
});
}
/// Initial Function: Startup
/** Decide whether to login or to create a new user */
function start() {
try {
status("Starting up ...");
@@ -412,4 +537,5 @@ function start() {
}
}
/// On Load, Call @ref start
$(start);