[ Impressum ]

LiveCode Server für HTTPS-Server mit PBKDF2

www.Rozek.de > LiveCode Server > PBKDF2
In einer anderen Notiz wurde erklärt, wie man sich mit Node.js [1] und Express.js [2] unter Verwendung der SimpleCGI "Middleware" einen eigenen HTTPS-Server bauen kann, der CGI-Anforderungen erkennt und an "LiveCode Server" [3] als CGI-Prozessor weiterleitet. Um auf diesen Server zugreifen zu können, müssen sich die Benutzer erst anmelden - Benutzername und Passwort liegen SHA-1-gesichert in einer externen Datei und werden vom Server bei Bedarf eingelesen.

Nun ist das simple Generieren eines SHA-1 Hash für ein gegebenes Passwort keine sehr sichere Methode für den Schutz von Passwörtern [4] und wurde nur vorgestellt, weil solche Hashes auch in LiveCode sehr einfach berechnet werden können.

Diese Seite zeigt deshalb einen ähnlichen Server, der Benutzer-Passwörter allerdings mithilfe von PBKDF2 absichert - wenn auch mit dem (kleinen) Nachteil, dass diese Hashes jetzt ebenfalls mit Node.js gebildet werden müssen, weil LiveCode nicht (noch nicht?) über die erforderlichen Funktionen verfügt.

PBKDF2 ist eine "Password-Based Key Derivation Function", die bewusst so entworfen wurde, dass sie die Berechnung eines Passwort-Hashes möglichst aufwändig(!) gestaltet. Bei der Anmeldung eines Benutzers an einem Server spielt dieser Aufwand keine allzu große Rolle (weil die Berechnung nur selten erfolgen muss - vor allem, wenn man die Anmeldung mit einem "Session"-Konzept kombiniert). Für einen Angreifer, der von einem gespeicherten Hash nur dadurch auf das zugrunde liegende Passwort schließen kann, indem er dieselbe Berechnung für Millionen möglicher Passwörter durchführt und deren Ergebnis mit der Vorgabe vergleicht, macht dieser gestiegene Aufwand den Einbruch aber unattraktiv: denn der genannte Vorgang muss dank eines zufälligen Anteiles ("Salt") zudem noch für jeden einzelnen Benutzer wiederholt werden!

Note bene: der hier vorgestellte Server unterstützt keine "Sessions" und ist deshalb nur sinnvoll, wenn die zu authentisierenden Benutzer nur einen bzw. sehr wenige weitere Requests an den Server absetzen! Außerdem bietet der Server (bislang) keinen Schutz gegen CSRF ("Cross-Site Request Forgery")-Attacken.

Übersicht

  • Ein HTTPS-Server mit PBKDF2
  • Berechnung der Password-Hashes zur Registrierung von Benutzern

Ein HTTPS-Server mit PBKDF2

Da Node.js bereits alle Funktionen zur Berechnung von PBKDF2-Hashes mitbringt, ist ein Server mit dieser Funktionalität schnell erstellt:

#!/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);

Den Kern dieses Skriptes bildet die neue Authentisierungsfunktion, die in der Datei WebServer-UserList.txt nach dem zu authentisierenden Benutzer sucht und dessen PasswordSalt und (zuvor bereits mittels PBDKF2 berechneten) PasswordHash ausliest. Aus UserName, Password und PasswordSalt wird anschließend wieder ein Password-Hash berechnet und mit dem eingelesenen PasswordHash verglichen: sind beide Werte gleich, gilt der Benutzer als authentisiert, anderenfalls nicht.

Berechnung der Password-Hashes zur Registrierung von Benutzern

LiveCode kennt keine Funktion zur Berechnung von Hashes mittels PBKDF2. Die betreffenden Zeilen für die Datei WebServer-UserList.txt müssen jetzt also ebenfalls mithilfe eines Node.js-Skriptes berechnet werden.

Glücklicherweise ist ein solches Skript nicht allzu komplex:

#!/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);

Speichern Sie den gezeigten Quelltext z.B. in eine Datei namens WebServer-Hash und markieren Sie diese Datei als ausführbar.

Anschließend können Sie Passwort wie folgt kodieren

WebServer-Hash <username> <password>

Darin ist
  • <username> - der Name des zu registrierenden Benutzers
  • <password> - das zu kodierende Passwort des Benutzers
Der <username> darf weder leer sein noch einen Doppelpunkt enthalten, für das <password> gelten folgende Bedingungen:
  • es muss mindestens acht Zeichen umfassen und
  • es muss mindestens je eine Ziffer, ein großer und ein kleiner Buchstabe und ein Sonderzeichen (also z.B. Punkt, Komma, Unterstrich, Klammer usw.) enthalten.
Diese Bedingungen werden nur vom Skript WebServer-Hash festgesetzt und kontrolliert und können deshalb dort auch leicht angepasst werden - dem WebServer selbst ist die Qualität des Passwortes gleichgültig.

Ein möglicher Aufruf wäre also

WebServer-Hash Agent Top-S3cr3t

mit einer möglichen Ausgabe der Form

Agent:ca5f1f091f4ebc560bd54c9eae84b015de70909398288c534be7cfb196d55fa740f4943770cc7511a79a4bd2f03332b5c63767e346ddb49754c7fc26083425ac:337afef7a020ffdcedcba6e27113d781b3b3dd7c3939c04373b284df47a17b221abb838779cc4276fc05512e0627aad85be2a8af0c9ff99d2967ceb9ca3a8b6f

Diese Zeile fügen Sie bitte der Datei WebServer-UserList.txt hinzu. Anschließend können Sie mit dem gezeigten Benutzernamen und Passwort auf "LiveCode Server"-Skripte zugreifen.

Nota bene: das PasswordSalt (d.h. die Angabe hinter dem ersten Doppelpunkt) ist bewusst zufällig - und damit ändert sich auch der PasswordHash selbst (also die Angabe hinter dem zweiten Doppelpunkt). Sie erhalten also für jeden Aufruf des Skriptes neue Ausgaben - selbst wenn <username> und <password> gleich bleiben.

Der Vollständigkeit halber soll hier noch ein kleines bash-Skript gezeigt werden, mit dem man WebServer-Hash automatisiert testen kann:

#!/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

Speichern Sie den gezeigten Quelltext z.B. in einer Datei namens WebServer-Hash-Test (im selben Verzeichnis wie WebServer-Hash auch) ab und markieren Sie die Datei als ausführbar.

Durch simplen Aufruf des Test-Skriptes können Sie jetzt WebServer-Hash komplett durchtesten.

                       
Viel Spaß mit "LiveCode Server" und diesem HTTPS-Server! Creative Commons Lizenzvertrag

Literaturhinweise

[1]
Joyent Inc.
node.js
Node.js ist zunächst einmal eine Plattform für in JavaScript geschriebene und dennoch äußerst leistungsfähige Netzwerk-Anwendungen. In Verbindung mit weiteren Technologien (wie z.B. Node-WebKit) kann Node.js aber auch für mehr als nur HTTP-Server eingesetzt werden.
[2]
Tj Holowaychuk
Express - node.js web application framework
Express ist ein schlankes Web Application Framework für Node.js. Dank seines "Baukastensystems" ermöglicht Express eine zügige Entwicklung von HTTP-Servern auf Basis von Node.js.
[3]
(RunRev Ltd.)
LiveCode | LiveCode Server Guide
Der LiveCode Server ist ein Interpreter für LiveCode-Skripte, der von der Kommandozeile aus gestartet wird (ohne grafische Benutzeroberfläche auskommt) und vor allem als CGI-Prozessor gedacht ist (auf diese Weise können Web-Seiten mit LiveCode bearbeitet werden - man muß also nicht mehr unbedingt PHP lernen).
[4]
(Defuse Security)
Secure Salted Password Hashing - How to do it Properly
Wer einen Server betreibt, der von seinen Benutzern eine Anmeldung verlangt, muss diese Sicherheits-relevanten Daten irgendwie persistieren. Diese Web-Seite beschreibt die potentiellen Probleme und bietet entsprechende Lösungen.