PageRenderTime 52ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/core/OneTimePassword/OneTimePasswordGenerator.class.php

https://bitbucket.org/multimedium/bob361
PHP | 414 lines | 89 code | 41 blank | 284 comment | 9 complexity | 75cb72406323d31ab1a8b503f24ebf9f MD5 | raw file
Possible License(s): LGPL-2.0
  1. <?php
  2. /**
  3. * M_OneTimePasswordGenerator
  4. *
  5. * @package Core
  6. */
  7. class M_OneTimePasswordGenerator extends M_OneTimePasswordSystem {
  8. /* -- PROPERTIES -- */
  9. /**
  10. * Seed
  11. *
  12. * The seed is provided by the One-Time Password Server, as part of the
  13. * challenge.
  14. *
  15. * @see M_OneTimePasswordGenerator::setChallenge()
  16. * @access private
  17. * @var string
  18. */
  19. private $_seed;
  20. /**
  21. * Algorithm
  22. *
  23. * The algorithm, or hash function, is provided by the One-Time Password
  24. * Server, as part of the challenge.
  25. *
  26. * @see M_OneTimePasswordGenerator::setChallenge()
  27. * @see M_OneTimePasswordHelper::getSupportedHashFunctions()
  28. * @access private
  29. * @var string
  30. */
  31. private $_hashFunction;
  32. /**
  33. * Sequence number
  34. *
  35. * The sequence number is provided by the One-Time Password Server, as part
  36. * of the challenge.
  37. *
  38. * @see M_OneTimePasswordGenerator::setChallenge()
  39. * @access private
  40. * @var integer
  41. */
  42. private $_sequenceNumber;
  43. /**
  44. * Pass-phrase
  45. *
  46. * The secret pass-phrase is known only to this object. For more information,
  47. * read the docs on {@link M_OneTimePasswordGenerator::setPassPhrase()}
  48. *
  49. * @see M_OneTimePasswordGenerator::setPassPhrase()
  50. * @access private
  51. * @var string
  52. */
  53. private $_passPhrase;
  54. /**
  55. * One-Time Password
  56. *
  57. * This property stores the one-time password that is generated by this object,
  58. * with {@link M_OneTimePasswordGenerator::getOneTimePassword()}.
  59. *
  60. * @see M_OneTimePasswordGenerator::getOneTimePassword()
  61. * @see M_OneTimePasswordGenerator::getOneTimePasswordHexadecimal()
  62. * @see M_OneTimePasswordGenerator::getOneTimePasswordInDictionaryWords()
  63. * @access private
  64. * @var string
  65. */
  66. private $_oneTimePassword;
  67. /* -- CONSTRUCTOR -- */
  68. /**
  69. * Constructor
  70. *
  71. * @uses M_OneTimePasswordGenerator::setChallenge()
  72. * @access public
  73. * @param string $otpServerChallenge
  74. * The challenge, provided by the server
  75. * @return M_OneTimePasswordGenerator
  76. */
  77. public function __construct($otpServerChallenge = NULL) {
  78. // If a challenge has been provided to the constructor:
  79. if($otpServerChallenge) {
  80. // Then, we set the challenge in the generator object:
  81. $this->setChallenge($otpServerChallenge);
  82. }
  83. }
  84. /* -- SETTERS -- */
  85. /**
  86. * Set the challenge, provided by the One-Time Password Server
  87. *
  88. * The seed is provided by the One-Time Password Server, as part of the
  89. * challenge. The pass-phrase, known only to the One-Time Password Generator,
  90. * is concatenated with a seed that is transmitted from the server in clear
  91. * text. This non-secret seed allows clients to use the same secret pass-phrase
  92. * on multiple machines (using different seeds) and to safely recycle their
  93. * secret pass-phrases by changing the seed.
  94. *
  95. * The sequence number and seed together constitute a larger unit of data
  96. * called the challenge. The challenge gives the generator the parameters it
  97. * needs to calculate the correct one-time password from the secret
  98. * pass-phrase. The challenge MUST be in a standard syntax so that automated
  99. * generators can recognize the challenge in context and extract these
  100. * parameters. The syntax of the challenge is:
  101. *
  102. * <code>otp-<algorithm identifier> <sequence integer> <seed></code>
  103. *
  104. * The three tokens MUST be separated by a white space (defined as any number
  105. * of spaces and/or tabs) and the entire challenge string MUST be terminated
  106. * with either a space or a new line. The string "otp-" MUST be in lower case.
  107. * The algorithm identifier is case sensitive (the existing identifiers are
  108. * all lower case), and the seed is case insensitive and converted before use
  109. * to lower case.
  110. *
  111. * An example of a valid OTP challenge would be:
  112. *
  113. * <code>otp-sha1 487 dog2</code>
  114. *
  115. * NOTE:
  116. * This method will throw an exception, if the provided challenge could not
  117. * be parsed correctly due to a corrupted format.
  118. *
  119. * @throws M_OneTimePasswordException
  120. * @access public
  121. * @param string $otpServerChallenge
  122. * @return M_OneTimePasswordGenerator $generator
  123. * Returns itself, for a fluent programming interface
  124. */
  125. public function setChallenge($otpServerChallenge) {
  126. // We split up the provided challenge string into 3 pieces: the algorithm,
  127. // the sequence number, and the seed:
  128. $parts = preg_split('/[\s]+/m', M_Helper::trimCharlist($otpServerChallenge));
  129. // We make sure that exactly 3 parts have been included in the provided
  130. // challenge:
  131. if(count($parts) != 3) {
  132. // If that's not the case, then the provided challenge is not a valid
  133. // One-Time Password Server Challenge. We throw an exception, to inform
  134. // about the error:
  135. throw new M_OneTimePasswordException(sprintf(
  136. 'Could not parse the One-Time Password Server Challenge "%s". ' .
  137. 'Expecting exactly 3 elements in the challenge: the algorithm ' .
  138. '(eg. otp-sha1), the sequence number (eg 471) and the seed (eg dog2)',
  139. $otpServerChallenge
  140. ));
  141. }
  142. // If we are still here, then we know that the challenge does contain
  143. // exactly 3 elements. We extract the hash function name:
  144. $hashFunction = substr($parts[0], 4);
  145. // We set the internals for the generator:
  146. $this
  147. ->_setHashFunction($hashFunction)
  148. ->_setSeed($parts[2])
  149. ->_setSequenceNumber((int) $parts[1]);
  150. // Returns itself:
  151. return $this;
  152. }
  153. /**
  154. * Set secret pass-phrase
  155. *
  156. * The generator must produce the appropriate one-time password from the
  157. * user's secret pass-phrase and from information provided in the challenge
  158. * from the server.
  159. *
  160. * In principle, the user's secret pass-phrase may be of any length. To
  161. * reduce the risk from techniques such as exhaustive search or dictionary
  162. * attacks, character string pass-phrases MUST contain at least 10 characters.
  163. * All implementations MUST support a pass-phrases of at least 63 characters.
  164. * The secret pass-phrase is frequently, but is not required to be, textual
  165. * information provided by a user.
  166. *
  167. * The secret pass-phrase is seen only by the OTP generator. To allow
  168. * interchangeability of generators, all generators MUST support a secret
  169. * pass-phrase of 10 to 63 characters. Implementations MAY support a longer
  170. * pass-phrase, but such implementations risk the loss of interchangeability
  171. * with implementations supporting only the minimum.
  172. *
  173. * NOTE:
  174. * This method will comply with the RFC 2289 specifications, and make sure
  175. * that the provided pass-phrase adheres to the requirements. If the pass-
  176. * phrase does not, an exception will be thrown.
  177. *
  178. * @throws M_OneTimePasswordException
  179. * @access public
  180. * @param string $passPhrase
  181. * @return M_OneTimePasswordGenerator $generator
  182. * Returns itself, for a fluent programming interface
  183. */
  184. public function setPassPhrase($passPhrase) {
  185. // We make sure that the provided passPhrase is a string, of at least
  186. // 10 characters long:
  187. $isValid = ((is_string($passPhrase) && strlen($passPhrase) >= 10));
  188. // If that is not the case:
  189. if(! $isValid) {
  190. // We throw an exception, to inform about the error
  191. throw new M_OneTimePasswordException(sprintf(
  192. 'The pass-phrase "%s" cannot be used to generate one-time passwords. ' .
  193. 'The pass-phrase should be at least 10 characters long!',
  194. $passPhrase
  195. ));
  196. }
  197. // Set the pass-phrase
  198. $this->_passPhrase = $passPhrase;
  199. // Returns itself
  200. return $this;
  201. }
  202. /* -- GETTERS -- */
  203. /**
  204. * Get One-Time Password in hexadecimal output
  205. *
  206. * To facilitate the implementation of smaller generators, hexadecimal output
  207. * is an acceptable alternative for the presentation of the one-time password.
  208. * According to the RFC 2289 specifications, all implementations of the server
  209. * software MUST accept case-insensitive hexadecimal as well as six-word
  210. * format. The hexadecimal digits may be separated by white space so servers
  211. * are REQUIRED to ignore all white space. If the representation is partitioned
  212. * by white space, leading zeros must be retained.
  213. *
  214. * Examples of hexadecimal format:
  215. * <code>
  216. * Representation Value
  217. * 3503785b369cda8b 0x3503785b369cda8b
  218. * e5cc a1b8 7c13 096b 0xe5cca1b87c13096b
  219. * C7 48 90 F4 27 7B A1 CF 0xc74890f4277ba1cf
  220. * 47 9 A68 28 4C 9D 0 1BC 0x479a68284c9d01bc
  221. * </code>
  222. *
  223. * @access public
  224. * @return string $hex
  225. * The one-time password, in hexadecimal representation
  226. */
  227. public function getOneTimePasswordHexadecimal() {
  228. // Convert binary data into hexadecimal representation:
  229. return bin2hex($this->_getOneTimePassword());
  230. }
  231. /**
  232. * Get One-Time Password in dictionary words
  233. *
  234. * The one-time password generated is 64 bits in length. Entering a 64 bit
  235. * number is a difficult and error prone process. Some generators insert this
  236. * password into the input stream and some others make it available for
  237. * system "cut and paste." Still other arrangements require the one-time
  238. * password to be entered manually. The OTP system is designed to facilitate
  239. * this manual entry without impeding automatic methods. The one-time password
  240. * therefore MAY be converted to, and all servers MUST be capable of accepting
  241. * it as, a sequence of six short (1 to 4 letter) easily typed words that
  242. * only use characters from ISO-646 IVCS. Each word is chosen from a dictionary
  243. * of 2048 words; at 11 bits per word, all one-time passwords may be encoded.
  244. *
  245. * Generators that produce the six-word format MUST present the words in
  246. * upper case with single spaces used as separators. All servers MUST accept
  247. * six-word format without regard to case and white space used as a separator.
  248. *
  249. * @access public
  250. * @return string $words
  251. * The one-time password, in dictionary words
  252. */
  253. public function getOneTimePasswordInDictionaryWords() {
  254. // Output
  255. $out = '';
  256. // First we get the bit representation of the one-time password:
  257. $bitString = $this->getOneTimePasswordInBits();
  258. // The bit representation is one of 64 bits. Two extra bits are foreseen
  259. // for a two-bit checksum (needed by the otp server). The result is a
  260. // bit string representation of 66 bits, or 11 bits per word.
  261. $bitString .= $this->_getBitStringChecksum($bitString);
  262. // We get the dictionary. This dictionary may either be an alternative one,
  263. // or the default one...
  264. $dictionary = $this->getDictionary();
  265. // We run through the bit string in chunks of 11 bits. Dividing 66 by 11,
  266. // we know that we will need 6 loops to do so:
  267. for($i = 0; $i < 6; $i ++) {
  268. // Each chunk of 11 bits can be converted to its decimal equivalent.
  269. // This number will be used as a key, to look up the corresponding
  270. // word from the dictionary array. So, first we get the decimal number:
  271. $number = bindec(substr($bitString, ($i * 11), 11));
  272. // Then, we add the word from the dictionary:
  273. $out .= (empty($out) ? '' : ' ') . $dictionary[$number];
  274. }
  275. // We return the one-time password, in words:
  276. return $out;
  277. }
  278. /**
  279. *
  280. */
  281. public function getOneTimePasswordInDecimals() {
  282. // Output
  283. $out = '';
  284. // First we get the bit representation of the one-time password:
  285. $bitString = $this->getOneTimePasswordInBits();
  286. // The bit representation is one of 64 bits. Two extra bits are foreseen
  287. // for a two-bit checksum (needed by the otp server). The result is a
  288. // bit string representation of 66 bits, or 11 bits per number.
  289. $bitString .= $this->_getBitStringChecksum($bitString);
  290. // We run through the bit string in chunks of 11 bits. Dividing 66 by 11,
  291. // we know that we will need 6 loops to do so:
  292. for($i = 0; $i < 6; $i ++) {
  293. // Each chunk of 11 bits can be converted to its decimal equivalent.
  294. // This number will is typically used as a key, to look up the
  295. // corresponding word from the dictionary array. However, in this
  296. // format, we will use the number as-is...
  297. $out .= (empty($out) ? '' : ' ') . bindec(substr($bitString, ($i * 11), 11));
  298. }
  299. // We return the one-time password, in numbers:
  300. return $out;
  301. }
  302. /**
  303. *
  304. */
  305. public function getOneTimePasswordInBits() {
  306. return $this->_getBitStringFromHexadecimal($this->getOneTimePasswordHexadecimal());
  307. }
  308. /* -- PRIVATE/PROTECTED -- */
  309. /**
  310. * Get secret pass-phrase
  311. *
  312. * @access protected
  313. * @return string
  314. */
  315. protected function _getPassPhrase() {
  316. return $this->_passPhrase;
  317. }
  318. /**
  319. * Get One-Time Password
  320. *
  321. * The generator must produce the appropriate one-time password from the
  322. * user's secret pass-phrase and from information provided in the challenge
  323. * from the server.
  324. *
  325. * This method will produce the *binary* one-time password, and return it
  326. * as result. This method is used to produce the one-time password in other
  327. * formats, by:
  328. *
  329. * - {@link M_OneTimePasswordGenerator::getOneTimePasswordHexadecimal()}
  330. * - {@link M_OneTimePasswordGenerator::getOneTimePasswordInDictionaryWords()}
  331. * - {@link M_OneTimePasswordGenerator::getOneTimePasswordInDecimals()}
  332. * - {@link M_OneTimePasswordGenerator::getOneTimePasswordInBits()}
  333. *
  334. * @see M_OneTimePasswordGenerator::setChallenge()
  335. * @see M_OneTimePasswordGenerator::setPassPhrase()
  336. * @access protected
  337. * @return string
  338. */
  339. protected function _getOneTimePassword() {
  340. // If not requested before:
  341. if(! $this->_oneTimePassword) {
  342. // The hash function that is to be used:
  343. $hashFunction = $this->getHashFunction();
  344. // The first step is to concatenate the pass phrase with the seed. The
  345. // output of this step is called S
  346. $otp = $this->getSeed() . $this->_getPassPhrase();
  347. // A sequence of one-time passwords is produced by applying the secure
  348. // hash function multiple times to S. That is, the first one-time password
  349. // to be used is produced by passing S through the secure hash function
  350. // a number of times (N) specified by the user. The next one-time
  351. // password to be used is generated by passing S though the secure hash
  352. // function N-1 times.
  353. // For each of the times that S should be passed through the hash function:
  354. for($i = 0, $n = $this->getSequenceNumber(); $i <= $n; $i ++) {
  355. // We digest the current S, with the hash function:
  356. switch($hashFunction) {
  357. // SHA1:
  358. case 'sha1':
  359. $otp = $this->_getSha1($otp);
  360. break;
  361. // MD5:
  362. case 'md5':
  363. $otp = $this->_getMd5($otp);
  364. break;
  365. }
  366. }
  367. // Set the final one-time password:
  368. $this->_oneTimePassword = $otp;
  369. }
  370. // Return the one-time password:
  371. return $this->_oneTimePassword;
  372. }
  373. }