Login   Register  
PHP Classes
elePHPant
Icontem

File: multiotp.class.php

Recommend this page to a friend!
Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of André Liechti  >  Multi-OTP PHP class  >  multiotp.class.php  >  Download  
File: multiotp.class.php
Role: Class source
Content type: text/plain
Description: Main file, class definition
Class: Multi-OTP PHP class
Authenticate and manage OTP strong user tokens
Author: By
Last change: Now it is possible to import PSKC Algorithm Profiles containing tokens definition for TOTP and HOTP algorithm. Thus, creating a user and attributing a token is easier. You only need to give the name of the user, the token id and the desired pin code of the user.

The multiotp-database-format flat file has been enhanced to version 3. Regular attributes are written attribute=value and encrypted attributes are now written encrypted_attribute:=encrypted_value. If you want to set a new pin for a user, you can open the file of the user and change the line user_pin:=ACQwJw== by user_pin=1234. The new value will be correctly read the next time, and encrypted again the next time something is written in the file.

In debug mode, the command line version is now returning a text information after the exit code.
Date: 2010-09-02 14:04
Size: 84,616 bytes
 

Contents

Class file image Download
<?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 developer@sysco.ch
 * 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 developer@sysco.ch.
 *
 *
 * 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;
    }
}

?>