From 92960f9fe5885c2b50df010a72c878baf48028f4 Mon Sep 17 00:00:00 2001 From: Danelif Date: Fri, 7 Jun 2024 10:37:30 +0200 Subject: [PATCH] [EHN] Adding SCRAM-256-PLUS authentication mechanism to cypht --- modules/imap/hm-imap.php | 48 +++++++++++++++ modules/smtp/hm-smtp.php | 85 +++++++++++++++++++++++++- tests/phpunit/imap_commands.php | 8 ++- tests/phpunit/modules/imap/hm-imap.php | 11 ++++ 4 files changed, 150 insertions(+), 2 deletions(-) diff --git a/modules/imap/hm-imap.php b/modules/imap/hm-imap.php index 9b4ffda3c0..b6e8f22edb 100644 --- a/modules/imap/hm-imap.php +++ b/modules/imap/hm-imap.php @@ -240,12 +240,60 @@ public function disconnect() { * @param string $password IMAP password * @return bool true on sucessful login */ + + //function to generate client proof in SCRAM-SHA-256-PLUS + private function generateClientProof($username, $password, $salt, $clientNonce, $serverNonce) { + $iterations = 4096; + $keyLength = 32; // In bytes + $passwordBytes = hash('sha256', $password, true); + $saltedPassword = hash_pbkdf2('sha256', $passwordBytes, $salt, $iterations, $keyLength, true); + $clientKey = hash_hmac('sha256', "Client Key", $saltedPassword, true); + $storedKey = hash('sha256', $clientKey, true); + $authMessage = 'n='.$username.',r='.$clientNonce.',s='.$serverNonce; + $clientSignature = hash_hmac('sha256', $authMessage, $storedKey, true); + $clientProof = base64_encode($clientKey ^ $clientSignature); + return $clientProof; + } public function authenticate($username, $password) { $this->get_capability(); if (!$this->tls) { $this->starttls(); } switch (strtolower($this->auth)) { + case 'scram-sha-256-plus': + // SCRAM-SHA-256-PLUS authentication + $this->banner = $this->fgets(1024); + $scram1 = 'AUTHENTICATE SCRAM-SHA-256-PLUS'."\r\n"; + $this->send_command($scram1); + $response = $this->get_response(); + if (!empty($response) && substr($response[0], 0, 2) == '+ ') { + // Extract salt and server nonce from the server's challenge + $serverChallenge = base64_decode(substr($response[0], 2)); + $parts = explode(',', $serverChallenge); + $serverNonce = base64_decode(substr($parts[0], strpos($parts[0], "=") + 1)); + $salt = base64_decode(substr($parts[1], strpos($parts[1], "=") + 1)); + + // Generate client nonce + $clientNonce = base64_encode(random_bytes(32)); + + // Calculate client proof + $clientProof = $this->generateClientProof($username, $password, $salt, $clientNonce, $serverNonce); + + // Construct client final message + $clientFinalMessage = 'c=biws,r='.$serverChallenge.',p='.$clientProof; + $clientFinalMessageEncoded = base64_encode($clientFinalMessage); + + // Send client final message to server + $this->send_command($clientFinalMessageEncoded."\r\n"); + + // Verify server's response + $response = $this->get_response(); + if (!empty($response) && $response[0] == 'OK') { + return true; // Authentication successful + } + } + return false; // Authentication failed + break; case 'cram-md5': $this->banner = $this->fgets(1024); diff --git a/modules/smtp/hm-smtp.php b/modules/smtp/hm-smtp.php index eb0ea73cc5..c2ea00b975 100644 --- a/modules/smtp/hm-smtp.php +++ b/modules/smtp/hm-smtp.php @@ -315,7 +315,19 @@ function choose_auth() { } return trim($this->supports_auth[0]); } - + //function to generate client proof in SCRAM-SHA-256-PLUS + private function generateClientProof($username, $password, $salt, $clientNonce, $serverNonce) { + $iterations = 4096; + $keyLength = 32; // In bytes + $passwordBytes = hash('sha256', $password, true); + $saltedPassword = hash_pbkdf2('sha256', $passwordBytes, $salt, $iterations, $keyLength, true); + $clientKey = hash_hmac('sha256', "Client Key", $saltedPassword, true); + $storedKey = hash('sha256', $clientKey, true); + $authMessage = 'n='.$username.',r='.$clientNonce.',s='.$serverNonce; + $clientSignature = hash_hmac('sha256', $authMessage, $storedKey, true); + $clientProof = base64_encode($clientKey ^ $clientSignature); + return $clientProof; +} /** * authenticate the username and password to the server */ @@ -331,6 +343,77 @@ function authenticate($username, $password, $mech) { $command = 'AUTH XOAUTH2 '.base64_encode($challenge); $this->send_command($command); break; + case 'scram-sha-256-plus': + // Get salt and server nonce from server response + $response = $this->get_response(); + if (empty($response) || !isset($response[0][1][0]) || $this->compare_response($response,'334') != 0) { + $result = 'FATAL: Server challenge not received'; + break; + } + $serverChallenge = base64_decode(trim($response[0][1][0])); + $serverChallengeParts = explode(',', $serverChallenge); + $salt = ''; + $serverNonce = ''; + foreach ($serverChallengeParts as $part) { + list($key, $value) = explode('=', $part, 2); + if ($key === 's') { + $salt = base64_decode($value); + } elseif ($key === 'r') { + $serverNonce = $value; + } + } + if (empty($salt) || empty($serverNonce)) { + $result = 'FATAL: Invalid server challenge'; + break; + } + // Generate client nonce + $clientNonce = base64_encode(random_bytes(32)); + // Generate client proof + $clientProof = $this->generateClientProof($username, $password, $salt, $clientNonce, $serverNonce); + // Construct client final message + $clientFinalMessage = 'c=biws,r=' . $serverNonce . ',p=' . $clientProof; + // Send client final message to the server + $command = 'AUTH SCRAM-SHA-256-PLUS ' . base64_encode($clientFinalMessage); + $this->send_command($command); + // Receive and process server final message + $response = $this->get_response(); + if (empty($response)) { + $result = 'Error: Empty response from server'; + } else { + $serverFinalMessage = base64_decode($response[0][1][0]); + $serverFinalMessageParts = explode(',', $serverFinalMessage); + $serverProof = null; + $serverNonce = null; + foreach ($serverFinalMessageParts as $part) { + list($key, $value) = explode('=', $part, 2); + if ($key === 'v') { + $serverProof = $value; + } elseif ($key === 'r') { + $serverNonce = $value; + } + } + if (!$serverProof || !$serverNonce) { + $result = 'Error: Invalid server final message format'; + } else { + // Generate server key + $saltedPassword = hash_pbkdf2('sha256', $password, $salt, 4096, 32, true); + $clientKey = hash_hmac('sha256', "Client Key", $saltedPassword, true); + $storedKey = hash('sha256', $clientKey, true); + // Construct client final message without proof + $clientFinalMessageWithoutProof = 'c=biws,r=' . $serverNonce; + // Calculate client signature + $clientSignature = hash_hmac('sha256', $clientFinalMessageWithoutProof, $storedKey, true); + // Generate client proof + $clientProof = base64_encode($clientKey ^ $clientSignature); + // Compare client proof with server proof + if ($clientProof === $serverProof) { + $result = 'Authentication successful'; + } else { + $result = 'Authentication failed: Server proof does not match'; + } + } + } + break; case 'cram-md5': $command = 'AUTH CRAM-MD5'; $this->send_command($command); diff --git a/tests/phpunit/imap_commands.php b/tests/phpunit/imap_commands.php index caf47adc41..c2f14b17de 100644 --- a/tests/phpunit/imap_commands.php +++ b/tests/phpunit/imap_commands.php @@ -2,7 +2,7 @@ return array( 'A1 CAPABILITY' => - "* CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE AUTH=PLAIN AUTH=CRAM-MD5\r\n", + "* CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE AUTH=PLAIN AUTH=CRAM-MD5 AUTH=SCRAM-SHA-256-PLUS\r\n", 'A3 CAPABILITY' => "* CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=". @@ -32,6 +32,12 @@ 'dGVzdHVzZXIgMGYxMzE5YmIxMzMxOWViOWU4ZDdkM2JiZDJiZDJlOTQ=' => "A2 OK authentication successful\r\n", + + 'A2 AUTHENTICATE SCRAM-SHA-256-PLUS' => + "+ r=fyko+d2lbbFgONRv9qkxdawL,z=SCRAM-SHA-256-PLUS,c=biws,n,,\r\n", + + 'c=biws,r=fyko+d2lbbFgONRv9qkxdawL1qkxdawL,p=YzQxNmVmY2ViMDAxYzdmNTkxOWFlZDIyNTgzM2NlZDBlZjBiNTNkZjUxNTQ1ZmZmNmY5NTlkOGZjNjYxYWEyNQ==' => + "A2 OK authentication successful\r\n", 'A2 AUTHENTICATE XOAUTH2 dXNlcj10ZXN0dXNlcgFhdXRoPUJlYXJlciB0ZXN0cGFzcwEB' => "+ V1WTI5dENnPT0BAQ==\r\n", diff --git a/tests/phpunit/modules/imap/hm-imap.php b/tests/phpunit/modules/imap/hm-imap.php index 9b3593d5d0..656e21cf56 100644 --- a/tests/phpunit/modules/imap/hm-imap.php +++ b/tests/phpunit/modules/imap/hm-imap.php @@ -93,6 +93,17 @@ public function test_authenticate_cram() { $this->connect(); $res = $this->debug(); $this->assertEquals('Logged in successfully as testuser', $res['debug'][2]); + } + /** + * @preserveGlobalState disabled + * @runInSeparateProcess + */ + public function test_authenticate_scram() { + $this->reset(); + $this->config['auth'] = 'scram-sha-256-plus'; + $this->connect(); + $res = $this->debug(); + $this->assertEquals('Logged in successfully as testuser', $res['debug'][2]); } /** * @preserveGlobalState disabled