[ Impressum ]

LiveCode Server for HTTPS Server with PBKDF2

www.Rozek.de > LiveCode Server > PBKDF2
In another note it was explained how to build an HTTPS server using Node.js [1] and Express.js [2] in combination with the SimpleCGI "Middleware", that recognizes CGI requests and forwards them to "LiveCode Server" [3] as its CGI processor. In order to access this server, the user must first login - user name and (the SHA-1 protected) password are stored in an external file and can be read in from there by the server when necessary.

The simple generation of a SHA-1 hash for a given password is not a very safe method for the protection of passwords [4] and was only introduced because such hashes can be calculated very easily in LiveCode.

This page therefore shows a very similar server, which, however, protects user passwords using PBKDF2 hashes - albeit with the (small) disadvantage that these hashes must be now calculated with Node.js as well, since LiveCode does not (not yet?) provide the necessary functions.

PBKDF2 is a "Password-Based Key Derivation Function", which was deliberately designed to make calculating password hashes a processor-intensive(!) task. When a user logs on to a server, this effort does not play a major role (since the calculation must be carried out only rarely - especially when you combine the authentication with a "session" concept). For an attacker, who may only derive the password for a given hash by performing the same calculation for millions of possible passwords and comparing the result with the captured hash, this increased effort makes the whole attack unattractive: especially, since the described procedure has to be repeated for each individual user again, thanks to a random part ("Salt") in the hash!

Note bene: the server presented here does not support "sessions", and is therefore only useful if you expect relatively few users only and/or these users do not submit too many requests to the server! In addition, this server does not (yet) provide any protection against CSRF ("Cross-Site Request Forgery") attacks.

Overview

  • An HTTPS Server with PBKDF2
  • Calculation of Password Hashes for User Registration

An HTTPS Server with PBKDF2

Since Node.js already provides the functions for calculating PBKDF2 hashes, a server with this functionality is created quickly:

#!/usr/bin/env node
var crypto = require('crypto');
var express = require('express');
var fs = require('fs');
var http = require('http');
var https = require('https');
var SimpleCGI = require('simplecgi');

var oneDay = 24*60*60*1000;

var WebServer = express();
WebServer.use(express.compress());
WebServer.use(express.staticCache());

WebServer.all(/^.+[.]lc$/, express.basicAuth(function(UserName,Password, next){
UserName = UserName.toLowerCase();

fs.readFile(
__dirname + '/WebServer-UserList.txt', function (Error, FileContent) {
if (Error) {
console.log('Internal Error during Authentication: ', Error);
return next(null, false);
};

var LineList = FileContent.toString().split('\n');
for (var i = 0; i < LineList.length; i++) {
if (LineList[i].replace(/ *:.*$/,'').replace(/^ */,'').toLowerCase() === UserName) {
var PasswordSalt = LineList[i].replace(/^[^:]+: */,'').replace(/ *:.*$/,'');
var PasswordHash = LineList[i].replace(/^.*: */,'').replace(/ *$/,'');

LineList = null; // no longer needed, saves a little memory

return crypto.pbkdf2(
Password, new Buffer(PasswordSalt,'hex'), 10000, 64, function (Error, DerivedKey) {
if (Error) {
console.log('PBKDF2 failed: ', Error);
return next(null, false);
} else {
return next(null, PasswordHash === DerivedKey.toString('hex'));
};
}
);
};
};

return next(null, false); // user could not be found
}
);
}), SimpleCGI(
'/usr/local/bin/livecode-server', __dirname + '/www', /^.+[.]lc$/
));

WebServer.use(express.static(__dirname + '/www', { maxAge:oneDay }));

WebServer.use(express.errorHandler());
http.createServer(WebServer).listen(8080); // actually starts HTTP server
https.createServer({ // dto. for HTTPS
key: fs.readFileSync(__dirname + '/WebServer-Key.pem'),
cert: fs.readFileSync(__dirname + '/WebServer-Certificate.pem')
}, WebServer).listen(8081);

At the core of this script is the new authentication function, which searches the file WebServer-UserList.txt after the user to be authenticated and reads its PasswordSalt and PasswordHash (which had been previously calculated using PBKDF2). Based on UserName, Password and PasswordSalt, a new password hash is then calculated and compared with the PasswordHash read before: if both values are equal, the user is considered as authenticated, otherwise not.

Calculation of Password Hashes for User Registration

LiveCode has no function for calculating hashes using PBKDF2. Thus, the relevant lines for file WebServer-UserList.txt must now also be calculated using a Node.js.

Fortunately, such a script is not too complex:

#!/usr/bin/env node var argc = process.argv.length; // just a shortcut
if (argc !== 4) {
switch (argc) {
case 2: console.log('missing username and password'); break;
case 3: console.log('missing password'); break;
default: console.log('too many arguments given'); break;
};

console.log('usage: %s <username> <password>', process.argv[1]);
process.exit(1);
};

var UserName = process.argv[2].replace(/^ */,'').replace(/ *$/,'');
if (UserName === '') {
console.log('the given <username> is empty');
process.exit(2);
};

if (UserName.indexOf(':') > -1) {
console.log('<username> must not contain a colon ":"');
process.exit(3);
};

// insert additional <username> validations here

var Password = process.argv[3].replace(/^ */,'').replace(/ *$/,'');
if (Password === '') {
console.log('the given <password> is empty');
process.exit(4);
};

if (Password.length < 8) {
console.log('the given <password> contains less than 8 characters');
process.exit(5);
};

if (Password.match(/[0-9]/) === null) {
console.log('the given <password> does not contain any digit');
process.exit(6);
};

if (Password.match(/[a-z]/) === null) {
console.log('the given <password> does not contain any lower-case character');
process.exit(7);
};

if (Password.match(/[A-Z]/) === null) {
console.log('the given <password> does not contain any upper-case character');
process.exit(8);
};

if (Password.match(/^[0-9a-zA-Z]+$/) !== null) {
console.log('the given <password> does not contain any "special" character');
process.exit(9);
};

// insert additional <password> validations here

var crypto = require('crypto');

var PasswordSalt = null;
try {
PasswordSalt = crypto.randomBytes(64);
} catch (Signal) {
console.log('Error while generating password salt: ', Signal);
process.exit(10);
};

var PasswordHash = null;
try {
PasswordHash = crypto.pbkdf2Sync(Password, PasswordSalt, 10000, 64);
} catch (Signal) {
console.log('Error while calculating PBKDF2: ', Signal);
process.exit(11);
};

console.log('here is the requested user-password setting:');
console.log(
'%s:%s:%s', UserName, PasswordSalt.toString('hex'), PasswordHash.toString('hex')
);
process.exit(0);

Save the shown source code, for example, in a file called WebServer-Hash and mark this file as executable.

You can then encode a password using

WebServer-Hash <username> <password>

In this command
  • <username> - is the name of the user to be registered
  • <password> - is the user's password to be encoded
<username> must neither be empty nor contain a colon. The <password> must fulfill the following conditions:
  • it must be at least eight characters long and
  • it must at least contain a digit, an upper-case and a lower-case letter and a "special" character (such as comma, underscore, parenthesis, etc.).
These conditions are set and controlled by the WebServer-Hash script only and may therefore be easily adapted there - the WebServer itself does not care about the quality of a given password.

A possible example would be

WebServer-Hash Agent Top-S3cr3t

with a possible output of the form

Agent:ca5f1f091f4ebc560bd54c9eae84b015de70909398288c534be7cfb196d55fa740f4943770cc7511a79a4bd2f03332b5c63767e346ddb49754c7fc26083425ac:337afef7a020ffdcedcba6e27113d781b3b3dd7c3939c04373b284df47a17b221abb838779cc4276fc05512e0627aad85be2a8af0c9ff99d2967ceb9ca3a8b6f

Please add this line to file WebServer-UserList.txt. From then on, you can access "LiveCode Server" scripts using the shown username and password.

Nota bene: the PasswordSalt (i.e., the specification after the first colon) is purposefully random - and thus, the PasswordHash itself (i.e., the specification after the second colon) changes as well. You will therefore get different results upon each call of the script - even if <username> and <password> remain the same.

For completeness, a small bash script for automated testing of WebServer-Hash shall also be shown here:

#!/bin/bash
#-------------------------------------------------------------------------------
# assert a simple "assert" function for unit testing of scripts
#-------------------------------------------------------------------------------

assert () { # $1 = condition to test, $2 = line number of test
Condition=$1
if [ -z "$Condition" ]; then
echo "error in 'assert': no <condition> given"
exit 255
fi

LineNumber=$2
if [ -z "$LineNumber" ]; then
LineNumber="<unknown>"
fi

if [ ! $Condition ]; then
echo "assertion failed: \"$1\""
echo "in file \"$0\", line $LineNumber"
exit 254
fi
} # (inspired by http://tldp.org/LDP/abs/html/debugging.html#ASSERT)

#-------------------------------------------------------------------------------
# StringContains a simple string containment test
#-------------------------------------------------------------------------------

StringContains () { # $1 shall contain $2
Haystack=$1
if [ -z "$Haystack" ]; then
echo "false"; return 0
fi

Needle=$2
if [ -z "$Needle" ]; then
echo "true"; return 0
fi

if [[ $Haystack == *$Needle* ]]; then
echo "true"; return 0
else
echo "false"; return 0
fi
}


Output=`./WebServer-Hash`
ExitCode=$?
assert "`StringContains \"$Output\" \"missing username and password\"` == true" $LINENO
assert "$ExitCode -eq 1" $LINENO


Output=`./WebServer-Hash username`
ExitCode=$?
assert "`StringContains \"$Output\" \"missing password\"` == true" $LINENO
assert "$ExitCode -eq 1" $LINENO


Output=`./WebServer-Hash username password and more`
ExitCode=$?
assert "`StringContains \"$Output\" \"too many arguments given\"` == true" $LINENO
assert "$ExitCode -eq 1" $LINENO


Output=`./WebServer-Hash "" password`
ExitCode=$?
assert "`StringContains \"$Output\" \"the given <username> is empty\"` == true" $LINENO
assert "$ExitCode -eq 2" $LINENO


Output=`./WebServer-Hash "user:name" password`
ExitCode=$?
assert "`StringContains \"$Output\" \"<username> must not contain a colon\"` == true" $LINENO
assert "$ExitCode -eq 3" $LINENO


Output=`./WebServer-Hash username ""`
ExitCode=$?
assert "`StringContains \"$Output\" \"the given <password> is empty\"` == true" $LINENO
assert "$ExitCode -eq 4" $LINENO


Output=`./WebServer-Hash username 1234`
ExitCode=$?
assert "`StringContains \"$Output\" \"the given <password> contains less than 8 characters\"` == true" $LINENO
assert "$ExitCode -eq 5" $LINENO


Output=`./WebServer-Hash username abcdefgh`
ExitCode=$?
assert "`StringContains \"$Output\" \"the given <password> does not contain any digit\"` == true" $LINENO
assert "$ExitCode -eq 6" $LINENO


Output=`./WebServer-Hash username 12345678`
ExitCode=$?
assert "`StringContains \"$Output\" \"the given <password> does not contain any lower-case character\"` == true" $LINENO
assert "$ExitCode -eq 7" $LINENO


Output=`./WebServer-Hash username abcd5678`
ExitCode=$?
assert "`StringContains \"$Output\" \"the given <password> does not contain any upper-case character\"` == true" $LINENO
assert "$ExitCode -eq 8" $LINENO


Output=`./WebServer-Hash username abCD5678`
ExitCode=$?
assert "`StringContains \"$Output\" \"the given <password> does not contain any \\\"special\\\" character\"` == true" $LINENO
assert "$ExitCode -eq 9" $LINENO


Output=`./WebServer-Hash username Top-S3cr3t`
ExitCode=$?
# assert "`StringContains \"$Output\" \"here is the requested user-password setting: \"` == true" $LINENO
assert "$ExitCode -eq 0" $LINENO

echo "all tests passed"
exit 0

Save the shown source code, for example, in a file called WebServer-Hash-Test (in the same directory as WebServer-Hash itself) and mark the file as executable.

With a simple call of the test script you are now ready to completely test WebServer-Hash.

                       
Have fun with "LiveCode Server" and this HTTPS server! Creative Commons License

Bibliography

[1]
Joyent Inc.
node.js
Node.js is first and foremost a platform for extremely powerful network applications written in JavaScript. However, in combination with other technologies (such as Node-WebKit) Node.js may also be used for more than just HTTP servers.
[2]
Tj Holowaychuk
Express - node.js web application framework
Express is a lightweight web application framework for Node.js. Thanks to its modularity, Express allows for rapid development of HTTP servers based on Node.js.
[3]
(RunRev Ltd.)
LiveCode | LiveCode Server Guide
The LiveCode Server is an interpreter for LiveCode scripts that is started from the command line (does not offer any graphical user interface) and is intended primarily as a CGI processor (this way, web pages can be processed with LiveCode - thus, you do not necessarily have to learn PHP any longer).
[4]
(Defuse Security)
Secure Salted Password Hashing - How to do it Properly
If you run a server that requires a login from its users, you will have to persist these security-relevant data somehow. This web page describes the potential problems and provides appropriate solutions.