Location: PHPKode > scripts > Multi-OTP PHP class > multiotp.php
#!/usr/bin/php
<?php

/*********************************************************************
 *
 * MultiOTP PHP CLI header - Strong two-factor authentication PHP class
 * http://www.multiotp.net
 *
 * Donation are always welcome! Please check http://www.multiotp.net
 * and you will find the magic button ;-)
 *
 * If the name of this file is multiotp.php, it means that it is already
 * the result of the merge of the two files multiotp.cli.header.php and
 * multiotp.class.php
 *
 * The MultiOTP PHP CLI header is simply merged with the MultiOTP PHP
 * class in order to provide an authentication command line script.
 *
 * This script can be used as an external authentication provider with at
 * least the following RADIUS servers:
 *  - TekRADIUS, a free Radius server for Windows with MS-SQL backend
 *    (http:/www.tekradius.com)
 *  - TekRADIUS LT, a free Radius server for Windows with SQLite backend
 *    (http:/www.tekradius.com)
 *  - FreeRADIUS, a free Radius server implementation for Linux
 *    and *nix environments (http://freeradius.org)
 *
 * For Windows, you can also use the multiotp.exe file provided, which is
 * an embedded PHP interpreter together with the result of the merge.
 *
 *
 * LICENCE
 *
 *   Copyright (c) 2010, SysCo systemes de communication sa
 *   SysCo (tm) is a trademark of SysCo systemes de communication sa
 *   (http://www.sysco.ch)
 *   All rights reserved.
 * 
 *   This file is part of the MultiOTP PHP class
 *
 *   MultiOTP PHP class is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU Lesser General Public License as
 *   published by the Free Software Foundation, either version 3 of the License,
 *   or (at your option) any later version.
 * 
 *   MultiOTP PHP class is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Lesser General Public License for more details.
 * 
 *   You should have received a copy of the GNU Lesser General Public
 *   License along with MultiOTP PHP class.
 *   If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * @author: SysCo/al
 * @since CreationDate: 2010-06-08
 * @copyright (c) 2010 by SysCo systemes de communication sa
 * @version $LastChangedRevision: 3.0.0 $
 * @version $LastChangedDate: 2010-09-02 $
 * @version $LastChangedBy: SysCo/al $
 * @link $HeadURL: multiotp.cli.header.php $
 * @link http://www.multiotp.net
 * @link hide@address.com
 * Language: PHP 4.4.4 or higher
 *
 *
 * Command line usage
 *
 *   Type multiotp -help to have the full description of the options,
 *    and have a look at the readme.txt file for enhanced explanations
 *
 *
 * Return codes
 *
 *   0 OK: Token accepted
 *  11 INFO: User successfully created or updated
 *  12 INFO: User successfully deleted
 *  13 INFO: User PIN code successfully changed
 *  14 INFO: Token has been resynchronized successfully
 *  15 INFO: XML tokens definition file successfully imported
 *  19 INFO: Requested operation successfully done
 *  21 ERROR: User doesn't exist
 *  22 ERROR: User already exists
 *  23 ERROR: Invalid algorithm
 *  24 ERROR: User locked (too many tries)
 *  25 ERROR: User delayed (too many tries, but still a hope in a few minutes)
 *  26 ERROR: The time based token has already been used
 *  27 ERROR: Resynchronization of the token has failed
 *  28 ERROR: Unable to write the changes in the file
 *  29 ERROR: Token doesn't exist
 *  30 ERROR: At least one parameter is missing
 *  31 ERROR: XML tokens definition file doesn't exist
 *  32 ERROR: XML tokens definition file not successfully imported
 *  99 ERROR: Authentication failed (and other possible unknown errors)
 *
 *
 * Radius integration examples
 *
 *   Example 1 (TekRADIUS or TekRADIUS LT under Windows)
 *
 *     TekRADIUS supports a Default Username to be used when a matching user
 *     profile cannot be found for an incoming RADIUS authentication request.
 *     So a quick and easy way is to create in the TekRADIUS Manager a User
 *     named 'Default' that belongs to the existing 'Default' Group.
 *     Then add to this Default user the following attribute :
 *     Check  External-Executable  C:\multitop\multiotp.exe %ietf|1% %ietf|2%
 *
 *
 *   Example 2 (FreeRADIUS under Linux)
 *
 *     Define a DEFAULT entry in the /etc/freeradius/users file like this:
 *     DEFAULT Auth-Type = Accept
 *     Exec-Program-Wait = "/usr/local/bin/multiotp.php %{User-Name} %{User-Password}",
 *     Fall-Through = Yes,
 *     Reply-Message = "Hello, %{User-Name}"
 *
 *
 * External files created
 *
 *   Users database files in the subfolder called users
 *   Tokens database files in the subfolder called tokens
 *
 *
 * External file needed
 *
 *   Users database files in the subfolder called users
 *   Tokens database files in the subfolder called tokens
 *
 *
 * Special issues
 *
 *   If you need specific developements concerning strong authentication,
 *   do not hesistate to contact us per email at hide@address.com
 *
 *
 * Users feedbacks and comments
 *
 * 2010-08-20 BirdNet, C. Christophi
 *   Documentation enhancement proposal for the TekRADIUS part, thanks !
 *
 *
 * Change Log
 *
 *   2010-09-02 3.0.0  SysCo/al Adding tokens handling support, including importing XML tokens definition file
 *                              Enhanced flat database file format (multiotp is still compatible with old formats)
 *                              Internal method SetDataReadFlag renamed to SetUserDataReadFlag
 *                              Internal method GetDataReadFlag renamed to GetUserDataReadFlag
 *   2010-08-21 2.0.4  SysCo/al Enhancement in order to use an alternate php "compiler" for Windows command line
 *                              Documentation enhancement
 *   2010-08-18 2.0.3  SysCo/al Minor notice fix, define timezone if not defined (for embedded command line)
 *                              If user doesn't exist, do not create the related flat file after a check
 *   2010-07-21 2.0.2  SysCo/al Fix to create correctly the folders "uaers" and "log" if needed
 *   2010-07-19 2.0.1  SysCo/al Adding more information in the help text
 *   2010-07-19 2.0.0  SysCo/al New design using a class and a cli header stub
 *   2010-06-15 1.1.5  SysCo/al Adding OATH/TOTP support
 *   2010-06-15 1.1.4  SysCo/al Project renamed to multiotp to avoid overlapping
 *   2010-06-08 1.1.3  SysCo/al Typo in script folder detection
 *   2010-06-08 1.1.2  SysCo/al Typo in variable name
 *   2010-06-08 1.1.1  SysCo/al Status bar during resynchronization
 *   2010-06-08 1.1.0  SysCo/al Fix in the example, distribution not compressed
 *   2010-06-07 1.0.0  SysCo/al Initial implementation
 *
 *********************************************************************/

 
// Trick to have mostly the correct timezone in embedded command line version
// and to avoid error messages when using time functions

if (function_exists("date_default_timezone_get"))
{
    $actual_timezone = @date_default_timezone_get();
    if (function_exists("date_default_timezone_set"))
    {
        @date_default_timezone_set($actual_timezone);
    }
}


// Be sure that STDIN, STDOUT and STDERR are defined correctly for command line edition
if (!defined('STDIN'))
{
    define(STDIN, fopen('php://stdin', 'r'));
}
if (!defined('STDOUT'))
{
    define(STDOUT, fopen('php://stdout', 'w'));
}
if (!defined('STDERR'))
{
    define(STDERR, fopen('php://stderr', 'w'));
}


// Create a new Multiotp object
// The log and users subfolders are set by default under the folder of the script
$multiotp = new Multiotp();


// Set a specific encryption key for the tokens and users files
$multiotp->SetEncryptionKey('DefaultCliEncryptionKey');

// Initialize some variables
$command           = "check";
$display_help      = FALSE;
$display_status    = FALSE;
$prefix_pin        = FALSE;
$crlf              = chr(13).chr(10);
$result            = 99; // Unknown error
$token_id_creation = FALSE;

 
// Extract all parameters
$param_count = 0;
$all_args = array();

for ($arg_loop=1; $arg_loop <= $_SERVER["argc"]-1; $arg_loop++)
{
    if ("-check" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $command = "check";
    }
    elseif ("-create" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $command = "create";
    }
    elseif ("-debug" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $multiotp->EnableVerboseLog();
    }
    elseif ("-delete" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $command = "delete";
    }
    elseif ("-help" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $command = "help";
    }
    elseif ("-import-xml" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $command = "import-xml";
    }
    elseif ("-log" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $multiotp->EnableLog();
    }
    elseif ("-no-prefix-pin" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $prefix_pin = FALSE;
    }
    elseif ("-prefix-pin" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $prefix_pin = TRUE;
    }
    elseif ("-resync" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $command = "resync";
    }
    elseif ("-status" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $display_status = TRUE;
    }
    elseif ("-token-id" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $token_id_creation = TRUE;
    }
    elseif ("-update" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $command = "update";
    }
    elseif ("-update-pin" == strtolower($_SERVER["argv"][$arg_loop]))
    {
        $command = "update-pin";
    }
    elseif (("-version" == strtolower($_SERVER["argv"][$arg_loop])) || ("-v" == strtolower($_SERVER["argv"][$arg_loop])))
    {
        $command = "version";
    }
    else
    {
        $param_count++;
        $all_args[$param_count] = $_SERVER["argv"][$arg_loop];
    }
}

// Be sure that inexistant parameters are empty
for ($i = ($param_count+1); $i <= 20; $i++)
{
    $all_args[$i] = "";
}

// if not enough parameters, display help
if (($param_count < 1) && ($command != "version"))
{    $command = "help";
}

switch ($command)
{
    case "version":
        echo "multiotp ".$multiotp->GetVersion()." (".$multiotp->GetDate().")".$crlf;
        $result = 19;
        break;
    case "check";
        if  ($param_count < 2)
        {
            $result = 30; // ERROR: At least one parameter is missing
        }
        elseif (!$multiotp->ReadUserData($all_args[1]))
        {
            $result = 22; // ERROR: user doesn't exist.
        }
        else
        {
            $result = $multiotp->CheckToken($all_args[2]); // Result provided by the MultiOTP class
        }
        break;
    case "create":
    case "update":
        if (("create" == $command) && $multiotp->ReadUserData($all_args[1], TRUE))
        {
            $result = 22; // ERROR: user already exists.
        }
        elseif (("update" == $command) && (!$multiotp->ReadUserData($all_args[1])))
        {
            $result = 21; // ERROR: user doesn't exist.
        }
        elseif  ($param_count < 3)
        {
            $result = 30; // ERROR: At least one parameter is missing
        }
        else
        {
            $multiotp->SetUser($all_args[1]);
            $multiotp->SetUserPrefixPin($prefix_pin?1:0);
            
            if ($token_id_creation)
            {
                $key_id = $all_args[2];
                if (!$multiotp->ReadTokenData($key_id))
                {
                    $result = 29; // ERROR: token doesn't exist.
                }
                else
                {
                    $multiotp->SetUserKeyId($key_id);
                    if (!$multiotp->SetUserAlgorithm($multiotp->GetTokenAlgorithm()))
                    {
                        $result = 23; // ERROR: invalid algorithm
                    }
                    else
                    {
                        $multiotp->SetUserTokenSeed($multiotp->GetTokenSeed());
                        $multiotp->SetUserTokenNumberOfDigits($multiotp->GetTokenNumberOfDigits());
                        $multiotp->SetUserTokenTimeInterval($multiotp->GetTokenTimeInterval());
                        $multiotp->SetUserTokenLastEvent($multiotp->GetTokenLastEvent());
                        
                        $multiotp->SetUserPin($all_args[3]);
                        
                        if ($multiotp->WriteUserData())
                        {
                            $result = 11; // INFO: user successfully created or updated
                        }
                        else
                        {
                            $result = 28; // ERROR: Unable to write the changes in the file
                        }
                    }
                }
            }
            elseif (!$multiotp->SetUserAlgorithm($all_args[2]))
            {
                $result = 23; // ERROR: invalid algorithm
            }
            else
            {
                $multiotp->SetUserTokenSeed($all_args[3]);
                
                if  ($param_count < 4)
                {
                    $result = 30; // ERROR: At least one parameter is missing
                }
                else
                {
                    $multiotp->SetUserPin($all_args[4]);
                    if ("" == $all_args[5])
                    {
                        $all_args[5] = 6; // Default numnber of digits is set to 6
                    }
                    $multiotp->SetUserTokenNumberOfDigits($all_args[5]);
                    switch (strtoupper($all_args[2]))
                    {
                        // This is the time interval for mOTP
                        case "MOTP":
                            if ("" == $all_args[6])
                            {
                                $all_args[6] = 10; // Default windows value interval for mOTP
                            }
                            $multiotp->SetUserTokenTimeInterval($all_args[6]);
                            break;
                        // This is the time interval for TOTP
                        case "TOTP":
                            if ("" == $all_args[6])
                            {
                                $all_args[6] = 30; // Default windows value interval for TOTP
                            }
                            $multiotp->SetUserTokenTimeInterval($all_args[6]);
                            break;
                        // This is the next event for HOTP
                        case "HOTP":
                        default:
                            if ("" == $all_args[6])
                            {
                                $all_args[6] = 0; // Default next event
                            }
                            $multiotp->SetUserTokenLastEvent($all_args[6]-1);
                            // -1 because we are saving the last event in the user file database
                            break;
                    }
                    if ($multiotp->WriteUserData())
                    {
                        $result = 11; // INFO: user successfully created or updated
                    }
                    else
                    {
                        $result = 28; // ERROR: Unable to write the changes in the file
                    }
                }
            }
        }
        break;
    case "delete":
        $multiotp->SetUser($all_args[1]);
        if (!$multiotp->DeleteUser())
        {
            $result = 21; // ERROR: user doesn't exist.
        }
        else
        {
            $result = 12; // INFO: user successfully deleted.
        }
        break;
    case "resync":
        if  ($param_count < 3)
        {
            $result = 30; // ERROR: At least one parameter is missing
        }
        elseif (!$multiotp->ReadUserData($all_args[1]))
        {
            $result = 22; // ERROR: user doesn't exist.
        }
        else
        {
            $result = $multiotp->CheckToken($all_args[2], $all_args[3], $display_status); // Result provided by the MultiOTP class
        }
        break;
    case "update-pin":
        if  ($param_count < 2)
        {
            $result = 30; // ERROR: At least one parameter is missing
        }
        elseif (!$multiotp->ReadUserData($all_args[1]))
        {
            $result = 21; // ERROR: user doesn't exist.
        }
        else
        {
            $multiotp->SetUserPin($all_args[2]);
            if ($multiotp->WriteUserData())
            {
                $result = 13; // INFO: pin successfully changed
            }
        }
        break;
    case "import-xml":
        if (!file_exists($all_args[1]))
        {
            $result = 31; // ERROR: XML tokens definition file doesn't exist.
        }
        else
        {
            if ($multiotp->ImportTokensFromXml($all_args[1]))
            {
                $result = 15; // INFO: XML tokens definition file successfully imported
            }
            else
            {
                $result = 32; // ERROR: XML tokens definition file not successfully imported.
            }
        }
        break;
    default: // help or others
        echo "multiotp ".$multiotp->GetVersion()." (".$multiotp->GetDate().")".$crlf;
        echo $multiotp->GetCopyright().$crlf;
        echo $multiotp->GetWebsite().$crlf;
        echo $crlf;
        echo "multiotp will check if the token of a user is correct, based on a specified".$crlf;
        echo "algorithm (currently Mobile-OTP (http://motp.sf.net), OATH/HOTP (RFC 4226) ".$crlf;
		echo "and OATH/TOTP (HOTPTimeBased RFC 4226 extension) are implemented).".$crlf;
        echo $crlf;
        echo "If you are using the command line tool for Windows, be sure that the file".$crlf;
        echo "multiotp.php is not present in the directory, otherwise conflict may appears.".$crlf;
        echo $crlf;
        echo "If a token is locked (return code 24), you can resync the token to unlock.".$crlf;
        echo $crlf;
        echo "It will return 0 for a correct token, or an error code (11-99) otherwise.".$crlf;
        echo $crlf;
        echo "Return codes:".$crlf;
        
        reset($multiotp->_errors_text);
        while(list($key, $value) = each($multiotp->_errors_text))
        {
            echo substr('  '.$key,-2)." ".$value.$crlf;
        }
        echo $crlf;
        echo "Usage:".$crlf;
        echo " multiotp [-log] -import-xml xml_tokens_definition_file.xml".$crlf;
        echo " multiotp [-log] -create [-prefix-pin] user algo seed pin digits [pos|interval]".$crlf;
        echo " multiotp [-log] -create -token-id [-prefix-pin] user token-id pin".$crlf;
        echo " multiotp [-log] -resync [-status] user token1 token2 (two consecutive tokens)".$crlf;
        echo " multiotp [-log] -update-pin user pin".$crlf;
        echo " multiotp [-log] user token".$crlf;
        echo " multiotp -delete user".$crlf;
        echo $crlf;
        echo " token-id: id of the previously imported token to attribute to the user".$crlf;
        echo " user:     name of the user (should be the account name)".$crlf;
        echo " algo:     available algorithms are mOTP, HOTP and TOTP".$crlf;
        echo " seed:     hexadecimal seed of the token".$crlf;
        echo " pin:      private pin code of the user".$crlf;
        echo " digits:   number of digits given by the token".$crlf;
        echo " pos:      for HOTP algorithm, position of the next awaited event".$crlf;
        echo " interval: for mOTP and TOTP algorithms, token interval time in seconds".$crlf;
        echo $crlf;
        echo "Options:".$crlf;
        echo " -help        Display this help page".$crlf;
        echo " -version     Display the current version".$crlf;
        echo " -prefix-pin  The pin and the token must be typed merged by the user".$crlf;
        echo "              (if you pin is 1234 and your token displays 5556677,".$crlf;
        echo "               you will have to type 1234556677)".$crlf;
        echo " -status      Display a status bar during resynchronization".$crlf;
        echo " -log         Log operation in the log file (in the \log subdirectory)".$crlf;
        echo " -debug       Enhanced log information and code result on screen".$crlf;
        echo $crlf;
        echo "Examples:".$crlf;
        echo " multiotp -log -debug -import-xml tokens.xml".$crlf;
        echo " multiotp -log -create jimmy mOTP 004f5a158bca13984d349a7f23 1234 6 10".$crlf;
        echo " multiotp -create -prefix-pin alan TOTP 3683453456769abc3452 2233 6 60".$crlf;
        echo " multiotp -create -prefix-pin anna TOTP 56821bac24fbd2343393 4455 6 30".$crlf;
        echo " multiotp -create -prefix-pin john HOTP 31323334353637383930 5678 6 137".$crlf;
        echo " multiotp -create -token-id -prefix-pin rick 2010090201901 2345".$crlf;
        echo " multiotp -resync john 5678456789 5678345231".$crlf;
        echo " multiotp -resync -status anna 4455487352 4455983513".$crlf;
        echo " multiotp -update-pin alan 4417".$crlf;
        echo " multiotp -log -debug jimmy ea2315".$crlf;
        echo " multiotp -log anna 546078".$crlf;
        echo " multiotp john 5678124578".$crlf;
        echo $crlf;
        echo "When used with TekRADIUS (http://www.tekradius.com) the External-Executable".$crlf;
        echo "must be called like this: C:\multitop\multiotp.exe %ietf|1% %ietf|2%".$crlf;
        echo $crlf;
        break;
}
if ($multiotp->GetVerboseFlag())
{
    echo $result;
    if (isset($multiotp->_errors_text[$result]))
    {
        echo " ".$multiotp->_errors_text[$result];
    }
}
exit($result);

?><?php

/*********************************************************************
 *
 * MultiOTP PHP class - Strong two-factor authentication PHP class
 * http://www.multiotp.net
 *
 * Donation are always welcome! Please check http://www.multiotp.net
 * and you will find the magic button ;-)
 *
 * The MultiOTP class is a strong authentication class in pure PHP
 * that supports the following algorithms:
 *  - mOTP (http://motp.sourceforge.net)
 *  - OATH/HOTP RFC 4226 (http://www.ietf.org/rfc/rfc4226.txt)
 *  - OATH/TOTP HOTPTimeBased RFC 4226 extension
 *
 * This class can be used as is in your own PHP project, but it can also be
 * used easily as an external authentication provider with at least the
 * following RADIUS servers (using the multiotp command line script):
 *  - TekRADIUS, a free Radius server for Windows with MS-SQL backend
 *    (http:/www.tekradius.com)
 *  - TekRADIUS LT, a free Radius server for Windows with SQLite backend
 *    (http:/www.tekradius.com)
 *  - FreeRADIUS, a free Radius server implementation for Linux
 *    and *nix environments (http://freeradius.org)
 *
 *
 * LICENCE
 *
 *   Copyright (c) 2010, SysCo systemes de communication sa
 *   SysCo (tm) is a trademark of SysCo systemes de communication sa
 *   (http://www.sysco.ch)
 *   All rights reserved.
 * 
 *   This file is part of the MultiOTP PHP class
 *
 *   MultiOTP PHP class is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU Lesser General Public License as
 *   published by the Free Software Foundation, either version 3 of the License,
 *   or (at your option) any later version.
 * 
 *   MultiOTP PHP class is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Lesser General Public License for more details.
 * 
 *   You should have received a copy of the GNU Lesser General Public
 *   License along with MultiOTP PHP class.
 *   If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * @author: SysCo/al
 * @since CreationDate: 2010-06-08
 * @copyright (c) 2010 by SysCo systemes de communication sa
 * @version $LastChangedRevision: 3.0.0 $
 * @version $LastChangedDate: 2010-09-02 $
 * @version $LastChangedBy: SysCo/al $
 * @link $HeadURL: multiotp.class.php $
 * @link http://www.multiotp.net
 * @link hide@address.com
 * Language: PHP 4.4.4 or higher
 *
 *
 * Usage
 *
 *   require_once('multiotp.class.php');
 *   $multiotp = new Multiotp();
 *   $multiotp->SetUser('user);
 *   $result = $multiotp->CheckToken('token');
 *
 *
 * Examples
 *
 *   Example 1
 *     <?php
 *         require_once('multiotp.class.php');
 *         $multiotp = new Multiotp();
 *         $multiotp->SetUser('user');
 *         if ($multiotp->CheckToken('token'))
 *         {
 *             echo "Authentication accepted.";
 *         }
 *         else
 *         {
 *             echo "Authentication rejected.";
 *         }
 *     ?>
 *
 *   Example 2
 *     <?php
 *         require_once('multiotp.class.php');
 *         $multiotp = new Multiotp();
 *         // Set a specific encryption key for the tokens and users files
 *         $multiotp->SetEncryptionKey('MyEncryptionKey');
 *         // Set specific attributes to encrypt in the flat files
 *         $multiotp->SetAttributesToEncrypt('*user_pin*token_seed*token_serial*');
 *         $multiotp->SetUser('user');
 *         if ($multiotp->CheckToken('token'))
 *         {
 *             echo "Authentication accepted.";
 *         }
 *         else
 *         {
 *             echo "Authentication rejected.";
 *         }
 *     ?>
 *
 *   For examples on how to integrate it with radius servers, please have a look
 *   to the readme.txt file or read the header of the multiotp.cli.header.php file.
 *
 *
 * External files created
 *
 *   Users database files in the subfolder called users
 *   Tokens database files in the subfolder called tokens
 *
 *
 * External file needed
 *
 *   Users database files in the subfolder called users
 *   Tokens database files in the subfolder called tokens
 *
 *
 * Special issues
 *
 *   If you need specific developements concerning strong authentication,
 *   do not hesistate to contact us per email at hide@address.com
 *
 *
 * Other related ressources
 *
 *   Mobile-OTP: Strong Two-Factor Authentication with Mobile Phones:
 *     http://motp.sourceforge.net
 *
 *   The Initiative for Open Authentication:
 *     http://www.openauthentication.org
 *
 *   TekRADIUS, a free RADIUS server for windows, available in two versions (MS-SQL and SQLite):
 *     http://www.tekradius.com
 *
 *   FreeRADIUS, a free Radius server implementation for Linux and *nix environments:
 *     http://www.freeradius.org
 *
 *   Additional Portable Symmetric Key Container (PSKC) Algorithm Profiles
 *     http://tools.ietf.org/html/draft-hoyer-keyprov-pskc-algorithm-profiles-00
 *
 *
 * Users feedbacks and comments
 *
 * 2010-08-20 BirdNet, C. Christophi
 *   Documentation enhancement proposal for the TekRADIUS part, thanks !
 *
 * 2010-07-19 SysCo/al
 *   Well, as requested by some users, the new "class" design is done, enjoy !
 *
 *
 * Todos
 *
 *   Add more comments in the main class file
 *   Add more information in the log
 *   Add more verbose information in the log
 *
 *
 * Change Log
 *
 *   2010-09-02 3.0.0  SysCo/al Adding tokens handling support, including importing XML tokens definition file
 *                               (http://tools.ietf.org/html/draft-hoyer-keyprov-pskc-algorithm-profiles-00)
 *                              Enhanced flat database file format (multiotp is still compatible with old versions)
 *                              Internal method SetDataReadFlag renamed to SetUserDataReadFlag
 *                              Internal method GetDataReadFlag renamed to GetUserDataReadFlag
 *   2010-08-21 2.0.4  SysCo/al Enhancement in order to use an alternate php "compiler" for Windows command line
 *                              Documentation enhancement
 *   2010-08-18 2.0.3  SysCo/al Minor notice fix
 *   2010-07-21 2.0.2  SysCo/al Fix to create correctly the folders "uaers" and "log" if needed
 *   2010-07-19 2.0.1  SysCo/al Foreach was not working well in "compiled" Windows command line
 *   2010-07-19 2.0.0  SysCo/al New design using a class, mOTP support, cleaning of the code
 *   2010-06-15 1.1.5  SysCo/al Adding OATH/TOTP support
 *   2010-06-15 1.1.4  SysCo/al Project renamed to multiotp to avoid overlapping
 *   2010-06-08 1.1.3  SysCo/al Typo in script folder detection
 *   2010-06-08 1.1.2  SysCo/al Typo in variable name
 *   2010-06-08 1.1.1  SysCo/al Status bar during resynchronization
 *   2010-06-08 1.1.0  SysCo/al Fix in the example, distribution not compressed
 *   2010-06-07 1.0.0  SysCo/al Initial implementation
 *
 *********************************************************************/

 
/*********************************************************************
 *
 * Name: Multiotp
 * MultiOTP PHP class
 *
 * Creation 2010-07-18
 * Update 2010-09-02
 * @package multiotp
 * @version 3.0.0
 * @author SysCo/al
 *
 *********************************************************************/
class Multiotp
{

    var $_version;                  // Current version of the library
    var $_date;                     // Current date of the library
    var $_copyright;                // Copyright message of the library
    var $_website;                  // Website of the library

    var $_valid_algorithms;         // String containing valid algorithms to be used
    var $_attributes_to_encrypt;    // Attributes to encrypt in the flat files
    var $_encryption_key;           // Symetric encryption key for the users files and the tokens files
    var $_errors_text;              // An array containing errors text description
    var $_user;                     // Current user, case insensitive
    var $_user_data;                // An array with all the user related info
    var $_user_data_read_flag;      // Indicate if the user data has been read from the database file
    var $_users_folder;             // Folder where users definition files are stored
    var $_token;                    // Current token, case insensitive
    var $_token_data;               // An array with all the user related info
    var $_token_data_read_flag;     // Indicate if the user data has been read from the database file
    var $_tokens_folder;            // Folder where users definition files are stored
    var $_log_folder;               // Folder where log file is written
    var $_log_file_name;            // Name of the log file
    var $_log_flag;                 // Enable or disable the log
    var $_log_header_written;       // Internal flag to know if the header was already written or not in the log file
    var $_log_verbose_flag;         // Enable or disable the verbose mode for the log
    var $_max_event_window;         // Maximum event window to be accepted
    var $_max_event_resync_window;  // Maximum event window to be accepted for resync
    var $_max_time_window;          // Maximum time window to be accepted, in seconds (+/-)
    var $_max_time_resync_window;   // Maximum time window to be accepted for resync (+/-)
    var $_max_delayed_failures;     // Number of consecutive failures before delaying the next request
    var $_failure_delayed_time;     // Number of seconds to wait before releasing the failure delay
    var $_max_block_failures;       // Number of consecutive failures before blocking the token. A blocked token needs a resync.

    /*********************************************************************
     *
     * Name: Multiotp
     * Short description: Multiotp class constructor
     *
     * Creation 2010-07-18
     * Update 2010-09-02
     * @package multiotp
     * @version 3.0.0
     * @author SysCo/al
     * @return  void
     *********************************************************************/
    function Multiotp()
    {
        $this->_version                  = "3.0.0";
        $this->_date                     = "2010-09-02";
        $this->_copyright                = "(c) 2010 SysCo systemes de communication sa";
        $this->_website                  = "http://www.multiotp.net";
        
        $this->_log_header_written       = FALSE; // Flag to know if the header has already been written in the log file or not
        $this->_valid_algorithms        = '*mOTP*HOTP*TOTP*';
        $this->_attributes_to_encrypt   = '*user_pin*token_seed*';
        $this->_encryption_key          = 'MuLtIoTpEnCrYpTiOn';
        $this->_log_file_name           = "multiotp.log";
        $this->_log_flag                = FALSE;
        $this->_log_verbose_flag        = FALSE;
        $this->_max_event_window        = 100; // Number of events accepted for event based algorithm(s) token
        $this->_max_event_resync_window = 10000; // NUmber of events accepted to sync event based algorithm(s) token
        $this->_max_time_window         = 8000; // a little bit more than +/- 2 hours
        $this->_max_time_resync_window  = 90000; // more than +/- one day
        $this->_max_delayed_failures    = 3; // Number of authorized failures before locking for a fixed delay
        $this->_failure_delayed_time    = 300; // Locking delay between two trials after "_max_delayed_failures" failures
        $this->_max_block_failures      = 6; // Number of login trial failures that will block the user
        $this->_user                    = ""; // Name of the current user to authenticate
        $this->_user_data_read_flag     = FALSE; // Flag to know if the data concerning the current user has been read
        $this->_users_folder            = ""; // Folders which contain the users flat files
        $this->_log_folder              = ""; // Folder which contains the log file

        // Initialize the user array
    
        // User pin
        $this->_user_data['user_pin'] = '';
        
        // Algorithm used by the token
        $this->_user_data['algorithm'] = '';
        
        // Time interval in seconds for a time based token
        $this->_user_data['time_interval'] = 0;
        
        // Number of digits returned by the token
        $this->_user_data['number_of_digits'] = 6;
        
        // Request the pin as a prefix of the rturned token value
        $this->_user_data['request_prefix_pin'] = 0;
        
        // Last successful login
        $this->_user_data['last_login'] =  0;
        
        // Last successful event
        $this->_user_data['last_event'] = -1;
        
        // Last error login
        $this->_user_data['last_error'] =  0;
        
        // Delta time in seconds for a time based token
        $this->_user_data['delta_time'] = 0;
        
        // Key identification number, if any
        $this->_user_data['key_id'] = '';

        // Token seed, default set to the RFC test seed
        $this->_user_data['token_seed'] = '3132333435363738393031323334353637383930';

        // Login error counter
        $this->_user_data['error_counter'] = 0;

        // Token locked
        $this->_user_data['locked'] = 0;
        
        
        // Initialize the errors text array
        $this->_errors_text[0] = "OK: Token accepted";

        $this->_errors_text[11] = "INFO: User successfully created or updated";
        $this->_errors_text[12] = "INFO: User successfully deleted";
        $this->_errors_text[13] = "INFO: User PIN code successfully changed";
        $this->_errors_text[14] = "INFO: Token has been resynchronized successfully";
        $this->_errors_text[15] = "INFO: XML tokens definition file successfully imported";
        $this->_errors_text[19] = "INFO: Requested operation successfully done";

        $this->_errors_text[21] = "ERROR: User doesn't exist";
        $this->_errors_text[22] = "ERROR: User already exists";
        $this->_errors_text[23] = "ERROR: Invalid algorithm";
        $this->_errors_text[24] = "ERROR: User locked (too many tries)";
        $this->_errors_text[25] = "ERROR: User delayed (too many tries, but still a hope in a few minutes)";
        $this->_errors_text[26] = "ERROR: The time based token has already been used";
        $this->_errors_text[27] = "ERROR: Resynchronization of the token has failed";
        $this->_errors_text[28] = "ERROR: Unable to write the changes in the file";
        $this->_errors_text[29] = "ERROR: Token doesn't exist";
        $this->_errors_text[30] = "ERROR: At least one parameter is missing";
        $this->_errors_text[31] = "ERROR: XML tokens definition file doesn't exist";
        $this->_errors_text[32] = "ERROR: XML tokens definition file not successfully imported";
        $this->_errors_text[99] = "ERROR: Authentication failed (and other possible unknown errors)";
    }


    /*********************************************************************
     *
     * Name: ShowStatus
     * Short description: Show a progress status bar in the console
     *
     * Creation 2010
     * Source: http://brian.moonspot.net/status_bar.php.txt
     * @author Copyright (c) 2010, dealnews.com, Inc. - All rights reserved.
     *
     * @param   int     $done   how many items are completed
     * @param   int     $total  how many items are to be done total
     * @param   int     $size   optional size of the status bar
     * @return  void
     *
     * Redistribution and use in source and binary forms, with or without
     * modification, are permitted provided that the following conditions are met:
     *
     * - Redistributions of source code must retain the above copyright notice,
     *   this list of conditions and the following disclaimer.
     * - Redistributions in binary form must reproduce the above copyright
     *   notice, this list of conditions and the following disclaimer in the
     *   documentation and/or other materials provided with the distribution.
     * - Neither the name of dealnews.com, Inc. nor the names of its contributors
     *   may be used to endorse or promote products derived from this software
     *   without specific prior written permission.
     *
     *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
     *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
     *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
     *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
     *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
     *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
     *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
     *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
     *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
     *  POSSIBILITY OF SUCH DAMAGE.
     *
     *
     * Usage
     * 
     * for($x=1;$x<=100;$x++)
     * {
     *     ShowStatus($x, 100);
     *     usleep(100000);
     * }
     *
     * @param   int     $done   how many items are completed
     * @param   int     $total  how many items are to be done total
     * @param   int     $size   optional size of the status bar
     * @return  void
     *
     *********************************************************************/
    function ShowStatus($done, $total, $size=30)
    {

        static $start_time;

        // if we go over our bound, just ignore it
        if($done > $total) return;

        if(empty($start_time)) $start_time=time();
        $now = time();

        $perc=(double)($done/$total);

        $bar=floor($perc*$size);

        $status_bar="\r[";
        $status_bar.=str_repeat("=", $bar);
        if($bar<$size)
        {
            $status_bar.=">";
            // $status_bar.=str_repeat(" ", $size-$bar);
            $status_bar.=str_repeat("-", $size-$bar);
        }
        else
        {
            $status_bar.="=";
        }

        $disp=number_format($perc*100, 0);

        $status_bar.="] $disp%  $done/$total";

        $rate = ($now-$start_time)/$done;
        $left = $total - $done;
        $eta = round($rate * $left, 2);

        $elapsed = $now - $start_time;

        // $status_bar.= " remaining: ".number_format($eta)." sec.  elapsed: ".number_format($elapsed)." sec.";

        echo "$status_bar  ";

        flush();

        // when done, send a newline
        if($done == $total)
        {
            echo "\n";
        }
    }
    
    
    // Defining this custom function if hash_hmac is not available in the actual configuration
    function HashHmac($algo, $data, $key, $raw_output = false)
    {
        $algo = strtolower($algo);
        $pack = 'H'.strlen($algo('test'));
        $size = 64;
        $opad = str_repeat(chr(0x5C), $size);
        $ipad = str_repeat(chr(0x36), $size);

        if (strlen($key) > $size)
        {
            $key = str_pad(pack($pack, $algo($key)), $size, chr(0x00));
        }
        else
        {
            $key = str_pad($key, $size, chr(0x00));
        }

        for ($i = 0; $i < strlen($key) - 1; $i++)
        {
            $opad[$i] = $opad[$i] ^ $key[$i];
            $ipad[$i] = $ipad[$i] ^ $key[$i];
        }

        $output = $algo($opad.pack($pack, $algo($ipad.$data)));

        return ($raw_output) ? pack($pack, $output) : $output;
    }


    // Defining this custom function if str_split is not available in the actual configuration
    function StrSplit($string, $length = 1)
    {
        if ($length <= 0)
        {
            trigger_error(__FUNCTION__."(): The the length of each segment must be greater then zero:", E_USER_WARNING);
            return false;
        }
        $splitted  = array();
        $str_length = strlen($string);
        $i = 0;
        if ($length == 1)
        {
            while ($str_length--)
            {
                $splitted[$i] = $string[$i++];
            }
        }
        else
        {
            $j = $i;
            while ($str_length > 0)
            {
                $splitted[$j++] = substr($string, $i, $length);
                $str_length -= $length;
                $i += $length;
            }
        }
        return $splitted;
    }
    
    
    function GetVersion()
    {
        return $this->_version;
    }
    
    
    function GetDate()
    {
        return $this->_date;
    }


    function GetCopyright()
    {
        return $this->_copyright;
    }


    function GetWebsite()
    {
        return $this->_website;
    }

    
    function SetMaxTimeWindow($time_window)
    {
        $this->_max_time_window = intval($time_window);
    }


    function GetMaxTimeWindow()
    {
        return $this->_max_time_window;
    }


    function SetMaxTimeResyncWindow($time_resync_window)
    {
        $this->_max_time_resync_window = intval($time_resync_window);
    }


    function GetMaxTimeResyncWindow()
    {
        return $this->_max_time_resync_window;
    }


    function SetMaxEventWindow($event_window)
    {
        $this->_max_time_window = intval($event_window);
    }


    function GetMaxEventWindow()
    {
        return $this->_max_event_window;
    }
    
    
    function SetMaxEventResyncWindow($event_resync_window)
    {
        $this->_max_event_resync_window = intval($event_resync_window);
    }


    function GetMaxEventResyncWindow()
    {
        return $this->_max_event_resync_window;
    }
    
    
    function SetMaxBlockFailures($max_failures)
    {
        $this->_max_block_failures = $max_failures;
    }


    function GetMaxBlockFailures()
    {
        return $this->_max_block_failures;
    }

    
    /*********************************************************************
     *
     * Name: ComputeMotp
     * Short description: Compute the mOTP result
     *
     * Creation 2010-06-07
     * Update 2010-07-19
     * @package multiotp
     * @version 2.0.0
     * @author SysCo/al
     *
     * @param   string  $seed_and_pin  Key used to compute the mOTP result
     * @param   int     $timestep      Timestep used to calculate the token
     * @param   int     $token_size    Token size
     * @return  string                 mOTP result
     *
     *********************************************************************/
    function ComputeMotp($seed_and_pin, $timestep, $token_size)
    {
        return substr(md5($timestep.$seed_and_pin),0,$token_size);
    }


    /*********************************************************************
     *
     * Name: ComputeOathHotp
     * Short description: Compute the OATH defined hash
     *
     * Creation 2010-06-07
     * Update 2010-07-19
     * @package multiotp
     * @version 2.0.0
     * @author SysCo/al
     *
     * @param   string  $key      Key used to compute the OATH hash
     * @param   int     $counter  Counter position
     * @return  string            Full OATH hash
     *
     *********************************************************************/
    function ComputeOathHotp($key, $counter)
    {
        // Counter
        //the counter value can be more than one byte long, so we need to go multiple times
        $cur_counter = array(0,0,0,0,0,0,0,0);
        for($i=7;$i>=0;$i--)
        {
            $cur_counter[$i] = pack ('C*', $counter);
            $counter = $counter >> 8;
        }
        $bin_counter = implode($cur_counter);
        // Pad to 8 chars
        if (strlen ($bin_counter) < 8)
        {
            $bin_counter = str_repeat(chr(0), 8 - strlen($bin_counter)) . $bin_counter;
        }

        // HMAC hash
        $hash = $this->HashHmac('sha1', $bin_counter, $key);
        return $hash;
    }

    
    /*********************************************************************
     *
     * Name: ComputeOathTruncate
     * Short description: Truncate the result as defined by the OATH
     *
     * Creation 2010-06-07
     * Update 2010-07-19
     * @package multiotp
     * @version 2.0.0
     * @author SysCo/al
     *
     * @param   string  $hash     Full OATH hash to be truncated
     * @param   int     $length   Length of the result token
     * @return  string            Truncated OATH hash
     *
     *********************************************************************/
    function ComputeOathTruncate($hash, $length = 6)
    {
        // Convert to decimal
        foreach($this->StrSplit($hash,2) as $hex)
        {
            $hmac_result[]=hexdec($hex);
        }

        // Find offset
        $offset = $hmac_result[19] & 0xf;

        // Algorithm from RFC
        return
        substr(str_repeat('0',$length).((
            (($hmac_result[$offset+0] & 0x7f) << 24 ) |
            (($hmac_result[$offset+1] & 0xff) << 16 ) |
            (($hmac_result[$offset+2] & 0xff) << 8 ) |
            ($hmac_result[$offset+3] & 0xff)
        ) % pow(10,$length)),-$length); // & 0x7FFFFFFF before the pow()
    }

    
    /*********************************************************************
     *
     * Name: ConvertHex2Bin
     * Short description: Convert hexadecimal string to binary content
     *
     * Creation 2010-06-07
     * Update 2010-07-19
     * @package multiotp
     * @version 2.0.0
     * @author SysCo/al
     *
     * @param   string  $hexdata  Full string in hex format to convert
     * @return  string            Converted binary content
     *
     *********************************************************************/
    function ConvertHex2Bin($hexdata)
    {
        $bindata = "";
        for ($i=0;$i<strlen($hexdata);$i+=2)
        {
            $bindata.=chr(hexdec(substr($hexdata,$i,2)));
        }
        return $bindata;
    }


    function SetEncryptionKey($key)
    {
        $this->_encryption_key = $key;
    }
    
    
    function Encrypt($key, $value)
    {
        $result = '';
        if (strlen($this->_encryption_key) > 0)
        {
            for ($i=0;  $i < strlen($value); $i++)
            {
                $encrypt_char = ord(substr($this->_encryption_key,$i % strlen($this->_encryption_key),1));
                $key_char = ord(substr($key,$i % strlen($key),1));
                $result .= chr($encrypt_char^$key_char^ord(substr($value,$i,1)));
            }
            $result = base64_encode($result);
        }
        else
        {
            $result = $value;
        }
        return $result;
    }
    
    
    function Decrypt($key, $value)
    {
        $result = '';
        if (strlen($this->_encryption_key) > 0)
        {
            $value_to_decrypt = base64_decode($value);
            for ($i=0;  $i < strlen($value_to_decrypt); $i++)
            {
                $encrypt_char = ord(substr($this->_encryption_key,$i % strlen($this->_encryption_key),1));
                $key_char = ord(substr($key,$i % strlen($key),1));
                $result .= chr($encrypt_char^$key_char^ord(substr($value_to_decrypt,$i,1)));
            }
        }
        else
        {
            $result = $value;
        }
        return $result;
    }

    
    function SetMaxDelayedFailures($failures)
    {
        $this->_max_delayed_failures = $failures;
    }

    
    function GetMaxDelayedFailures()
    {
        return $this->_max_delayed_failures;
    }


    function SetMaxDelayedTime($seconds)
    {
        $this->_failure_delayed_time = $seconds;
    }

    
    function GetMaxDelayedTime()
    {
        return $this->_failure_delayed_time;
    }

    
    function SetUser($user)
    {
        $this->_user = $user;
        $this->SetUserDataReadFlag(FALSE);
    }


    function GetUser()
    {
        return $this->_user;
    }

    
    function SetUserDataReadFlag($flag)
    {
        $this->_user_data_read_flag = $flag;
    }
    
    
    function GetUserDataReadFlag()
    {
        return $this->_user_data_read_flag;
    }
    

    function SetUserPrefixPin($value)
    {
        $this->_user_data['request_prefix_pin'] = $value;
    }

    
    function GetUserPrefixPin()
    {
        return $this->_user_data['request_prefix_pin'];
    }

    
    function SetUserAlgorithm($algorithm)
    {
        $result = FALSE;
        if (FALSE === strpos(strtoupper($this->_valid_algorithms), strtoupper('*'.$algorithm.'*')))
        {
            $this->WriteLog("Error: ".$algorithm." algorithm is unknown");
        }
        else
        {
            $this->_user_data['algorithm'] = $algorithm;
            $result = TRUE;
        }
        return $result;
    }


    function GetUserAlgorithm()
    {
        return $this->_user_data['algorithm'];
    }


    function SetUserTokenSeed($seed)
    {
        $this->_user_data['token_seed'] = $seed;
    }

    
    function GetUserTokenSeed()
    {
        return $this->_user_data['token_seed'];
    }

    
    function SetUserPin($pin)
    {
        $this->_user_data['user_pin'] = $pin;
    }
    
    
    function GetUserPin()
    {
        return $this->_user_data['user_pin'];
    }

    
    function SetUserTokenDeltaTime($delta_time)
    {
        $this->_user_data['delta_time'] = $delta_time;
    }
    
    
    function GetUserTokenDeltaTime()
    {
        return $this->_user_data['delta_time'];
    }

    
    function SetUserKeyId($key_id)
    {
        $this->_user_data['key_id'] = $key_id;
    }
    
    
    function GetUserKeyId()
    {
        return $this->_user_data['key_id'];
    }

    
    function SetUserTokenNumberOfDigits($number_of_digits)
    {
        $this->_user_data['number_of_digits'] = $number_of_digits;
    }
    
    
    function GetUserTokenNumberOfDigits()
    {
        return $this->_user_data['number_of_digits'];
    }


    function SetUserTokenTimeInterval($interval)
    {
        if (intval($interval) > 0)
        {
            $this->_user_data['time_interval'] = intval($interval);
        }
    }
    
    
    function GetUserTokenTimeInterval()
    {
        return $this->_user_data['time_interval'];
    }


    function SetUserTokenLastEvent($last_event)
    {
        $this->_user_data['last_event'] = $last_event;
    }
    
    
    function GetUserTokenLastEvent()
    {
        return $this->_user_data['last_event'];
    }

    
    function SetUserTokenLastLogin($time)
    {
        $this->_user_data['last_login'] = $time;
    }
    
    
    function GetUserTokenLastLogin()
    {
        return $this->_user_data['last_login'];
    }


    function SetUserTokenLastError($time)
    {
        $this->_user_data['last_error'] = $time;
    }
    
    
    function GetUserTokenLastError()
    {
        return $this->_user_data['last_error'];
    }


    function SetUserLocked($locked)
    {
        $this->_user_data['locked'] = $locked;
    }
    
    
    function GetUserLocked()
    {
        return $this->_user_data['locked'];
    }


    function SetUserErrorCounter($counter)
    {
        $this->_user_data['error_counter'] = $counter;
    }
    
    
    function GetUserErrorCounter()
    {
        return $this->_user_data['error_counter'];
    }

    
    function SetToken($token)
    {
        $this->_token = $token;
        $this->SetTokenDataReadFlag(FALSE);
    }


    function GetToken()
    {
        return $this->_token;
    }

    
    function SetTokenDataReadFlag($flag)
    {
        $this->_token_data_read_flag = $flag;
    }
    
    
    function GetTokenDataReadFlag()
    {
        return $this->_token_data_read_flag;
    }
    
    
    function GetScriptFolder()
    {
        // Detect the current folder, change Windows notation to universal notation if needed
        $current_folder = $this->ConvertToUnixPath(getcwd());
        $current_script_folder = $this->ConvertToUnixPath($_SERVER["argv"][0]);
        if ("" == (trim($current_script_folder)))
        {
            $current_script_folder = $_SERVER['SCRIPT_FILENAME'];
        }
        
        if (FALSE === strpos($current_script_folder,"/"))
        {
            $current_script_folder_detected = dirname($current_folder."/fake.file");
        }
        else
        {
            $current_script_folder_detected = dirname($current_script_folder);
        }

        if (substr($current_script_folder_detected,-1) != "/")
        {
            $current_script_folder_detected.="/";
        }
        return $this->ConvertToWindowsPathIfNeeded($current_script_folder_detected);
    }

    
    function ConvertToUnixPath($path)
    {
        return str_replace("\\","/",$path);
    }

    
    function ConvertToWindowsPathIfNeeded($path)
    {
        $result = $path;
        if (FALSE !== strpos($result,":"))
        {
            $result = str_replace("/","\\",$result);
        }
        return $result;
    }

    
    function SetLogFolder($folder)
    {
        $new_folder = $this->ConvertToUnixPath($folder);
        if (substr($new_folder,-1) != "/")
        {
            $new_folder.="/";
        }
        $new_folder = $this->ConvertToWindowsPathIfNeeded($new_folder);
        $this->_log_folder = $new_folder;
        if (!file_exists($new_folder))
        {
            @mkdir($new_folder);
        }
    }


    function GetLogFolder()
    {
        if ("" == $this->_log_folder)
        {
            $this->SetLogFolder($this->GetScriptFolder()."log/");
        }
        return $this->ConvertToWindowsPathIfNeeded($this->_log_folder);
    }


    function WriteLog($log_info)
    {
        if ($this->_log_flag)
        {
            if (!file_exists($this->GetLogFolder()))
            {
                @mkdir($this->_log_flag);
            }
            $log_file_handle = fopen($this->GetLogFolder().$this->_log_file_name,"ab+");
            if (!$this->_log_header_written)
            {
                fwrite($log_file_handle,str_repeat("=",40)."\n");
                fwrite($log_file_handle,'multiotp '.$this->GetVersion()."\n");
                $this->_log_header_written = TRUE;
            }
            fwrite($log_file_handle,date("Y-m-d H:i:s")." ".$log_info."\n");
            fclose($log_file_handle);
        }
    }
    
    
    function EnableLog()
    {
        $this->_log_flag = TRUE;
        if ("" == $this->_log_folder)
        {
            $this->SetLogFolder($this->GetScriptFolder()."log/");
        }
    }

    
    function DisableLog()
    {
        $this->_log_flag = FALSE;
    }


    function EnableVerboseLog()
    {
        $this->EnableLog();
        $this->_log_verbose_flag = TRUE;
    }

    
    function DisableVerboseLog()
    {
        $this->_log_verbose_flag = FALSE;
    }


    function GetVerboseFlag()
    {
        return $this->_log_verbose_flag;
    }

    
    function SetAttributesToEncrypt($attributes_to_encrypt)
    {
        $this->_attributes_to_encrypt = $attributes_to_encrypt;
    }


    function GetAttributesToEncrypt()
    {
        return $this->_attributes_to_encrypt;
    }
    
    
    
    function SetUsersFolder($folder)
    {
        $new_folder = $this->ConvertToUnixPath($folder);
        if (substr($new_folder,-1) != "/")
        {
            $new_folder.="/";
        }
        $new_folder = $this->ConvertToWindowsPathIfNeeded($new_folder);
        $this->_users_folder = $new_folder;
        if (!file_exists($new_folder))
        {
            if (!@mkdir($new_folder))
            {
                $this->WriteLog("Error: unable to create the missing users folder ".$new_folder);
            }
        }
    }

    
    function GetUsersFolder()
    {
        if ("" == $this->_users_folder)
        {
            $this->SetUsersFolder($this->GetScriptFolder()."users/");
        }
        return $this->ConvertToWindowsPathIfNeeded($this->_users_folder);
    }

    
    function SetTokenManufacturer($manufacturer)
    {
        $this->_token_data['manufacturer'] = $manufacturer;
    }


    function GetTokenManufacturer()
    {
        return $this->_token_data['manufacturer'];
    }
    
    
    function SetTokenSerialNumber($token_serial)
    {
        $this->_token_data['token_serial'] = $token_serial;
    }


    function GetTokenSerialNumber()
    {
        return $this->_token_data['token_serial'];
    }
    
    
    function SetTokenIssuer($issuer)
    {
        $this->_token_data['issuer'] = $issuer;
    }


    function GetTokenIssuer()
    {
        return $this->_token_data['issuer'];
    }
    
    
    function SetTokenKeyAlgorithm($key_algorithm)
    {
        $this->_token_data['key_algorithm'] = $key_algorithm;
    }


    function GetTokenKeyAlgorithm()
    {
        return $this->_token_data['key_algorithm'];
    }
    
    
    function SetTokenAlgorithm($algorithm)
    {
        $this->_token_data['algorithm'] = $algorithm;
    }


    function GetTokenAlgorithm()
    {
        return $this->_token_data['algorithm'];
    }
    
    
    function SetTokenOtp($otp)
    {
        $this->_token_data['otp'] = $otp;
    }


    function GetTokenOtp()
    {
        return $this->_token_data['otp'];
    }
    
    
    function SetTokenFormat($format)
    {
        $this->_token_data['format'] = $format;
    }


    function GetTokenFormat()
    {
        return $this->_token_data['format'];
    }
    
    
    function SetTokenNumberOfDigits($number_of_digits)
    {
        $this->_token_data['number_of_digits'] = $number_of_digits;
    }


    function GetTokenNumberOfDigits()
    {
        return $this->_token_data['number_of_digits'];
    }
    
    
    function SetTokenLastEvent($last_event)
    {
        $this->_token_data['last_event'] = $last_event;
    }


    function GetTokenLastEvent()
    {
        return $this->_token_data['last_event'];
    }
    
    
    function SetTokenDeltaTime($delta_time)
    {
        $this->_token_data['delta_time'] = $delta_time;
    }


    function GetTokenDeltaTime()
    {
        return $this->_token_data['delta_time'];
    }
    
    
    function SetTokenTimeInterval($time_interval)
    {
        $this->_token_data['time_interval'] = $time_interval;
    }


    function GetTokenTimeInterval()
    {
        return $this->_token_data['time_interval'];
    }
    
    
    function SetTokenSeed($token_seed)
    {
        $this->_token_data['token_seed'] = $token_seed;
    }


    function GetTokenSeed()
    {
        return $this->_token_data['token_seed'];
    }
    

    function SetTokensFolder($folder)
    {
        $new_folder = $this->ConvertToUnixPath($folder);
        if (substr($new_folder,-1) != "/")
        {
            $new_folder.="/";
        }
        $new_folder = $this->ConvertToWindowsPathIfNeeded($new_folder);
        $this->_tokens_folder = $new_folder;
        if (!file_exists($new_folder))
        {
            if (!@mkdir($new_folder))
            {
                $this->WriteLog("Error: unable to create the missing tokens folder ".$new_folder);
            }
        }
    }

    
    function GetTokensFolder()
    {
        if ("" == $this->_tokens_folder)
        {
            $this->SetTokensFolder($this->GetScriptFolder()."tokens/");
        }
        return $this->ConvertToWindowsPathIfNeeded($this->_tokens_folder);
    }


    function DeleteUser()
    {
        $result = FALSE;
        $user_filename = strtolower($this->_user).'.db';
        if (!file_exists($this->GetUsersFolder().$user_filename))
        {
            $this->WriteLog("Error: unable to delete user ".$this->_user.", database file ".$this->GetUsersFolder().$user_filename." does not exist");
        }
        else
        {
            $result = unlink($this->GetUsersFolder().$user_filename);
            if ($result)
            {
                $this->WriteLog("Information: user ".$this->_user." successfully deleted");
            }
            else
            {
                $this->WriteLog("Error: unable to delete user ".$this->_user);
            }
        }
        return $result;
    }
        
    function ReadUserData($user = "", $create = FALSE)
    {
        if ("" != $user)
        {
            $this->SetUser($user);
        }
        $result = FALSE;
        $user_filename = strtolower($this->GetUser()).'.db';
        if (!file_exists($this->GetUsersFolder().$user_filename))
        {
            if (!$create)
            {
                $this->WriteLog("Error: database file ".$this->GetUsersFolder().$user_filename." for user ".$this->_user." does not exist");
            }
        }
        else
        {
            $user_file_handler = fopen($this->GetUsersFolder().$user_filename, "rt");
            $first_line = trim(fgets($user_file_handler));
            
            // First version format support
			if (FALSE === strpos(strtolower($first_line),"multiotp-database-format"))
            {
                $this->_user_data['algorithm']          = $first_line;
                $this->_user_data['token_seed']         = trim(fgets($user_file_handler));
                $this->_user_data['user_pin']           = trim(fgets($user_file_handler));
                $this->_user_data['number_of_digits']   = trim(fgets($user_file_handler));
                $this->_user_data['last_event']         = intval(trim(fgets($user_file_handler)) - 1);
                $this->_user_data['request_prefix_pin'] = intval(trim(fgets($user_file_handler)));
                $this->_user_data['last_login']         = intval(trim(fgets($user_file_handler)));
                $this->_user_data['error_counter']      = intval(trim(fgets($user_file_handler)));
                $this->_user_data['locked']             = intval(trim(fgets($user_file_handler)));
            }
            else
            {
                while (!feof($user_file_handler))
                {
					$v3 = (FALSE !== strpos(strtolower($first_line),"multiotp-database-format-v3"));
                    $line = trim(fgets($user_file_handler));
                    $line_array = explode("=",$line,2);
					if ($v3) // v3 format, only tags followed by := instead od = are encrypted
					{
						if (":" == substr($line_array[0], -1))
						{
							$line_array[0] = substr($line_array[0], 0, strlen($line_array[0]) -1);
							$line_array[1] = $this->Decrypt($line_array[0],$line_array[1]);
						}
					}
					else // v2 format, only defined tags are encrypted
					{
						if (FALSE !== strpos(strtolower($this->_attributes_to_encrypt), strtolower('*'.$line_array[0].'*')))
						{
							$line_array[1] = $this->Decrypt($line_array[0],$line_array[1]);
						}
					}
                    if ("" != trim($line_array[0]))
                    {
                        $this->_user_data[strtolower($line_array[0])] = $line_array[1];
                    }
                }
            }
            fclose($user_file_handler);
            $result = TRUE;
        }
        $this->SetUserDataReadFlag($result);
        return $result;
    }
    
    
    function WriteUserData()
    {
        $result = FALSE;
        $user_filename = strtolower($this->_user).'.db';
        if (!($user_file_handler = fopen($this->GetUsersFolder().$user_filename, "wt")))
        {
            $this->WriteLog("Error: database file for user ".$this->_user." cannot be written");
        }
        else
        {
            fwrite($user_file_handler,"multiotp-database-format-v3"."\n");
            // foreach ($this->_user_data as $key => $value) // this is not working well in CLI mode
			reset($this->_user_data);
            while(list($key, $value) = each($this->_user_data))
            {
                if ("" != trim($key))
                {
                    $line = strtolower($key);
                    if (FALSE !== strpos(strtolower($this->_attributes_to_encrypt), strtolower('*'.$key.'*')))
                    {
                        $value = $this->Encrypt($key,$value);
						$line = $line.":";
                    }
                    $line = $line."=".$value;
                    fwrite($user_file_handler,$line."\n");
                }
            }
            $result = TRUE;
            fclose($user_file_handler);
        }
        return $result;
    }


    function ReadTokenData($token = "", $create = FALSE)
    {
        if ("" != $token)
        {
            $this->SetToken($token);
        }
        $result = FALSE;
        $token_filename = strtolower($this->GetToken()).'.db';
        if (!file_exists($this->GetTokensFolder().$token_filename))
        {
            if (!$create)
            {
                $this->WriteLog("Error: database file ".$this->GetTokensFolder().$token_filename." for token ".$this->_token." does not exist");
            }
        }
        else
        {
            $token_file_handler = fopen($this->GetTokensFolder().$token_filename, "rt");
            $first_line = trim(fgets($token_file_handler));
            
            while (!feof($token_file_handler))
            {
                $line = trim(fgets($token_file_handler));
                $line_array = explode("=",$line,2);
                if (":" == substr($line_array[0], -1))
                {
                    $line_array[0] = substr($line_array[0], 0, strlen($line_array[0]) -1);
                    $line_array[1] = $this->Decrypt($line_array[0],$line_array[1]);
                }
                if ("" != trim($line_array[0]))
                {
                    $this->_token_data[strtolower($line_array[0])] = $line_array[1];
                }
            }
            
            fclose($token_file_handler);
            $result = TRUE;
        }
        $this->SetTokenDataReadFlag($result);
        return $result;
    }
    
    
    function WriteTokenData()
    {
        $result = FALSE;
        $token_filename = strtolower($this->_token).'.db';
        if (!($token_file_handler = fopen($this->GetTokensFolder().$token_filename, "wt")))
        {
            $this->WriteLog("Error: database file for token ".$this->_token." cannot be written");
        }
        else
        {
            fwrite($token_file_handler,"multiotp-database-format-v3"."\n");
            // foreach ($this->_token_data as $key => $value) // this is not working well in CLI mode
			reset($this->_token_data);
            while(list($key, $value) = each($this->_token_data))
            {
                if ("" != trim($key))
                {
                    $line = strtolower($key);
                    if (FALSE !== strpos(strtolower($this->_attributes_to_encrypt), strtolower('*'.$key.'*')))
                    {
                        $value = $this->Encrypt($key,$value);
						$line = $line.":";
                    }
                    $line = $line."=".$value;
                    fwrite($token_file_handler,$line."\n");
                }
            }
            $result = TRUE;
            fclose($token_file_handler);
        }
        return $result;
    }


    /*********************************************************************
     *
     * Name: CheckToken
     * Short description: Check the token and give the result, with resync options
     *
     * Creation 2010-06-07
     * Update 2010-07-19
     * @package multiotp
     * @version 2.0.0
     * @author SysCo/al
     *
     * @param   string  $input       Token to check
     * @param   string  $input_sync  Second token to check for resync
     * @return  int                  Error code (0 = successful authentication, 1n = info, >= 20 = error)
     *
     *********************************************************************/
    function CheckToken($input = "", $input_sync = "", $display_status = FALSE)
    {
        if (!$this->ReadUserData($this->GetUser()))
        {
            $result = 21; // ERROR: user doesn't exist.
            $this->WriteLog("Error: user ".$this->GetUser()." doesn't exist");
        }
        else
        {
            $result = 99; // Unknown error

            $now_epoch = time();

            if ((1 == $this->GetUserLocked()) && ("" == $input_sync))
            {
                $result = 24; // ERROR: user locked;
                $this->WriteLog("Error: user ".$this->GetUser()." locked");
            }
            elseif(($this->GetUserErrorCounter() >= $this->GetMaxDelayedFailures()) && $now_epoch < ($this->GetMaxDelayedTime()+$this->GetMaxDelayedTime()))
            {
                $result = 25; // ERROR: user delayed;
                $this->WriteLog("Error: user ".$this->GetUser()." delayed");
            }
            else
            {
                $pin               = $this->GetUserPin();
                $need_prefix       = (1 == $this->GetUserPrefixPin());
                $seed              = $this->GetUserTokenSeed();
                $seed_bin          = $this->ConvertHex2Bin($seed);
                $delta_time        = $this->GetUserTokenDeltaTime();
                $interval          = $this->GetUserTokenTimeInterval();
                if (0 >= $interval)
                {
                    $interval = 1;
                }
                $last_event        = $this->GetUserTokenLastEvent();
                $last_login        = $this->GetUserTokenLastLogin();
                $digits            = $this->GetUserTokenNumberOfDigits();
                $error_counter     = $this->GetUserErrorCounter();
                $now_steps         = intval($now_epoch / $interval);
                $time_window       = $this->GetMaxTimeWindow();
                $step_window       = intval($time_window / $interval);
                $event_window      = $this->GetMaxEventWindow();
                $time_sync_window  = $this->GetMaxTimeResyncWindow();
                $step_sync_window  = intval($time_sync_window / $interval);
                $event_sync_window = $this->GetMaxEventResyncWindow();
                $last_login_step   = intval($last_login / $interval);
                $delta_step        = $delta_time / $interval;
                
                switch (strtoupper($this->GetUserAlgorithm()))
                {
                    case "MOTP":
                        if ("" == $input_sync)
                        {
                            $max_steps = 2 * $step_window;
                        }
                        else
                        {
                            $max_steps = 2 * $step_sync_window;
                        }
                        $check_step = 0;
                        do
                        {
                            $additional_step = (1 - (2 * ($check_step % 2))) * intval($check_step/2);
                            $calculated_token = $this->ComputeMotp($seed.$pin, $now_steps+$additional_step+$delta_step, $digits);
                            if ($need_prefix)
                            {
                                $calculated_token = $pin.$calculated_token;
                            }
                            if ($input == $calculated_token)
                            {
                                if ("" == $input_sync)
                                {
                                    if (($now_steps+$additional_step+$delta_step) > $last_login_step)
                                    {
                                        $this->SetUserTokenLastLogin(($now_steps+$additional_step+$delta_step) * $interval);
                                        $this->SetUserTokenDeltaTime(($additional_step+$delta_step) * $interval);
                                        $this->SetUserErrorCounter(0);
                                        $result = 0; // OK: This is the correct token
                                        $this->WriteLog("OK: user ".$this->GetUser()." successfully logged in");
                                    }
                                    else
                                    {
                                        $this->SetUserErrorCounter($error_counter+1);
                                        $this->SetUserTokenLastError($now_epoch);
                                        $result = 26; // ERROR: this token has already been used
                                        $this->WriteLog("Error: token of user ".$this->GetUser()." already used");
                                    }
                                }
                                else
                                {
                                    $calculated_token = $this->ComputeMotp($seed.$pin, $now_steps+$additional_step+$delta_step+1, $digits);
                                    if ($need_prefix)
                                    {
                                        $calculated_token = $pin.$calculated_token;
                                    }
                                    if ($input_sync == $calculated_token)
                                    {
                                        $this->SetUserTokenLastLogin(($now_steps+$additional_step+$delta_step+1) * $interval);
                                        $this->SetUserTokenDeltaTime(($additional_step+$delta_step+1) * $interval);
                                        $this->SetUserErrorCounter(0);
                                        $this->SetUserLocked(0);
                                        $result = 14; // INFO: token is now synchronized
                                        $this->WriteLog("Info: token for user ".$this->GetUser()." is now resynchronized with a delta of ".(($additional_step+$delta_step+1) * $interval). " seconds");
                                    }
                                    else
                                    {
                                        $result = 27; // ERROR: resync failed
                                        $this->WriteLog("Error: resync for user ".$this->GetUser()." has failed");
                                    }
                                }
                            }
                            else
                            {
                                $check_step++;
                                if ($display_status)
                                {
                                    $this->ShowStatus($check_step, $max_steps);
                                }
                            }
                        }
                        while (($check_step < $max_steps) && (99 == $result));
                        if ($display_status)
                        {
                            echo "\n\r";
                        }
                        if (99 == $result)
                        {
                            $this->SetUserErrorCounter($error_counter+1);
                            $this->SetUserTokenLastError($now_epoch);
                            $this->WriteLog("Error: authentication failed for user ".$this->GetUser());
                        }
                        break;
                    case "HOTP";
                        if ("" == $input_sync)
                        {
                            $max_steps = $event_window;
                        }
                        else
                        {
                            $max_steps = $event_sync_window;
                        }
                        $check_step = 1;
                        do
                        {
                            $calculated_token = $this->ComputeOathTruncate($this->ComputeOathHotp($seed_bin,$last_event+$check_step),$digits);
                            if ($need_prefix)
                            {
                                $calculated_token = $pin.$calculated_token;
                            }
                            if ($input == $calculated_token)
                            {
                                if ("" == $input_sync)
                                {
                                    $this->SetUserTokenLastLogin($now_epoch);
                                    $this->SetUserTokenLastEvent($last_event+$check_step);
                                    $this->SetUserErrorCounter(0);
                                    $result = 0; // OK: This is the correct token
                                    $this->WriteLog("OK: user ".$this->GetUser()." successfully logged in");
                                }
                                else
                                {
                                    $calculated_token = $this->ComputeOathTruncate($this->ComputeOathHotp($seed_bin,$last_event+$check_step+1),$digits);
                                    if ($need_prefix)
                                    {
                                        $calculated_token = $pin.$calculated_token;
                                    }
                                    if ($input_sync == $calculated_token)
                                    {
                                        $this->SetUserTokenLastLogin($now_epoch);
                                        $this->SetUserTokenLastEvent($last_event+$check_step+1);
                                        $this->SetUserErrorCounter(0);
                                        $this->SetUserLocked(0);
                                        $result = 14; // INFO: token is now synchronized
                                        $this->WriteLog("Info: token for user ".$this->GetUser()." is now resynchronized with the last event ".($last_event+$check_step+1));
                                    }
                                    else
                                    {
                                        $result = 27; // ERROR: resync failed
                                        $this->WriteLog("Error: resync for user ".$this->GetUser()." has failed");
                                    }
                                }
                            }
                            else
                            {
                                $check_step++;
                                if ($display_status)
                                {
                                    $this->ShowStatus($check_step, $max_steps);
                                }
                            }
                        }
                        while (($check_step < $max_steps) && (99 == $result));
                        if ($display_status)
                        {
                            echo "\n\r";
                        }
                        if (99 == $result)
                        {
                            $this->SetUserErrorCounter($error_counter+1);
                            $this->SetUserTokenLastError($now_epoch);
                            $this->WriteLog("Error: authentication failed for user ".$this->GetUser());
                        }
                        break;
                    case "TOTP";
                        if ("" == $input_sync)
                        {
                            $max_steps = 2 * $step_window;
                        }
                        else
                        {
                            $max_steps = 2 * $step_sync_window;
                        }
                        $check_step = 0;
                        do
                        {
                            $additional_step = (1 - (2 * ($check_step % 2))) * intval($check_step/2);
                            $calculated_token = $this->ComputeOathTruncate($this->ComputeOathHotp($seed_bin,$now_steps+$additional_step+$delta_step),$digits);
                            if ($need_prefix)
                            {
                                $calculated_token = $pin.$calculated_token;
                            }
                            if ($input == $calculated_token)
                            {
                                if ("" == $input_sync)
                                {
                                    if (($now_steps+$additional_step+$delta_step) > $last_login_step)
                                    {
                                        $this->SetUserTokenLastLogin(($now_steps+$additional_step+$delta_step) * $interval);
                                        $this->SetUserTokenDeltaTime(($additional_step+$delta_step) * $interval);
                                        $this->SetUserErrorCounter(0);
                                        $result = 0; // OK: This is the correct token
                                        $this->WriteLog("OK: user ".$this->GetUser()." successfully logged in");
                                    }
                                    else
                                    {
                                        $this->SetUserErrorCounter($error_counter+1);
                                        $this->SetUserTokenLastError($now_epoch);
                                        $result = 26; // ERROR: this token has already been used
                                        $this->WriteLog("Error: token of user ".$this->GetUser()." already used");
                                    }
                                }
                                else
                                {
                                    $calculated_token = $this->ComputeOathTruncate($this->ComputeOathHotp($seed_bin,$now_steps+$additional_step+$delta_step+1),$digits);
                                    if ($need_prefix)
                                    {
                                        $calculated_token = $pin.$calculated_token;
                                    }
                                    if ($input_sync == $calculated_token)
                                    {
                                        $this->SetUserTokenLastLogin(($now_steps+$additional_step+$delta_step+1) * $interval);
                                        $this->SetUserTokenDeltaTime(($additional_step+$delta_step+1) * $interval);
                                        $this->SetUserErrorCounter(0);
                                        $this->SetUserLocked(0);
                                        $result = 14; // INFO: token is now synchronized
                                        $this->WriteLog("Info: token for user ".$this->GetUser()." is now resynchronized with a delta of ".(($additional_step+$delta_step+1) * $interval). " seconds");
                                    }
                                    else
                                    {
                                        $result = 27; // ERROR: resync failed
                                        $this->WriteLog("Error: resync for user ".$this->GetUser()." has failed");
                                    }
                                }
                            }
                            else
                            {
                                $check_step++;
                                if ($display_status)
                                {
                                    $this->ShowStatus($check_step, $max_steps);
                                }
                            }
                        }
                        while (($check_step < $max_steps) && (99 == $result));
                        if ($display_status)
                        {
                            echo "\n\r";
                        }
                        if (99 == $result)
                        {
                            $this->SetUserErrorCounter($error_counter+1);
                            $this->SetUserTokenLastError($now_epoch);
                            $this->WriteLog("Error: authentication failed for user ".$this->GetUser());
                        }
                        break;
                    default:
                        $result = 23;
                        $this->WriteLog("Error: ".$this->GetUserAlgorithm()." algorithm is unknown");
                }
            }
        }
        if ($this->GetUserErrorCounter() >= $this->GetMaxBlockFailures())
        {
            $this->SetUserLocked(1);
        }
        $this->WriteUserData();
        return $result;
    }
    
    
    function ImportTokensFromXml($xml_file)
    {
        $result = TRUE;
        if (!file_exists($xml_file))
        {
            $this->WriteLog("Error: XML tokens definition file ".$xml_file." doesn't exist");
            $result = FALSE;
        }
        else
        {
            // http://tools.ietf.org/html/draft-hoyer-keyprov-pskc-algorithm-profiles-00
            
            //Get the XML document loaded into a variable
            $sXmlData = @file_get_contents($xml_file);

            //Set up the parser object
            $xml = new MultiotpXmlParser($sXmlData);

            //Parse it !
            $xml->Parse();

            // Array of key types
            $key_types = array();
            
            // Array of devices
            $devices = array();
            
            if (isset($xml->document->keyproperties))
            {
                foreach ($xml->document->keyproperties as $keyproperty)
                {
                    $id = (isset($keyproperty->tagAttrs['xml:id'])?$keyproperty->tagAttrs['xml:id']:"");
                    
                    if ('' != $id)
                    {
                        $key_types[$id]['id'] = $id;
                        $key_types[$id]['issuer'] = (isset($keyproperty->issuer[0]->tagData)?$keyproperty->issuer[0]->tagData:"");
                        $key_types[$id]['keyalgorithm'] = (isset($keyproperty->tagAttrs['keyalgorithm'])?$keyproperty->tagAttrs['keyalgorithm']:'');
                        $pos = strrpos($key_types[$id]['keyalgorithm'], "#");
                        $key_types[$id]['algorithm'] = (($pos === false)?'':strtoupper(substr($key_types[$id]['keyalgorithm'], $pos+1)));
                        $key_types[$id]['otp'] = (isset($keyproperty->usage[0]->tagAttrs['otp'])?$keyproperty->usage[0]->tagAttrs['otp']:'');
                        $key_types[$id]['format'] = (isset($keyproperty->usage[0]->responseformat[0]->tagAttrs['format'])?$keyproperty->usage[0]->responseformat[0]->tagAttrs['format']:'');
                        $key_types[$id]['length'] = (isset($keyproperty->usage[0]->responseformat[0]->tagAttrs['length'])?$keyproperty->usage[0]->responseformat[0]->tagAttrs['length']:-1);
                        $key_types[$id]['counter'] = (isset($keyproperty->data[0]->counter[0]->plainvalue[0]->tagData)?$keyproperty->data[0]->counter[0]->plainvalue[0]->tagData:-1);
                        $key_types[$id]['time'] = (isset($keyproperty->data[0]->time[0]->plainvalue[0]->tagData)?$keyproperty->data[0]->time[0]->plainvalue[0]->tagData:-1);
                        $key_types[$id]['timeinterval'] = (isset($keyproperty->data[0]->timeinterval[0]->plainvalue[0]->tagData)?$keyproperty->data[0]->timeinterval[0]->plainvalue[0]->tagData:-1);
                    }
                }
            }
            
            if (isset($xml->document->device))
            {
                foreach ($xml->document->device as $device)
                {
                    $keyid = (isset($device->key[0]->tagAttrs['keyid'])?$device->key[0]->tagAttrs['keyid']:'');
                    if ('' != $keyid)
                    {
                        $keyproperties = '';
                        $manufacturer = '';
                        $serialno = '';
                        $issuer = '';
                        $keyalgorithm = '';
                        $algorithm = '';
                        $otp = '';
                        $format = '';
                        $length = 0;
                        $counter = -1;
                        $time = 0;
                        $timeinterval = 0;
                        $secret = '';
                        
                        if (isset($device->key[0]->tagAttrs['keyproperties']))
                        {
                            $keyproperties = $device->key[0]->tagAttrs['keyproperties'];
                            if (isset($key_types[$keyproperties]))
                            {
                                reset($key_types[$keyproperties]);
                                while(list($key, $value) = each($key_types[$keyproperties]))
                                {
                                    $$key = $value;
                                }
                            }
                        }
                        
                        $manufacturer = (isset($device->deviceinfo[0]->manufacturer[0]->tagData)?$device->deviceinfo[0]->manufacturer[0]->tagData:$manufacturer);
                        $serialno = (isset($device->deviceinfo[0]->serialno[0]->tagData)?$device->deviceinfo[0]->serialno[0]->tagData:$serialno);

                        $issuer = (isset($device->key[0]->issuer[0]->tagData)?$device->key[0]->issuer[0]->tagData:$issuer);
                        
                        if (isset($device->key[0]->tagAttrs['keyalgorithm']))
                        {
                            $keyalgorithm = $device->key[0]->tagAttrs['keyalgorithm'];
                            $pos = strrpos($keyalgorithm, "#");
                            $algorithm = (($pos === false)?$algorithm:strtoupper(substr($keyalgorithm, $pos+1)));
                        }
                        
                        $otp = (isset($device->key[0]->usage[0]->tagAttrs['otp'])?$device->key[0]->usage[0]->tagAttrs['otp']:$otp);
                        $format = (isset($device->key[0]->usage[0]->responseformat[0]->tagAttrs['format'])?$device->key[0]->usage[0]->responseformat[0]->tagAttrs['format']:$format);
                        $length = (isset($device->key[0]->usage[0]->responseformat[0]->tagAttrs['length'])?$device->key[0]->usage[0]->responseformat[0]->tagAttrs['length']:$length);
                        $counter = (isset($device->key[0]->data[0]->counter[0])?$device->key[0]->data[0]->counter[0]->plainvalue[0]->tagData:$counter);
                        $time = (isset($device->key[0]->data[0]->time[0])?$device->key[0]->data[0]->time[0]->plainvalue[0]->tagData:$time);
                        $timeinterval = (isset($device->key[0]->data[0]->timeinterval[0])?$device->key[0]->data[0]->timeinterval[0]->plainvalue[0]->tagData:$timeinterval);
                        
                        if (isset($device->key[0]->data[0]->secret[0]->plainvalue[0]->tagData))
                        {
                            $secret = bin2hex(base64_decode($device->key[0]->data[0]->secret[0]->plainvalue[0]->tagData));
                        }

                        $this->SetToken($keyid);
                        $this->_token_data['manufacturer'] = $manufacturer;
                        $this->_token_data['token_serial'] = $serialno;
                        $this->_token_data['issuer'] = $issuer;
                        $this->_token_data['key_algorithm'] = $keyalgorithm;
                        $this->_token_data['algorithm'] = $algorithm;
                        $this->_token_data['otp'] = $otp;
                        $this->_token_data['format'] = $format;
                        $this->_token_data['number_of_digits'] = $length;
                        if ($counter >= 0)
                        {
                            $this->_token_data['last_event'] = $counter-1;
                        }
                        else
                        {
                            $this->_token_data['last_event'] = 0;
                        }
                        $this->_token_data['delta_time'] = $time;
                        $this->_token_data['time_interval'] = $timeinterval;
                        $this->_token_data['token_seed'] = $secret;
                        
                        $result = $this->WriteTokenData() && $result;
                        
                        $this->WriteLog("Information: Token with keyid ".$keyid." successfully imported");
                        if ($this->_log_verbose_flag)
                        {
                            reset($this->_token_data);
                            while(list($key, $value) = each($this->_token_data))
                            {
                                if ('' != $value)
                                {
                                    $this->WriteLog("  Token ".$keyid." - ".$key.": ".$value);
                                }
                            }
                        }
                    }
                }
            }
        }
        return $result;
    }    
}


/*********************************************************************
 *
 * XML Parser Class (php4)
 * Parses an XML document into an object structure much like the SimpleXML extension.
 *
 * Name: MultiotpXmlParser (original name: XMLParser)
 *
 * @author: MT Shahzad - http://mts.sw3solutions.com
 * Source: http://www.geosourcecode.com/post241.html
 *
 *********************************************************************/

class MultiotpXmlParser
{
    /**
     * The XML parser
     *
     * @var resource
     */
    var $parser;

    /**
    * The XML document
    *
    * @var string
    */
    var $xml;

    /**
    * Document tag
    *
    * @var object
    */
    var $document;

    /**
    * Current object depth
    *
    * @var array
    */
    var $stack;


    /**
     * Constructor. Loads XML document.
     *
     * @param string $xml The string of the XML document
     * @return MultiotpXmlParser
     */
    function MultiotpXmlParser($xml = '')
    {
        //Load XML document
        $this->xml = $xml;

        // Set stack to an array
        $this->stack = array();
    }

    /**
     * Initiates and runs PHP's XML parser
     */
    function Parse()
    {
        //Create the parser resource
        $this->parser = xml_parser_create();

        //Set the handlers
        xml_set_object($this->parser, $this);
        xml_set_element_handler($this->parser, 'StartElement', 'EndElement');
        xml_set_character_data_handler($this->parser, 'CharacterData');

        //Error handling
        if (!xml_parse($this->parser, $this->xml))
            $this->HandleError(xml_get_error_code($this->parser), xml_get_current_line_number($this->parser), xml_get_current_column_number($this->parser));

        //Free the parser
        xml_parser_free($this->parser);
    }

    /**
     * Handles an XML parsing error
     *
     * @param int $code XML Error Code
     * @param int $line Line on which the error happened
     * @param int $col Column on which the error happened
     */
    function HandleError($code, $line, $col)
    {
        trigger_error('XML Parsing Error at '.$line.':'.$col.'. Error '.$code.': '.xml_error_string($code));
    }


    /**
     * Gets the XML output of the PHP structure within $this->document
     *
     * @return string
     */
    function GenerateXML()
    {
        return $this->document->GetXML();
    }

    /**
     * Gets the reference to the current direct parent
     *
     * @return object
     */
    function GetStackLocation()
    {
        $return = '';

        foreach($this->stack as $stack)
            $return .= $stack.'->';

        return rtrim($return, '->');
    }

    /**
     * Handler function for the start of a tag
     *
     * @param resource $parser
     * @param string $name
     * @param array $attrs
     */
    function StartElement($parser, $name, $attrs = array())
    {
        //Make the name of the tag lower case
        $name = strtolower($name);

        //Check to see if tag is root-level
        if (count($this->stack) == 0)
        {
            //If so, set the document as the current tag
            $this->document = new XMLTag($name, $attrs);

            //And start out the stack with the document tag
            $this->stack = array('document');
        }
        //If it isn't root level, use the stack to find the parent
        else
        {
            //Get the name which points to the current direct parent, relative to $this
            $parent = $this->GetStackLocation();

            //Add the child
            eval('$this->'.$parent.'->AddChild($name, $attrs, '.count($this->stack).');');

            //Update the stack
            eval('$this->stack[] = $name.\'[\'.(count($this->'.$parent.'->'.$name.') - 1).\']\';');
        }
    }

    /**
     * Handler function for the end of a tag
     *
     * @param resource $parser
     * @param string $name
     */
    function EndElement($parser, $name)
    {
        //Update stack by removing the end value from it as the parent
        array_pop($this->stack);
    }

    /**
     * Handler function for the character data within a tag
     *
     * @param resource $parser
     * @param string $data
     */
    function CharacterData($parser, $data)
    {
        //Get the reference to the current parent object
        $tag = $this->GetStackLocation();

        //Assign data to it
        eval('$this->'.$tag.'->tagData .= trim($data);');
    }
}


/**
* XML Tag Object (php4)
*
* This object stores all of the direct children of itself in the $children array. They are also stored by
* type as arrays. So, if, for example, this tag had 2 <font> tags as children, there would be a class member
* called $font created as an array. $font[0] would be the first font tag, and $font[1] would be the second.
*
* To loop through all of the direct children of this object, the $children member should be used.
*
* To loop through all of the direct children of a specific tag for this object, it is probably easier
* to use the arrays of the specific tag names, as explained above.
*/
class XMLTag
{
    /**
     * Array with the attributes of this XML tag
     *
     * @var array
     */
    var $tagAttrs;

    /**
     * The name of the tag
     *
     * @var string
     */
    var $tagName;

    /**
     * The data the tag contains
     *
     * So, if the tag doesn't contain child tags, and just contains a string, it would go here
     *
     * @var string
     */
    var $tagData;

    /**
     * Array of references to the objects of all direct children of this XML object
     *
     * @var array
     */
    var $tagChildren;

    /**
     * The number of parents this XML object has (number of levels from this tag to the root tag)
     *
     * Used presently only to set the number of tabs when outputting XML
     *
     * @var int
     */
    var $tagParents;

    /**
     * Constructor, sets up all the default values
     *
     * @param string $name
     * @param array $attrs
     * @param int $parents
     * @return XMLTag
     */
    function XMLTag($name, $attrs = array(), $parents = 0)
    {
        //Make the keys of the attr array lower case, and store the value
        $this->tagAttrs = array_change_key_case($attrs, CASE_LOWER);

        //Make the name lower case and store the value
        $this->tagName = strtolower($name);

        //Set the number of parents
        $this->tagParents = $parents;

        //Set the types for children and data
        $this->tagChildren = array();
        $this->tagData = '';
    }

    /**
     * Adds a direct child to this object
     *
     * @param string $name
     * @param array $attrs
     * @param int $parents
     */
    function AddChild($name, $attrs, $parents)
    {
        //If there is no array already set for the tag name being added,
        //create an empty array for it
        if(!isset($this->$name))
            $this->$name = array();

        //If the tag has the same name as a member in XMLTag, or somehow the
        //array wasn't properly created, output a more informative error than
        //PHP otherwise would.
        if(!is_array($this->$name))
        {
            trigger_error('You have used a reserved name as the name of an XML tag. Please consult the documentation (http://www.thousandmonkeys.net/xml_doc.php) and rename the tag named '.$name.' to something other than a reserved name.', E_USER_ERROR);

            return;
        }

        //Create the child object itself
        $child = new XMLTag($name, $attrs, $parents);

        //Add the reference of it to the end of an array member named for the tag's name
        $this->{$name}[] =& $child;

        //Add the reference to the children array member
        $this->tagChildren[] =& $child;
    }

    /**
     * Returns the string of the XML document which would be generated from this object
     *
     * This function works recursively, so it gets the XML of itself and all of its children, which
     * in turn gets the XML of all their children, which in turn gets the XML of all thier children,
     * and so on. So, if you call GetXML from the document root object, it will return a string for
     * the XML of the entire document.
     *
     * This function does not, however, return a DTD or an XML version/encoding tag. That should be
     * handled by MultiotpXmlParser::GetXML()
     *
     * @return string
     */
    function GetXML()
    {
        //Start a new line, indent by the number indicated in $this->parents, add a <, and add the name of the tag
        $out = "\n".str_repeat("\t", $this->tagParents).'<'.$this->tagName;

        //For each attribute, add attr="value"
        foreach($this->tagAttrs as $attr => $value)
            $out .= ' '.$attr.'="'.$value.'"';

        //If there are no children and it contains no data, end it off with a />
        if(empty($this->tagChildren) && empty($this->tagData))
            $out .= " />";

        //Otherwise...
        else
        {
            //If there are children
            if(!empty($this->tagChildren))
            {
                //Close off the start tag
                $out .= '>';

                //For each child, call the GetXML function (this will ensure that all children are added recursively)
                foreach($this->tagChildren as $child)
                    $out .= $child->GetXML();

                //Add the newline and indentation to go along with the close tag
                $out .= "\n".str_repeat("\t", $this->tagParents);
            }

            //If there is data, close off the start tag and add the data
            elseif(!empty($this->tagData))
                $out .= '>'.$this->tagData;

            //Add the end tag
            $out .= '</'.$this->tagName.'>';
        }

        //Return the final output
        return $out;
    }
}

?>
Return current item: Multi-OTP PHP class