| 1: | <?php |
| 2: | |
| 3: | |
| 4: | |
| 5: | |
| 6: | |
| 7: | |
| 8: | |
| 9: | class ScramAuthenticator { |
| 10: | |
| 11: | private $hashes = array( |
| 12: | 'sha-1' => 'sha1', |
| 13: | 'sha1' => 'sha1', |
| 14: | 'sha-224' => 'sha224', |
| 15: | 'sha224' => 'sha224', |
| 16: | 'sha-256' => 'sha256', |
| 17: | 'sha256' => 'sha256', |
| 18: | 'sha-384' => 'sha384', |
| 19: | 'sha384' => 'sha384', |
| 20: | 'sha-512' => 'sha512', |
| 21: | 'sha512' => 'sha512' |
| 22: | ); |
| 23: | |
| 24: | private function getHashAlgorithm($scramAlgorithm) { |
| 25: | $parts = explode('-', mb_strtolower($scramAlgorithm)); |
| 26: | return $this->hashes[$parts[1]] ?? 'sha1'; |
| 27: | } |
| 28: | private function log($message) { |
| 29: | |
| 30: | Hm_Debug::add(sprintf($message)); |
| 31: | } |
| 32: | public function generateClientProof($username, $password, $salt, $clientNonce, $serverNonce, $algorithm) { |
| 33: | $iterations = 4096; |
| 34: | $keyLength = strlen(hash($algorithm, '', true)); |
| 35: | |
| 36: | $passwordBytes = hash($algorithm, $password, true); |
| 37: | $saltedPassword = hash_pbkdf2($algorithm, $passwordBytes, $salt, $iterations, $keyLength, true); |
| 38: | $clientKey = hash_hmac($algorithm, "Client Key", $saltedPassword, true); |
| 39: | $storedKey = hash($algorithm, $clientKey, true); |
| 40: | $authMessage = 'n=' . $username . ',r=' . $clientNonce . ',s=' . base64_encode($salt) . ',r=' . $serverNonce; |
| 41: | $clientSignature = hash_hmac($algorithm, $authMessage, $storedKey, true); |
| 42: | $clientProof = base64_encode($clientKey ^ $clientSignature); |
| 43: | $this->log("Client proof generated successfully"); |
| 44: | return $clientProof; |
| 45: | } |
| 46: | |
| 47: | public function authenticateScram($scramAlgorithm, $username, $password, $getServerResponse, $sendCommand) { |
| 48: | $algorithm = $this->getHashAlgorithm($scramAlgorithm); |
| 49: | |
| 50: | |
| 51: | $scramCommand = 'AUTHENTICATE ' . $scramAlgorithm . "\r\n"; |
| 52: | $sendCommand($scramCommand); |
| 53: | $response = $getServerResponse(); |
| 54: | if (!empty($response) && mb_substr($response[0], 0, 2) == '+ ') { |
| 55: | $this->log("Received server challenge: " . $response[0]); |
| 56: | |
| 57: | $serverChallenge = base64_decode(mb_substr($response[0], 2)); |
| 58: | $parts = explode(',', $serverChallenge); |
| 59: | $serverNonce = base64_decode(substr($parts[0], strpos($parts[0], "=") + 1)); |
| 60: | $salt = base64_decode(substr($parts[1], strpos($parts[1], "=") + 1)); |
| 61: | |
| 62: | |
| 63: | $clientNonce = base64_encode(random_bytes(32)); |
| 64: | $this->log("Generated client nonce: " . $clientNonce); |
| 65: | |
| 66: | |
| 67: | $clientProof = $this->generateClientProof($username, $password, $salt, $clientNonce, $serverNonce, $algorithm); |
| 68: | |
| 69: | |
| 70: | $channelBindingData = (mb_stripos($scramAlgorithm, 'plus') !== false) ? 'c=' . base64_encode('tls-unique') . ',' : 'c=biws,'; |
| 71: | $clientFinalMessage = $channelBindingData . 'r=' . $serverNonce . $clientNonce . ',p=' . $clientProof; |
| 72: | $clientFinalMessageEncoded = base64_encode($clientFinalMessage); |
| 73: | $this->log("Sending client final message: " . $clientFinalMessageEncoded); |
| 74: | |
| 75: | $sendCommand($clientFinalMessageEncoded . "\r\n"); |
| 76: | |
| 77: | |
| 78: | $response = $getServerResponse(); |
| 79: | if (!empty($response) && mb_substr($response[0], 0, 2) == '+ ') { |
| 80: | $serverFinalMessage = base64_decode(mb_substr($response[0], 2)); |
| 81: | $parts = explode(',', $serverFinalMessage); |
| 82: | $serverProof = substr($parts[0], strpos($parts[0], "=") + 1); |
| 83: | |
| 84: | |
| 85: | $passwordBytes = hash($algorithm, $password, true); |
| 86: | $saltedPassword = hash_pbkdf2($algorithm, $passwordBytes, $salt, 4096, strlen(hash($algorithm, '', true)), true); |
| 87: | $serverKey = hash_hmac($algorithm, "Server Key", $saltedPassword, true); |
| 88: | |
| 89: | |
| 90: | $authMessage = 'n=' . $username . ',r=' . $clientNonce . ',s=' . base64_encode($salt) . ',r=' . $serverNonce; |
| 91: | $serverSignature = base64_encode(hash_hmac($algorithm, $authMessage, $serverKey, true)); |
| 92: | |
| 93: | |
| 94: | if ($serverSignature === $serverProof) { |
| 95: | $this->log("SCRAM authentication successful"); |
| 96: | return true; |
| 97: | } else { |
| 98: | $this->log("SCRAM authentication failed: Server signature mismatch"); |
| 99: | } |
| 100: | } else { |
| 101: | $this->log("SCRAM authentication failed: Invalid server final response"); |
| 102: | } |
| 103: | } else { |
| 104: | $this->log("SCRAM authentication failed: Invalid server challenge"); |
| 105: | } |
| 106: | return false; |
| 107: | } |
| 108: | } |