PageRenderTime 96ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/library/Zend/Crypt/Password/Apache.php

https://bitbucket.org/gencer/zf2
PHP | 298 lines | 184 code | 23 blank | 91 comment | 28 complexity | fcf07f3fda06f6fd3a39afb9b1ecb784 MD5 | raw file
  1. <?php
  2. /**
  3. * Zend Framework (http://framework.zend.com/)
  4. *
  5. * @link http://github.com/zendframework/zf2 for the canonical source repository
  6. * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
  7. * @license http://framework.zend.com/license/new-bsd New BSD License
  8. */
  9. namespace Zend\Crypt\Password;
  10. use Traversable;
  11. use Zend\Math\Rand;
  12. /**
  13. * Apache password authentication
  14. *
  15. * @see http://httpd.apache.org/docs/2.2/misc/password_encryptions.html
  16. */
  17. class Apache implements PasswordInterface
  18. {
  19. CONST BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
  20. CONST ALPHA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
  21. /**
  22. * @var array
  23. */
  24. protected $supportedFormat = array(
  25. 'crypt',
  26. 'sha1',
  27. 'md5',
  28. 'digest',
  29. );
  30. /**
  31. * @var string
  32. */
  33. protected $format;
  34. /**
  35. * @var string AuthName (realm) for digest authentication
  36. */
  37. protected $authName;
  38. /**
  39. * @var string UserName
  40. */
  41. protected $userName;
  42. /**
  43. * Constructor
  44. *
  45. * @param array|Traversable $options
  46. * @throws Exception\InvalidArgumentException
  47. */
  48. public function __construct($options = array())
  49. {
  50. if (empty($options)) {
  51. return;
  52. }
  53. if (!is_array($options) && !$options instanceof Traversable) {
  54. throw new Exception\InvalidArgumentException(
  55. 'The options parameter must be an array or a Traversable'
  56. );
  57. }
  58. foreach ($options as $key => $value) {
  59. switch (strtolower($key)) {
  60. case 'format':
  61. $this->setFormat($value);
  62. break;
  63. case 'authname':
  64. $this->setAuthName($value);
  65. break;
  66. case 'username':
  67. $this->setUserName($value);
  68. break;
  69. }
  70. }
  71. }
  72. /**
  73. * Generate the hash of a password
  74. *
  75. * @param string $password
  76. * @throws Exception\RuntimeException
  77. * @return string
  78. */
  79. public function create($password)
  80. {
  81. if (empty($this->format)) {
  82. throw new Exception\RuntimeException(
  83. 'You must specify a password format'
  84. );
  85. }
  86. switch ($this->format) {
  87. case 'crypt' :
  88. $hash = crypt($password, Rand::getString(2, self::ALPHA64));
  89. break;
  90. case 'sha1' :
  91. $hash = '{SHA}' . base64_encode(sha1($password, true));
  92. break;
  93. case 'md5' :
  94. $hash = $this->apr1Md5($password);
  95. break;
  96. case 'digest':
  97. if (empty($this->userName) || empty($this->authName)) {
  98. throw new Exception\RuntimeException(
  99. 'You must specify UserName and AuthName (realm) to generate the digest'
  100. );
  101. }
  102. $hash = md5($this->userName . ':' . $this->authName . ':' .$password);
  103. break;
  104. }
  105. return $hash;
  106. }
  107. /**
  108. * Verify if a password is correct against a hash value
  109. *
  110. * @param string $password
  111. * @param string $hash
  112. * @return bool
  113. */
  114. public function verify($password, $hash)
  115. {
  116. if (substr($hash, 0, 5) === '{SHA}') {
  117. $hash2 = '{SHA}' . base64_encode(sha1($password, true));
  118. return ($hash === $hash2);
  119. }
  120. if (substr($hash, 0, 6) === '$apr1$') {
  121. $token = explode('$', $hash);
  122. if (empty($token[2])) {
  123. throw new Exception\InvalidArgumentException(
  124. 'The APR1 password format is not valid'
  125. );
  126. }
  127. $hash2 = $this->apr1Md5($password, $token[2]);
  128. return ($hash === $hash2);
  129. }
  130. if (strlen($hash) > 13) { // digest
  131. if (empty($this->userName) || empty($this->authName)) {
  132. throw new Exception\RuntimeException(
  133. 'You must specify UserName and AuthName (realm) to verify the digest'
  134. );
  135. }
  136. $hash2 = md5($this->userName . ':' . $this->authName . ':' .$password);
  137. return ($hash === $hash2);
  138. }
  139. return (crypt($password, $hash) === $hash);
  140. }
  141. /**
  142. * Set the format of the password
  143. *
  144. * @param string $format
  145. * @throws Exception\InvalidArgumentException
  146. * @return Apache
  147. */
  148. public function setFormat($format)
  149. {
  150. $format = strtolower($format);
  151. if (!in_array($format, $this->supportedFormat)) {
  152. throw new Exception\InvalidArgumentException(sprintf(
  153. 'The format %s specified is not valid. The supported formats are: %s',
  154. $format, implode(',', $this->supportedFormat)
  155. ));
  156. }
  157. $this->format = $format;
  158. return $this;
  159. }
  160. /**
  161. * Get the format of the password
  162. *
  163. * @return string
  164. */
  165. public function getFormat()
  166. {
  167. return $this->format;
  168. }
  169. /**
  170. * Set the AuthName (for digest authentication)
  171. *
  172. * @param string $name
  173. * @return Apache
  174. */
  175. public function setAuthName($name)
  176. {
  177. $this->authName = $name;
  178. return $this;
  179. }
  180. /**
  181. * Get the AuthName (for digest authentication)
  182. *
  183. * @return string
  184. */
  185. public function getAuthName()
  186. {
  187. return $this->authName;
  188. }
  189. /**
  190. * Set the username
  191. *
  192. * @param string $name
  193. * @return Apache
  194. */
  195. public function setUserName($name)
  196. {
  197. $this->userName = $name;
  198. return $this;
  199. }
  200. /**
  201. * Get the username
  202. *
  203. * @return string
  204. */
  205. public function getUserName()
  206. {
  207. return $this->userName;
  208. }
  209. /**
  210. * Convert a binary string using the alphabet "./0-9A-Za-z"
  211. *
  212. * @param string $value
  213. * @return string
  214. */
  215. protected function toAlphabet64($value)
  216. {
  217. return strtr(strrev(substr(base64_encode($value), 2)), self::BASE64, self::ALPHA64);
  218. }
  219. /**
  220. * APR1 MD5 algorithm
  221. *
  222. * @param string $password
  223. * @param null|string $salt
  224. * @return string
  225. */
  226. protected function apr1Md5($password, $salt = null)
  227. {
  228. if (null === $salt) {
  229. $salt = Rand::getString(8, self::ALPHA64);
  230. } else {
  231. if (strlen($salt) !== 8) {
  232. throw new Exception\InvalidArgumentException(
  233. 'The salt value for APR1 algorithm must be 8 characters long'
  234. );
  235. }
  236. for ($i = 0; $i < 8; $i++) {
  237. if (strpos(self::ALPHA64, $salt[$i]) === false) {
  238. throw new Exception\InvalidArgumentException(
  239. 'The salt value must be a string in the alphabet "./0-9A-Za-z"'
  240. );
  241. }
  242. }
  243. }
  244. $len = strlen($password);
  245. $text = $password . '$apr1$' . $salt;
  246. $bin = pack("H32", md5($password . $salt . $password));
  247. for ($i = $len; $i > 0; $i -= 16) {
  248. $text .= substr($bin, 0, min(16, $i));
  249. }
  250. for ($i = $len; $i > 0; $i >>= 1) {
  251. $text .= ($i & 1) ? chr(0) : $password[0];
  252. }
  253. $bin = pack("H32", md5($text));
  254. for ($i = 0; $i < 1000; $i++) {
  255. $new = ($i & 1) ? $password : $bin;
  256. if ($i % 3) {
  257. $new .= $salt;
  258. }
  259. if ($i % 7) {
  260. $new .= $password;
  261. }
  262. $new .= ($i & 1) ? $bin : $password;
  263. $bin = pack("H32", md5($new));
  264. }
  265. $tmp = '';
  266. for ($i = 0; $i < 5; $i++) {
  267. $k = $i + 6;
  268. $j = $i + 12;
  269. if ($j == 16) $j = 5;
  270. $tmp = $bin[$i] . $bin[$k] . $bin[$j] . $tmp;
  271. }
  272. $tmp = chr(0) . chr(0) . $bin[11] . $tmp;
  273. return '$apr1$' . $salt . '$' . $this->toAlphabet64($tmp);
  274. }
  275. }