*/ // This lib is developed by Yubico. // Take a look at https://developers.yubico.com/php-yubico/ require_once 'lib/Yubico.php'; // must be run within Dokuwiki if(!defined('DOKU_INC')) die(); /* * Class auth_plugin_authyubikey simply extends the basic * auth_plugin_authplain class definition by Andreas Gohr * . */ class auth_plugin_authyubikey extends auth_plugin_authplain { /** * Constructor * * Carry out sanity checks to ensure the object is * able to operate. Set capabilities. * * @author Dirk Scheer */ public function __construct() { parent::__construct(); } /** * Check user+password * * Checks if the given user exists and the given * plaintext password is correct * * @author Dirk Scheer * @param string $user * @param string $pass * @return bool */ public function checkPass($user, $pass) { global $INPUT; global $config; /* Get all defined users with their attributes */ $userinfo = $this->getUserData($user); if($userinfo === false) return false; /* Check the given password */ if(auth_verifyPassword($pass, $this->users[$user]['pass']) === false) return false; /* If this function is called in another context as the login form; * then checking of the password is enough. * (I hope, this is not a security risc!!!) */ if($INPUT->str('do') !== 'login') { return true; } /* Get the yubikey IDs of the user. If the user has no IDs, * no further checking is needed for this user. */ $yubikeys = $this->users[$user]['yubi']; if(count($yubikeys) === 0) return true; /* Get the one-time password, the user has entered * in the login form. From this OTP we have to extract the * first 12 bytes. These bytes build the ID of the key, which * is stored in the yubikey-mapping file. */ $otp = $INPUT->str('otp'); $yid = substr($otp, 0, 12); if(in_array($yid, $yubikeys) === false) return false; /* A corresponding Yubikey ID was found, so we will check * finally the entered OTP against the servers of Yubico. */ $yubi = new Auth_Yubico($this->getConf('yubico_client_id'), $this->getConf('yubico_secret_key')); $auth = $yubi->verify($otp); return (PEAR::isError($auth) ? false : true); } /** * Modify user data * * @author Dirk Scheer * @author Chris Smith * @param string $user nick of the user to be changed * @param array $changes array of field/value pairs to be changed (password will be clear text) * @return bool */ public function modifyUser($user, $changes) { global $ACT; global $INPUT; global $conf; global $config_cascade; // sanity checks, user must already exist and there must be something to change if(($userinfo = $this->getUserData($user)) === false) return false; if(!is_array($changes) || !count($changes)) return true; // update userinfo with new data, remembering to encrypt any password $newuser = $user; foreach($changes as $field => $value) { if($field == 'user') { $newuser = $value; continue; } if($field == 'pass') $value = auth_cryptPassword($value); $userinfo[$field] = $value; } // Check all entered Yubikeys $yubi = new Auth_Yubico($this->getConf('yubico_client_id'), $this->getConf('yubico_secret_key')); $errors = array(); $userinfo['yubi'] = array(); for($i=0; $i < intval($this->getConf('yubico_maxkeys')); $i++) { $otp = $INPUT->str('yubikeyid'.$i); if($otp !== '') { if($otp == $this->users[$user]['yubi'][$i]) { array_push($userinfo['yubi'], substr($otp, 0, 12)); } else { $auth = $yubi->verify($otp); if(PEAR::isError($auth) && $auth != 'REPLAYED_OTP') { if($this->getConf('yubico_maxkeys') == 1) { array_push($errors, sprintf($this->getLang('yubikeyiderr'), substr($otp, 0, 12), $auth)); } else { array_push($errors, sprintf($this->getLang('yubikeyidserr'), $i+1, substr($otp, 0, 12), $auth)); } } else { array_push($userinfo['yubi'], substr($otp, 0, 12)); } } } } if(count($errors) > 0) { foreach($errors as $error) { $errtext .= $error . '
'; } msg($errtext, -1); return false; } $userline = $this->_createUserLine($newuser, $userinfo['pass'], $userinfo['name'], $userinfo['mail'], $userinfo['grps']); if(!$this->deleteUsers(array($user))) { msg($this->getLang('yubikeymodifyerr1'), -1); return false; } if(!io_saveFile($config_cascade['plainauth.users']['default'], $userline, true)) { msg($this->getLang('yubikeymodifyerr2'), -1); // FIXME, user has been deleted but not recreated, should force a logout and redirect to login page $ACT = 'register'; return false; } $yubiline = ''; foreach($userinfo['yubi'] as $yubi) { $yubiline .= $newuser . ':' . $yubi . "\n"; } if(!io_saveFile(DOKU_CONF . 'users.yubikeys.php', $yubiline, true)) { msg($this->getLang('yubikeymodifyerr3'), -1); return false; } $this->users[$newuser] = $userinfo; return true; } /** * Remove one or more users from the list of registered users * * @author Dirk Scheer * @author Christopher Smith * @param array $users array of users to be deleted * @return int the number of users deleted */ public function deleteUsers($users) { global $config_cascade; if(!is_array($users) || empty($users)) return 0; if($this->users === null) $this->_loadUserData(); $deleted = array(); foreach($users as $user) { if(isset($this->users[$user])) $deleted[] = preg_quote($user, '/'); } if(empty($deleted)) return 0; $pattern = '/^('.join('|', $deleted).'):/'; io_deleteFromFile($config_cascade['plainauth.users']['default'], $pattern, true); io_deleteFromFile(DOKU_CONF . 'users.yubikeys.php', $pattern, true); // reload the user list and count the difference $count = count($this->users); $this->_loadUserData(); $count -= count($this->users); return $count; } /** * Load all user data * * loads the user file into a datastructure * * @author Dirk Scheer * @author Andreas Gohr */ protected function _loadUserData() { global $config_cascade; $this->users = array(); if(!@file_exists($config_cascade['plainauth.users']['default'])) return; $lines = file($config_cascade['plainauth.users']['default']); foreach($lines as $line) { $line = preg_replace('/#.*$/', '', $line); //ignore comments $line = trim($line); if(empty($line)) continue; /* NB: preg_split can be deprecated/replaced with str_getcsv once dokuwiki is min php 5.3 */ $row = $this->_splitUserData($line); $row = str_replace('\\:', ':', $row); $row = str_replace('\\\\', '\\', $row); $groups = array_values(array_filter(explode(",", $row[4]))); $this->users[$row[0]]['pass'] = $row[1]; $this->users[$row[0]]['name'] = urldecode($row[2]); $this->users[$row[0]]['mail'] = $row[3]; $this->users[$row[0]]['grps'] = $groups; $this->users[$row[0]]['yubi'] = array(); } /* Read the mapping table for Yubikeys */ $lines = file(DOKU_CONF . 'users.yubikeys.php'); foreach($lines as $line) { $line = preg_replace('/#.*$/', '', $line); //ignore comments $line = trim($line); if(empty($line)) continue; list($user, $yubikey) = explode(':', $line); if(isset($this->users[$user])) { array_push($this->users[$user]['yubi'], $yubikey); } } } }