Skip to content

Commit

Permalink
Merge pull request #1072 from cypht-org/enh-scram
Browse files Browse the repository at this point in the history
[ENH] Adding SCRAM-SHA authentication mechanisms to cypht
  • Loading branch information
kroky authored Jun 13, 2024
2 parents f77473b + c54497f commit cdeee04
Show file tree
Hide file tree
Showing 5 changed files with 359 additions and 89 deletions.
58 changes: 37 additions & 21 deletions modules/imap/hm-imap.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,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 @@ -235,40 +236,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 @@ -284,26 +302,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

0 comments on commit cdeee04

Please sign in to comment.