<?php
// Load the Twofactor_Auth_Module Class
require_once(dirname(__FILE__).'/../twofactor/authmod.php');
// Load the PHP_YubiAuthenticator Class
require_once(dirname(__FILE__).'/YubiAuthenticator.php');

/**
 * If we turn this into a helper class, it can have its own language and settings files.
 * Until then, we can only use per-user settings.
 */
class helper_plugin_twofactoryubiauth extends Twofactor_Auth_Module {
	/** 
	 * The user must have verified their GA is configured correctly first.
	 */
    public function canUse($user = null){		
		return ($this->_settingExists("verified", $user) && $this->getConf('enable') === 1);
	}
	
	/**
	 * This module does provide authentication functionality at the main login screen.
	 */
    public function canAuthLogin() {
		return true;
	}
		
	/**
	 * This user will need to insert an OTP in order to configure YA.
	 */
    public function renderProfileForm(){
		global $conf,$USERINFO;
		$elements = array();

		if ($this->_settingExists("publicID")) { // The user has a revokable YA Public ID.

            // Show saved Public ID
            $elements[] = form_makeTextField('yubiauth_showPublicID', $this->_settingGet("publicID"), $this->getLang('currentPublicID') , '', 'block', array('size'=>'12', 'readonly'=>'true'));
            // User can remove Yubi Key here
			$elements[] = form_makeCheckboxField('yubiauth_disable', '1', $this->getLang('killmodule'), '', 'block');
		}
		else { // The user may opt in using YA.
			// Provide a checkbox to create a personal secret.
			$elements[] = form_makeCheckboxField('yubiauth_enable', '1', $this->getLang('enablemodule'), '', 'block');
            // Provide text field for setup OTP
            $elements[] = form_makeTextField('yubiauth_setup', '', $this->getLang('needsetup'), '', 'block', array('size'=>'44'));
		}
		return $elements;
	}

	/**
	 * Process any user configuration.
	 */	
    public function processProfileForm(){
		global $INPUT;

        if ($this->_settingExists("publicID")) {
            if ($INPUT->bool('yubiauth_disable', false)) {
                $this->_settingDelete("publicID");
                $this->_settingDelete("verified");
                return true;
            }
        } else {
            if ($INPUT->bool('yubiauth_enable', false)) {
                $otp = $INPUT->str('yubiauth_setup', '');
                $checkResult = $this->processLogin($otp);
                if ($checkResult) {
                    $this->_settingSet("publicID", substr($otp, 0, 12));
                    $this->_settingSet("verified", true);
                    return 'verified';
                } else {
                    return 'failed';
                }
            }
        }
        return null;
	}	
	
	/**
	 * This module cannot send messages.
	 */
	public function canTransmitMessage() { return false; }
	
	/**
	 * Transmit the message via email to the address on file.
	 * As a special case, configure the mail settings to send only via text.
	 */
	//public function transmitMessage($subject, $message);
	
	/**
	 * 	This module authenticates against the configured API Server.
	 */
    public function processLogin($code, $user = null){
        if (strlen($code) < 44) {
            # wrong length (12 Chars static + 32 Chars dynamic)
            return false;
        }

        if ($this->_settingExists("publicID", $user)) {
            if (!$this->validateUser($code, $user)) {
                return false;
            }
        }
        $yubiAuthenticator = new PHP_YubiAuthenticator();
        $response = array();
		$auth = $yubiAuthenticator->verifyCode($this->generateYubiURL($code),$response);
        if($auth) {
            if($code != $response["otp"])
                // otp is not the one we used
                return false;

            if(empty($this->getConf("clientSecret"))) {
                return true;
            } else {
                return $this->checkSignature($response);
            }
        } else {
            return false;
        }
	}

    /**
     * 	Check if provided $code uses the users Public ID
     * @param $code OTP generated by Yubi Key
     * @param $user current user
     * @return boolean
     */
    public function validateUser($code, $user) {
        $publicID = $this->_settingGet("publicID",'', $user);
        $static = substr($code,0,12);
        return $publicID==$static;
    }

    /**
     * 	Generate URL for API Request
     * @param $code OTP generated by Yubi Key
     * @return string url
     */
    public function generateYubiURL($code) {
        $validationServer = $this->getConf("validationServer");
        $url = $validationServer;

        $data = array();
        $data["id"] = $this->getConf("clientID");
        if(empty($data["id"])) $data["id"]=87;
        $data["otp"] = $code;
        $data["nonce"] = $this->generateNonce();
        //https://api.yubico.com/wsapi/2.0/verify?otp=vvvvvvcucrlcietctckflvnncdgckubflugerlnr&id=87&timeout=8&sl=50&nonce=askjdnkajsndjkasndkjsnad
        return sprintf("%s?%s", $url, http_build_query($data));
    }

    /**
     * 	Generate random nonce
     * @param int $size length of desired nonce, must be between 16 and 40, otherwise goes for 32
     * @return string
     */
    private function generateNonce($size = 32) {
        $chars = array("a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","0","1","2","3","4","5","6","7","8","9");
        $nonce = "";
        if($size >= 16 && $size <= 40) {
        } else {$size = 32;}
        for($i=0; $i < $size; $i++) {
            $rand = rand(0,count($chars)-1);
            $nonce .= $chars[$rand];
        }
        return $nonce;
    }

    /**
     * 	Generate signature over response values and compare to the servers signature
     * @param $response array with key=>value pairs responded by the API server
     * @return boolean
     */
    private function checkSignature($response) {
        $secret = base64_decode($this->getConf("clientSecret"));
        $their_hash = $response["h"];
        unset($response["h"]);
        ksort($response);
        $sorted = "";
        foreach ($response as $k => $v) {
            $sorted .= '&' . $k . '=' . $v;
        }
        $sorted = substr($sorted,1);
        $hash = base64_encode(hash_hmac('sha1',$sorted,$secret, true));
        return ($hash == $their_hash);
    }

}