Skip to content

Commit

Permalink
[EHN] Adding SCRAM-256-PLUS authentication mechanism to cypht
Browse files Browse the repository at this point in the history
  • Loading branch information
Danelif committed Jun 7, 2024
1 parent cea402c commit bc2c3a9
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 3 deletions.
72 changes: 71 additions & 1 deletion modules/imap/hm-imap.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,83 @@ 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':
// SCRAM-SHA-256-PLUS authentication
$scram1 = 'AUTHENTICATE SCRAM-SHA-256'."\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 'scram-sha-256-plus':
// SCRAM-SHA-256-PLUS authentication
$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
$channelBindingData = 'c=' . base64_encode('tls-unique');
$clientFinalMessage = $channelBindingData . ',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);
$cram1 = 'AUTHENTICATE CRAM-MD5'."\r\n";
Expand Down
156 changes: 155 additions & 1 deletion modules/smtp/hm-smtp.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -331,6 +343,148 @@ function authenticate($username, $password, $mech) {
$command = 'AUTH XOAUTH2 '.base64_encode($challenge);
$this->send_command($command);
break;
case 'scram-sha-256':
// 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 ' . 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 '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 using a biding channel
$clientFinalMessage = 'c='.base64_encode('tls-unique').',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);
Expand Down
13 changes: 12 additions & 1 deletion tests/phpunit/imap_commands.php
Original file line number Diff line number Diff line change
Expand Up @@ -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=".
Expand Down Expand Up @@ -32,7 +32,18 @@

'dGVzdHVzZXIgMGYxMzE5YmIxMzMxOWViOWU4ZDdkM2JiZDJiZDJlOTQ=' =>
"A2 OK authentication successful\r\n",

'A2 AUTHENTICATE SCRAM-SHA-256' =>
"+ r=fyko+d2lbbFgONRv9qkxdawL,z=SCRAM-SHA-256,c=biws,n,,\r\n",

'c=biws,r=fyko+d2lbbFgONRv9qkxdawL1qkxdawL,p=YzQxNmVmY2ViMDAxYzdmNTkxOWFlZDIyNTgzM2NlZDBlZjBiNTNkZjUxNTQ1ZmZmNmY5NTlkOGZjNjYxYWEyNQ==' =>
"A2 OK authentication successful\r\n",

'A2 AUTHENTICATE SCRAM-SHA-256-PLUS' =>
"+ r=fyko+d2lbbFgONRv9qkxdawL,z=SCRAM-SHA-256-PLUS,c=tls-unique,n,,\r\n",

'c=tls-unique,r=fyko+d2lbbFgONRv9qkxdawL1qkxdawL,p=YzQxNmVmY2ViMDAxYzdmNTkxOWFlZDIyNTgzM2NlZDBlZjBiNTNkZjUxNTQ1ZmZmNmY5NTlkOGZjNjYxYWEyNQ==' =>
"A2 OK authentication successful\r\n",
'A2 AUTHENTICATE XOAUTH2 dXNlcj10ZXN0dXNlcgFhdXRoPUJlYXJlciB0ZXN0cGFzcwEB' =>
"+ V1WTI5dENnPT0BAQ==\r\n",

Expand Down
22 changes: 22 additions & 0 deletions tests/phpunit/modules/imap/hm-imap.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,28 @@ 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_sha256_plus() {
$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
* @runInSeparateProcess
*/
public function test_authenticate_scram_sha_256() {
$this->reset();
$this->config['auth'] = 'scram-sha-256';
$this->connect();
$res = $this->debug();
$this->assertEquals('Logged in successfully as testuser', $res['debug'][2]);
}
/**
* @preserveGlobalState disabled
Expand Down

0 comments on commit bc2c3a9

Please sign in to comment.