PHP OpenID consumer
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

keymanager.inc.php 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. <?PHP
  2. /* Poidsy 0.6 - http://chris.smith.name/projects/poidsy
  3. * Copyright (c) 2008-2010 Chris Smith
  4. *
  5. * Permission is hereby granted, free of charge, to any person obtaining a copy
  6. * of this software and associated documentation files (the "Software"), to deal
  7. * in the Software without restriction, including without limitation the rights
  8. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. * copies of the Software, and to permit persons to whom the Software is
  10. * furnished to do so, subject to the following conditions:
  11. *
  12. * The above copyright notice and this permission notice shall be included in
  13. * all copies or substantial portions of the Software.
  14. *
  15. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. * SOFTWARE.
  22. */
  23. require_once(dirname(__FILE__) . '/bigmath.inc.php');
  24. require_once(dirname(__FILE__) . '/logging.inc.php');
  25. require_once(dirname(__FILE__) . '/poster.inc.php');
  26. require_once(dirname(__FILE__) . '/urlbuilder.inc.php');
  27. class KeyManager {
  28. /** Diffie-Hellman P value, defined by OpenID specification. */
  29. const DH_P = '155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443';
  30. /** Diffie-Hellman G value. */
  31. const DH_G = '2';
  32. private static $header = null;
  33. private static $data = null;
  34. private static $bigmath = null;
  35. /**
  36. * Loads the KeyManager's data array from disk.
  37. */
  38. private static function loadData() {
  39. if (self::$data == null) {
  40. $data = file(dirname(__FILE__) . '/keycache.php');
  41. self::$header = array_shift($data);
  42. self::$data = unserialize(implode("\n", $data));
  43. }
  44. }
  45. /**
  46. * Saves the KeyManager's data array to disk.
  47. */
  48. private static function saveData() {
  49. file_put_contents(dirname(__FILE__) . '/keycache.php', self::$header . serialize(self::$data));
  50. }
  51. /**
  52. * Attempts to associate with the specified server.
  53. *
  54. * @param String $server The server to associate with
  55. */
  56. public static function associate($server, $assocType = null, $sessionType = null) {
  57. Logger::log('Attempting to associate with %s, assocType: %s, sessionType: %s', $server, $assocType, $sessionType);
  58. $data = URLBuilder::buildAssociate($server, $_SESSION['openid']['version'], $assocType, $sessionType);
  59. try {
  60. $res = Poster::post($server, $data);
  61. } catch (Exception $ex) {
  62. Logger::log('Exception while posting: %s', $ex->getMessage());
  63. return;
  64. }
  65. $data = array();
  66. foreach (explode("\n", $res) as $line) {
  67. if (preg_match('/^(.*?):(.*)$/', $line, $m)) {
  68. $data[$m[1]] = $m[2];
  69. }
  70. }
  71. if (isset($data['error_code']) && $data['error_code'] == 'unsupported-type') {
  72. $cont = false;
  73. if (isset($data['session_type']) && $data['session_type'] != $sessionType) {
  74. // TODO: Check we support it before actually trying
  75. $sessionType = $data['session_type'];
  76. $cont = true;
  77. }
  78. if (isset($data['assoc_type']) && $data['assoc_type'] != $assocType) {
  79. $assocType = $data['assoc_type'];
  80. $cont = true;
  81. }
  82. if ($cont) {
  83. self::associate($server, $assocType, $sessionType);
  84. }
  85. return;
  86. }
  87. try {
  88. $data = self::decodeKey($server, $data);
  89. } catch (Exception $ex) {
  90. return;
  91. }
  92. $data['expires_at'] = time() + $data['expires_in'];
  93. self::$data[$server][$data['assoc_handle']] = $data;
  94. self::saveData();
  95. }
  96. /**
  97. * Decodes the MAC key specified in the $data array.
  98. *
  99. * @param String $server The server which sent the data
  100. * @param Array $data Array of association data from the server
  101. * @return A copy of the $data array with the mac_key present
  102. */
  103. private static function decodeKey($server, $data) {
  104. switch (strtolower($data['session_type'])) {
  105. case 'dh-sha1':
  106. $algo = 'sha1';
  107. break;
  108. case 'dh-sha256':
  109. $algo = 'sha256';
  110. break;
  111. case 'no-encryption':
  112. case 'blank':
  113. case '':
  114. $algo = false;
  115. break;
  116. default:
  117. throw new Exception('Unable to handle session type ' . $data['session_type']);
  118. }
  119. if ($algo !== false) {
  120. // The key is DH'd
  121. $mac = base64_decode($data['enc_mac_key']);
  122. $x = self::getDhPrivateKey($server);
  123. $temp = self::$bigmath->btwoc_undo(base64_decode($data['dh_server_public']));
  124. $temp = self::$bigmath->powmod($temp, $x, self::DH_P);
  125. $temp = self::$bigmath->btwoc($temp);
  126. $temp = hash($algo, $temp, true);
  127. $mac = $mac ^ $temp;
  128. $data['mac_key'] = base64_encode($mac);
  129. unset($data['enc_mac_key'], $data['dh_server_public']);
  130. }
  131. return $data;
  132. }
  133. /**
  134. * Retrieves an active assoc_handle for the specified server.
  135. *
  136. * @param String $server The server whose handle we're looking for
  137. * @return An association handle for the server or null on failure
  138. */
  139. public static function getHandle($server) {
  140. self::loadData();
  141. if (!isset(self::$data[$server])) {
  142. return null;
  143. }
  144. foreach (self::$data[$server] as $handle => $data) {
  145. if ($handle == '__private') { continue; }
  146. if ($data['expires_at'] < time()) {
  147. unset(self::$data[$server][$handle]);
  148. } else {
  149. return $handle;
  150. }
  151. }
  152. return null;
  153. }
  154. /**
  155. * Determines if the KeyManager has at least one assoc_handle for the
  156. * specified server.
  157. *
  158. * @param String $server The server to check for
  159. * @return True if the KeyManager has a handle, false otherwise
  160. */
  161. public static function hasHandle($server) {
  162. return self::getHandle($server) !== null;
  163. }
  164. /**
  165. * Retrieves the association data array for the specified server and assoc
  166. * handle.
  167. *
  168. * @param String $server The server whose data is being requested
  169. * @param String $handle The current association handle for the server
  170. * @return Array of association data or null if none was found
  171. */
  172. public static function getData($server, $handle) {
  173. self::loadData();
  174. if (isset(self::$data[$server][$handle])) {
  175. if (self::$data[$server][$handle]['expires_at'] < time()) {
  176. self::revokeHandle($server, $handle);
  177. return null;
  178. } else {
  179. return self::$data[$server][$handle];
  180. }
  181. } else {
  182. return null;
  183. }
  184. }
  185. /**
  186. * Attempts to authenticate that the specified arguments are a valid query
  187. * from the specified server. If smart authentication is not available
  188. * or an error is encountered, throws an exception.
  189. *
  190. * @param String $server The server that supposedly sent the request
  191. * @param Array $args The arguments included in the request
  192. * @return True if the message was authenticated, false if it's a fake
  193. */
  194. public static function authenticate($server, $args) {
  195. $data = self::getData($server, $args['openid_assoc_handle']);
  196. if ($data === null) {
  197. throw new Exception('No key available for that server/handle');
  198. }
  199. $contents = '';
  200. foreach (explode(',', $args['openid_signed']) as $arg) {
  201. $argn = str_replace('.', '_', $arg);
  202. $contents .= $arg . ':' . $args['openid_' . $argn] . "\n";
  203. }
  204. switch (strtolower($data['assoc_type'])) {
  205. case 'hmac-sha1':
  206. $algo = 'sha1';
  207. break;
  208. case 'hmac-sha256':
  209. $algo = 'sha256';
  210. break;
  211. default:
  212. throw new Exception('Unable to handle association type ' . $data['assoc_type']);
  213. }
  214. $sig = base64_encode(hash_hmac($algo, $contents, base64_decode($data['mac_key']), true));
  215. if ($sig == $args['openid_sig']) {
  216. return true;
  217. } else {
  218. return false;
  219. }
  220. }
  221. /**
  222. * Validates the current request using dumb authentication (a POST to the
  223. * provider).
  224. *
  225. * @return True if the request has been authenticated, false otherwise.
  226. */
  227. public static function dumbAuthenticate() {
  228. $url = URLBuilder::buildAuth($_REQUEST, $_SESSION['openid']['version']);
  229. try {
  230. $data = Poster::post($_SESSION['openid']['endpointUrl'], $url);
  231. } catch (Exception $ex) {
  232. return false;
  233. }
  234. $valid = false;
  235. foreach (explode("\n", $data) as $line) {
  236. if (substr($line, 0, 9) == 'is_valid:') {
  237. $valid = (boolean) substr($line, 9);
  238. }
  239. }
  240. return $valid;
  241. }
  242. /**
  243. * Removes the specified association handle from the specified server's
  244. * records.
  245. *
  246. * @param String $server The server which is revoking the handle
  247. * @param String $handle The handle which is being revoked
  248. */
  249. public static function revokeHandle($server, $handle) {
  250. self::loadData();
  251. unset(self::$data[$server][$handle]);
  252. self::saveData();
  253. }
  254. /**
  255. * Determines if the keymanager is supported by the local environment or not.
  256. *
  257. * @return True if the keymanager can be used, false otherwise
  258. */
  259. public static function isSupported() {
  260. return @is_writable(dirname(__FILE__) . '/keycache.php')
  261. && function_exists('hash_hmac');
  262. }
  263. /**
  264. * Returns the base64-encoded representation of the dh_modulus parameter.
  265. *
  266. * @return Base64-encoded representation of dh_modulus
  267. */
  268. public static function getDhModulus() {
  269. return base64_encode(self::$bigmath->btwoc(self::DH_P));
  270. }
  271. /**
  272. * Returns the base64-encoded representation of the dh_gen parameter.
  273. *
  274. * @return Base64-encoded representation of dh_gen
  275. */
  276. public static function getDhGen() {
  277. return base64_encode(self::$bigmath->btwoc(self::DH_G));
  278. }
  279. /**
  280. * Retrieves our private key for the specified server.
  281. *
  282. * @param String $server The server which we're communicating with
  283. * @return Our private key for the specified server, or null if we don't have
  284. * one
  285. */
  286. public static function getDhPrivateKey($server) {
  287. self::loadData();
  288. if (isset(self::$data[$server])) {
  289. return self::$data[$server]['__private'];
  290. } else {
  291. return null;
  292. }
  293. }
  294. /**
  295. * Retrieves our public key for use with the specified server.
  296. *
  297. * @param String $server The server we wish to send the public key to
  298. * @return Base64-encoded public key for the specified server
  299. */
  300. public static function getDhPublicKey($server) {
  301. self::loadData();
  302. $key = self::createDhKey($server);
  303. self::saveData();
  304. return base64_encode(self::$bigmath->btwoc(self::$bigmath->powmod(self::DH_G, $key, self::DH_P)));
  305. }
  306. /**
  307. * Creates a private key for use when exchanging keys with the specified
  308. * server.
  309. *
  310. * @param String $server The name of the server we're dealing with
  311. * @return The server's new private key
  312. */
  313. private static function createDhKey($server) {
  314. return self::$data[$server]['__private'] = self::$bigmath->rand(self::DH_P);
  315. }
  316. /**
  317. * Determines whether the keymanager can support Diffie-Hellman key exchange.
  318. *
  319. * @return True if D-H exchange is supported, false otherwise.
  320. */
  321. public static function supportsDH() {
  322. return self::$bigmath != null;
  323. }
  324. /**
  325. * Initialises the key manager.
  326. */
  327. public static function init() {
  328. self::$bigmath = BigMath::getBigMath();
  329. }
  330. }
  331. KeyManager::init();
  332. ?>