1 | <?php |
||
52 | class SCRAM extends AbstractAuthentication implements ChallengeAuthenticationInterface, VerificationInterface |
||
53 | { |
||
54 | |||
55 | private $hashAlgo; |
||
56 | private $hash; |
||
57 | private $hmac; |
||
58 | private $gs2Header; |
||
59 | private $cnonce; |
||
60 | private $firstMessageBare; |
||
61 | private $saltedPassword; |
||
62 | private $authMessage; |
||
63 | |||
64 | /** |
||
65 | * Construct a SCRAM-H client where 'H' is a cryptographic hash function. |
||
66 | * |
||
67 | * @param Options $options |
||
68 | * @param string $hash The name cryptographic hash function 'H' as registered by IANA in the "Hash Function Textual |
||
69 | * Names" registry. |
||
70 | * @link http://www.iana.org/assignments/hash-function-text-names/hash-function-text-names.xml "Hash Function Textual |
||
71 | * Names" |
||
72 | * format of core PHP hash function. |
||
73 | * @throws InvalidArgumentException |
||
74 | */ |
||
75 | 4 | public function __construct(Options $options, $hash) |
|
76 | { |
||
77 | 4 | parent::__construct($options); |
|
78 | |||
79 | // Though I could be strict, I will actually also accept the naming used in the PHP core hash framework. |
||
80 | // For instance "sha1" is accepted, while the registered hash name should be "SHA-1". |
||
81 | 4 | $normalizedHash = str_replace('-', '', strtolower($hash)); |
|
82 | |||
83 | 4 | $hashAlgos = hash_algos(); |
|
84 | 4 | if (!in_array($normalizedHash, $hashAlgos)) { |
|
85 | 1 | throw new InvalidArgumentException("Invalid SASL mechanism type '$hash'"); |
|
86 | } |
||
87 | |||
88 | $this->hash = function ($data) use ($normalizedHash) { |
||
89 | 1 | return hash($normalizedHash, $data, true); |
|
90 | }; |
||
91 | |||
92 | $this->hmac = function ($key, $str, $raw) use ($normalizedHash) { |
||
93 | 1 | return hash_hmac($normalizedHash, $str, $key, $raw); |
|
94 | }; |
||
95 | |||
96 | 4 | $this->hashAlgo = $normalizedHash; |
|
97 | 4 | } |
|
98 | |||
99 | /** |
||
100 | * Provides the (main) client response for SCRAM-H. |
||
101 | * |
||
102 | * @param string $challenge The challenge sent by the server. |
||
103 | * If the challenge is null or an empty string, the result will be the "initial response". |
||
104 | * @return string|false The response (binary, NOT base64 encoded) |
||
105 | */ |
||
106 | 3 | public function createResponse($challenge = null) |
|
107 | { |
||
108 | 3 | $authcid = $this->formatName($this->options->getAuthcid()); |
|
109 | 3 | if (empty($authcid)) { |
|
110 | 1 | return false; |
|
111 | } |
||
112 | |||
113 | 2 | $authzid = $this->options->getAuthzid(); |
|
114 | 2 | if (!empty($authzid)) { |
|
115 | 2 | $authzid = $this->formatName($authzid); |
|
116 | } |
||
117 | |||
118 | 2 | if (empty($challenge)) { |
|
119 | 2 | return $this->generateInitialResponse($authcid, $authzid); |
|
120 | } else { |
||
121 | 1 | return $this->generateResponse($challenge, $this->options->getSecret()); |
|
122 | } |
||
123 | } |
||
124 | |||
125 | /** |
||
126 | * Prepare a name for inclusion in a SCRAM response. |
||
127 | * |
||
128 | * @param string $username a name to be prepared. |
||
129 | * @return string the reformated name. |
||
130 | */ |
||
131 | 1 | private function formatName($username) |
|
135 | |||
136 | /** |
||
137 | * Generate the initial response which can be either sent directly in the first message or as a response to an empty |
||
138 | * server challenge. |
||
139 | * |
||
140 | * @param string $authcid Prepared authentication identity. |
||
141 | * @param string $authzid Prepared authorization identity. |
||
142 | * @return string The SCRAM response to send. |
||
143 | */ |
||
144 | 1 | private function generateInitialResponse($authcid, $authzid) |
|
155 | |||
156 | /** |
||
157 | * Parses and verifies a non-empty SCRAM challenge. |
||
158 | * |
||
159 | * @param string $challenge The SCRAM challenge |
||
160 | * @return string|false The response to send; false in case of wrong challenge or if an initial response has not |
||
161 | * been generated first. |
||
162 | */ |
||
163 | 3 | private function generateResponse($challenge, $password) |
|
201 | |||
202 | /** |
||
203 | * Hi() call, which is essentially PBKDF2 (RFC-2898) with HMAC-H() as the pseudorandom function. |
||
204 | * |
||
205 | * @param string $str The string to hash. |
||
206 | * @param string $salt The salt value. |
||
207 | * @param int $i The iteration count. |
||
208 | */ |
||
209 | 1 | private function hi($str, $salt, $i) |
|
210 | { |
||
211 | 1 | $int1 = "\0\0\0\1"; |
|
212 | 1 | $ui = call_user_func($this->hmac, $str, $salt . $int1, true); |
|
213 | 1 | $result = $ui; |
|
214 | 1 | for ($k = 1; $k < $i; $k++) { |
|
215 | 1 | $ui = call_user_func($this->hmac, $str, $ui, true); |
|
216 | 1 | $result = $result ^ $ui; |
|
217 | } |
||
218 | 1 | return $result; |
|
219 | } |
||
220 | |||
221 | /** |
||
222 | * SCRAM has also a server verification step. On a successful outcome, it will send additional data which must |
||
223 | * absolutely be checked against this function. If this fails, the entity which we are communicating with is probably |
||
224 | * not the server as it has not access to your ServerKey. |
||
225 | * |
||
226 | * @param string $data The additional data sent along a successful outcome. |
||
227 | * @return bool Whether the server has been authenticated. |
||
228 | * If false, the client must close the connection and consider to be under a MITM attack. |
||
229 | */ |
||
230 | 2 | public function verify($data) |
|
247 | |||
248 | /** |
||
249 | * @return string |
||
250 | */ |
||
251 | 1 | public function getCnonce() |
|
255 | |||
256 | 1 | public function getSaltedPassword() |
|
260 | |||
261 | 1 | public function getAuthMessage() |
|
265 | |||
266 | 1 | public function getHashAlgo() |
|
270 | } |
||
271 |