Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] Adding SCRAM-SHA authentication mechanisms to cypht #1072

Merged
merged 1 commit into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 37 additions & 21 deletions modules/imap/hm-imap.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,12 @@ class Hm_IMAP extends Hm_IMAP_Cache {

/* current selected mailbox status */
public $folder_state = false;

private $scramAuthenticator;
/**
* constructor
*/
public function __construct() {
$this->scramAuthenticator = new ScramAuthenticator();
}

/* ------------------ CONNECT/AUTH ------------------------------------- */
Expand Down Expand Up @@ -233,40 +234,57 @@ public function disconnect() {
fclose($this->handle);
}
}

/**
* authenticate the username/password
* Authenticate the username/password
* @param string $username IMAP login name
* @param string $password IMAP password
* @return bool true on sucessful login
* @return bool true on successful login
*/
public function authenticate($username, $password) {
$this->get_capability();
if (!$this->tls) {
$this->starttls();
}
$scramMechanisms = [
'scram-sha-1', 'scram-sha-1-plus',
'scram-sha-256', 'scram-sha-256-plus',
'scram-sha-224', 'scram-sha-224-plus',
'scram-sha-384', 'scram-sha-384-plus',
'scram-sha-512', 'scram-sha-512-plus'
];
if (in_array(strtolower($this->auth), $scramMechanisms)) {
$scramAlgorithm = strtoupper($this->auth);
if ($this->scramAuthenticator->authenticateScram(
$scramAlgorithm,
$username,
$password,
[$this, 'get_response'],
[$this, 'send_command']
)) {
return true; // Authentication successful
}
}
switch (strtolower($this->auth)) {

case 'cram-md5':
$this->banner = $this->fgets(1024);
$cram1 = 'AUTHENTICATE CRAM-MD5'."\r\n";
$cram1 = 'AUTHENTICATE CRAM-MD5' . "\r\n";
$this->send_command($cram1);
$response = $this->get_response();
$challenge = base64_decode(substr(trim($response[0]), 1));
$pass = str_repeat(chr(0x00), (64-strlen($password)));
$pass = str_repeat(chr(0x00), (64 - strlen($password)));
$ipad = str_repeat(chr(0x36), 64);
$opad = str_repeat(chr(0x5c), 64);
$digest = bin2hex(pack("H*", md5(($pass ^ $opad).pack("H*", md5(($pass ^ $ipad).$challenge)))));
$challenge_response = base64_encode($username.' '.$digest);
fputs($this->handle, $challenge_response."\r\n");
$digest = bin2hex(pack("H*", md5(($pass ^ $opad) . pack("H*", md5(($pass ^ $ipad) . $challenge)))));
$challenge_response = base64_encode($username . ' ' . $digest);
fputs($this->handle, $challenge_response . "\r\n");
break;
case 'xoauth2':
$challenge = 'user='.$username.chr(1).'auth=Bearer '.$password.chr(1).chr(1);
$command = 'AUTHENTICATE XOAUTH2 '.base64_encode($challenge)."\r\n";
$challenge = 'user=' . $username . chr(1) . 'auth=Bearer ' . $password . chr(1) . chr(1);
$command = 'AUTHENTICATE XOAUTH2 ' . base64_encode($challenge) . "\r\n";
$this->send_command($command);
break;
default:
$login = 'LOGIN "'.str_replace(array('\\', '"'), array('\\\\', '\"'), $username).'" "'.str_replace(array('\\', '"'), array('\\\\', '\"'), $password). "\"\r\n";
$login = 'LOGIN "' . str_replace(array('\\', '"'), array('\\\\', '\"'), $username) . '" "' . str_replace(array('\\', '"'), array('\\\\', '\"'), $password) . "\"\r\n";
$this->send_command($login);
break;
}
Expand All @@ -282,26 +300,24 @@ public function authenticate($username, $password) {
$this->banner = $res[0];
}
}
if (stristr($response, 'A'.$this->command_count.' OK')) {
if (stristr($response, 'A' . $this->command_count . ' OK')) {
$authed = true;
$this->state = 'authenticated';
}
elseif (strtolower($this->auth) == 'xoauth2' && preg_match("/^\+ ([a-zA-Z0-9=]+)$/", $response, $matches)) {
} elseif (strtolower($this->auth) == 'xoauth2' && preg_match("/^\+ ([a-zA-Z0-9=]+)$/", $response, $matches)) {
$this->send_command("\r\n", true);
$this->get_response();
}
}
if ($authed) {
$this->debug[] = 'Logged in successfully as '.$username;
$this->debug[] = 'Logged in successfully as ' . $username;
$this->get_capability();
$this->enable();
//$this->enable_compression();
}
else {
$this->debug[] = 'Log in for '.$username.' FAILED';
} else {
$this->debug[] = 'Log in for ' . $username . ' FAILED';
}
return $authed;
}


/**
* attempt starttls
Expand Down
105 changes: 105 additions & 0 deletions modules/scram/scram.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

class ScramAuthenticator {

private $hashes = array(
'sha-1' => 'sha1',
'sha1' => 'sha1',
'sha-224' => 'sha224',
'sha224' => 'sha224',
'sha-256' => 'sha256',
'sha256' => 'sha256',
'sha-384' => 'sha384',
'sha384' => 'sha384',
'sha-512' => 'sha512',
'sha512' => 'sha512'
);

private function getHashAlgorithm($scramAlgorithm) {
$parts = explode('-', strtolower($scramAlgorithm));
return $this->hashes[$parts[1]] ?? 'sha1'; // Default to sha1 if the algorithm is not found
}
private function log($message) {
// Use Hm_Debug to add the debug message
Hm_Debug::add(sprintf($message));
}
public function generateClientProof($username, $password, $salt, $clientNonce, $serverNonce, $algorithm) {
$iterations = 4096;
$keyLength = strlen(hash($algorithm, '', true)); // Dynamically determine key length based on algorithm

$passwordBytes = hash($algorithm, $password, true);
$saltedPassword = hash_pbkdf2($algorithm, $passwordBytes, $salt, $iterations, $keyLength, true);
$clientKey = hash_hmac($algorithm, "Client Key", $saltedPassword, true);
$storedKey = hash($algorithm, $clientKey, true);
$authMessage = 'n=' . $username . ',r=' . $clientNonce . ',s=' . base64_encode($salt) . ',r=' . $serverNonce;
$clientSignature = hash_hmac($algorithm, $authMessage, $storedKey, true);
$clientProof = base64_encode($clientKey ^ $clientSignature);
$this->log("Client proof generated successfully");
return $clientProof;
}

public function authenticateScram($scramAlgorithm, $username, $password, $getServerResponse, $sendCommand) {
$algorithm = $this->getHashAlgorithm($scramAlgorithm);

// Send initial SCRAM command
$scramCommand = 'AUTHENTICATE ' . $scramAlgorithm . "\r\n";
$sendCommand($scramCommand);
$response = $getServerResponse();
if (!empty($response) && substr($response[0], 0, 2) == '+ ') {
$this->log("Received server challenge: " . $response[0]);
// 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));
$this->log("Generated client nonce: " . $clientNonce);

// Calculate client proof
$clientProof = $this->generateClientProof($username, $password, $salt, $clientNonce, $serverNonce, $algorithm);

// Construct client final message
$channelBindingData = (stripos($scramAlgorithm, 'plus') !== false) ? 'c=' . base64_encode('tls-unique') . ',' : 'c=biws,';
$clientFinalMessage = $channelBindingData . 'r=' . $serverNonce . $clientNonce . ',p=' . $clientProof;
$clientFinalMessageEncoded = base64_encode($clientFinalMessage);
$this->log("Sending client final message: " . $clientFinalMessageEncoded);
// Send client final message to server
$sendCommand($clientFinalMessageEncoded . "\r\n");

// Verify server's response
$response = $getServerResponse();
if (!empty($response) && substr($response[0], 0, 2) == '+ ') {
$serverFinalMessage = base64_decode(substr($response[0], 2));
$parts = explode(',', $serverFinalMessage);
$serverProof = substr($parts[0], strpos($parts[0], "=") + 1);

// Generate server key
$passwordBytes = hash($algorithm, $password, true);
$saltedPassword = hash_pbkdf2($algorithm, $passwordBytes, $salt, 4096, strlen(hash($algorithm, '', true)), true);
$serverKey = hash_hmac($algorithm, "Server Key", $saltedPassword, true);

// Calculate server signature
$authMessage = 'n=' . $username . ',r=' . $clientNonce . ',s=' . base64_encode($salt) . ',r=' . $serverNonce;
$serverSignature = base64_encode(hash_hmac($algorithm, $authMessage, $serverKey, true));

// Compare server signature with server proof
if ($serverSignature === $serverProof) {
$this->log("SCRAM authentication successful");
return true; // Authentication successful if they match
} else {
$this->log("SCRAM authentication failed: Server signature mismatch");
}
} else {
$this->log("SCRAM authentication failed: Invalid server final response");
}
} else {
$this->log("SCRAM authentication failed: Invalid server challenge");
}
return false; // Authentication failed
}
}


?>
Loading