plugins/yubikey/corelib.php
author Dan
Thu, 26 Feb 2009 01:19:45 -0500
changeset 2 6edc6ebb3b39
parent 0 9d2c4f04a0d0
child 3 d0fe7acaf0e8
permissions -rw-r--r--
Minor: if input OTP is empty and password looks like OTP, now copies password to OTP in memory and treats password field as OTP. Will require patch in Enano trunk to work for html login.

<?php

define('YK_SEC_NORMAL_USERNAME', 1);
define('YK_SEC_NORMAL_PASSWORD', 2);
define('YK_SEC_ELEV_USERNAME', 4);
define('YK_SEC_ELEV_PASSWORD', 8);
define('YK_SEC_ALLOW_NO_OTP', 16);

define('YK_DEFAULT_VERIFY_URL', 'http://api.yubico.com/wsapi/verify');

function generate_yubikey_field($name = 'yubikey_otp', $value = false)
{
  global $lang;
  
  $fid = substr(sha1(microtime() . mt_rand()), 0, 12);
  $class = $value ? 'wasfull' : 'wasempty';
  $html = '<input id="yubifield' . $fid . '" class="' . $class . '" type="hidden" name="' . $name . '" value="' . ( is_string($value) ? $value : '' ) . '" />';
  if ( $value )
  {
    $html .= '<span id="yubistat' . $fid . '" class="yubikey_status enrolled">' . $lang->get('yubiauth_ctl_status_enrolled') . '</span>';
    $atext = $lang->get('yubiauth_ctl_btn_change_key');
    $classadd = ' abutton_green';
  }
  else
  {
    $html .= '<span id="yubistat' . $fid . '" class="yubikey_status empty">' . $lang->get('yubiauth_ctl_status_empty') . '</span>';
    $atext = $lang->get('yubiauth_ctl_btn_enroll');
    $classadd = '';
  }
  $html .= ' <a class="abutton' . $classadd . ' yubikey_enroll" onclick="yk_mb_init(\'yubifield' . $fid . '\', \'yubistat' . $fid . '\'); return false;" href="#enroll">' . $atext . '</a>';
  if ( $value )
  {
    $html .= ' <a class="abutton abutton_red yubikey_enroll" onclick="yk_clear(\'yubifield' . $fid . '\', \'yubistat' . $fid . '\'); return false;" href="#enroll">'
             . $lang->get('yubiauth_ctl_btn_clear') .
             '</a>';
  }
  $html = '<noscript><input type="text" name="' . $name . '" class="yubikey_noscript" value="' . ( is_string($value) ? $value : '' ) . '" /> </noscript>'
          . $html; // '<script type="text/javascript">document.write(unescape("' . rawurlencode($html) . '"));</script>';
  return $html;
}

function yubikey_validate_otp($otp)
{
  $api_key = getConfig('yubikey_api_key');
  $api_id  = getConfig('yubikey_api_key_id');
  if ( !$api_key || !$api_id )
  {
    return array(
        'success' => false,
        'error' => 'missing_api_key'
      );
  }
  if ( !preg_match('/^[cbdefghijklnrtuv]{44}$/', $otp) )
  {
    return array(
        'success' => false,
        'error' => 'otp_invalid_chars'
      );
  }
  // make HTTP request
  require_once( ENANO_ROOT . '/includes/http.php' );
  $auth_url = getConfig('yubikey_auth_server', YK_DEFAULT_VERIFY_URL);
  $auth_url = preg_replace('#^https?://#i', '', $auth_url);
  if ( !preg_match('#^(\[?[a-z0-9-:]+(?:\.[a-z0-9-:]+\]?)*)(/.*)$#', $auth_url, $match) )
  {
    return array(
        'success' => false,
        'error' => 'invalid_auth_url'
      );
  }
  $auth_server =& $match[1];
  $auth_uri =& $match[2];
  $req = new Request_HTTP($auth_server, $auth_uri);
  $req->add_get('id', strval($api_id));
  $req->add_get('otp', $otp);
  $req->add_get('h', yubikey_sign($req->parms_get));
  
  $response = $req->get_response_body();
  
  if ( $req->response_code != HTTP_OK )
  {
    return array(
        'success' => false,
        'error' => 'http_response_error'
      );
  }
  $response = trim($response);
  $response_nosig = preg_replace('/^h=(.+?)$/m', '', $response);
  if ( !preg_match_all('/^([a-z0-9_]+)=(.*?)$/m', $response, $matches) )
  {
    return array(
        'success' => false,
        'error' => 'malformed_response'
      );
  }
  $response = array();
  foreach ( $matches[0] as $i => $_ )
  {
    $response[$matches[1][$i]] = $matches[2][$i];
  }
  // make sure we have a status
  if ( !isset($response['status']) )
  {
    return array(
        'success' => false,
        'error' => 'response_missing_status'
      );
  }
  // verify response signature
  // MISSING_PARAMETER is the ONLY situation under which an unsigned response is acceptable
  if ( $response['status'] !== 'MISSING_PARAMETER' )
  {
    if ( !isset($response['h']) )
    {
      return array(
          'success' => false,
          'error' => 'response_missing_sig'
        );
    }
    if ( yubikey_sign($response) !== $response['h'] )
    {
      return array(
          'success' => false,
          'error' => 'response_invalid_sig'
        );
    }
  }
  if ( $response['status'] === 'OK' )
  {
    return array(
        'success' => true
      );
  }
  else
  {
    return array(
        'success' => false,
        'error' => strtolower("response_{$response['status']}")
      );
  }
}

function yubikey_sign($arr)
{
  static $api_key = false;
  
  ksort($arr);
  if ( isset($arr['h']) )
    unset($arr['h']);
  
  if ( !$api_key )
  {
    $api_key = getConfig('yubikey_api_key');
    $api_key = hexencode(base64_decode($api_key), '', '');
  }
  
  $req = array();
  foreach ( $arr as $key => $val )
  {
    $req[] = "$key=$val";
  }
  $req = implode('&', $req);
  
  $sig = hmac_sha1($req, $api_key);
  $sig = hexdecode($sig);
  $sig = base64_encode($sig);
  
  return $sig;
}

$plugins->attachHook('compile_template', 'yubikey_attach_headers($this);');

function yubikey_attach_headers(&$template)
{
  $template->add_header('<script type="text/javascript" src="' . scriptPath . '/plugins/yubikey/yubikey.js"></script>');
  $template->add_header('<link rel="stylesheet" type="text/css" href="' . scriptPath . '/plugins/yubikey/yubikey.css" />');
}