PHP Digest authentication with MD5 - php

I wrote a class to authenticate a user using HTTP Authentication the Digest way. I read a few articles and I got it working. Now, I would like to let it make use of Md5 passwords, but I can't seem to get it working, this is the function authenticating the users.
public function authenticate() {
// In case the user is not logged in already.
if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
// Return the headers.
$this->show_auth();
} else {
// Parse the given Digest-data.
$data = $this->parse_request($_SERVER['PHP_AUTH_DIGEST']);
// Check the data.
if (!$data) {
// Display an error message.
die($this->unauthorized);
} else {
// Based on the given information, generate the valid response.
$usr_password = "test";
// Generate the response partly.
$A1 = md5($data['username'].":".$this->get_realm().":".$usr_password);
$A2 = md5($_SERVER['REQUEST_METHOD'].":".$data['uri']);
// Generate the valid response.
$val_response = md5($A1.":".$data['nonce'].":".$data['nc'].":".$data['cnonce'].":".$data['qop'].":".$A2);
// Compare the valid response with the given response.
if ($data['response'] != $val_response) {
// Display the login again.
$this->show_auth();
} else {
// Return true.
return true;
}
}
}
}
So imagine the $usr_password="test" will be $usr_password=md5("test");
How do I compare passwords then?
Thanks.

The MD5 function is hashing function, one-directional method to produce the same result for the same input.
Thus, to compare $password1 to $password2 without revealing (comparing directly) both of them it should be enough to compare their hashes:
$hash1 = md5($password1); // hash for pass 1
$hash2 = md5($password2); // hash for pass 2
if ($hash1 === $hash2) {
// here goes the code to support case of passwords being identical
} else {
// here goes the code to support case of passwords not being identical
}
Is it clear enough? Let me know.

Related

Check hash validity

I'm trying to validate password against invalid hash stored in database. Instead of getting false (as I supposed) in this situation, my application dies with Invalid hash exception.
Is there any Yii2-built-in way to validate hash, before feeding it to validatePassword, to handle this kind of situation more gently?
Or the only way I'm left with is to copy code used by validatePassword:
if (!preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) {
throw new InvalidParamException('Hash is invalid.');
}
to my own code and simply not call validatePassword, when hash is invalid?
You can always use try - catch block in your password validation:
/**
* Validates password
*
* #param string $password password to validate
* #return boolean if password provided is valid for current user
*/
public function validatePassword($password)
{
try {
$data = Yii::$app->getSecurity()->validatePassword($password, $this->password_hash);
return $data;
} catch(\yii\base\InvalidParamException $e) {
return false;
}
}
Something like that, but why you even want to try to validate hash if it's already malformed? Maybe some criminal will pass something bad there, which can pass your login etc.

PHP : Understand and implement a token-based authentication system

I have read a lot about it but i still don't completely get it.
I may use a library of an existing solution in the future but i want to understand and implement my own system right now.
In order to be stateless and scalable I think i mustn't store user context on server.
The main problem is a conception one, if i understand the system i will succeed to code it
I have tested code found on Internet which i have modified (french website ref : http://blog.nalis.fr/index.php?post/2009/09/28/Securisation-stateless-PHP-avec-un-jeton-de-session-(token)-protection-CSRF-en-PHP).
Can you tell me if it's correct or if i don't get it?
So to create a token i use this function which takes as parameters, the user's data
define('SECRET_KEY', "fakesecretkey");
function createToken($data)
{
/* Create a part of token using secretKey and other stuff */
$tokenGeneric = SECRET_KEY.$_SERVER["SERVER_NAME"]; // It can be 'stronger' of course
/* Encoding token */
$token = hash('sha256', $tokenGeneric.$data);
return array('token' => $token, 'userData' => $data);
}
So a user can authentified himself and receive an array which contains a token (genericPart + his data, encoded), and hisData not encoded :
function auth($login, $password)
{
// we check user. For instance, it's ok, and we get his ID and his role.
$userID = 1;
$userRole = "admin";
// Concatenating data with TIME
$data = time()."_".$userID."-".$userRole;
$token = createToken($data);
echo json_encode($token);
}
Then the user can send me his token + his un-encoded data in order to check :
define('VALIDITY_TIME', 3600);
function checkToken($receivedToken, $receivedData)
{
/* Recreate the generic part of token using secretKey and other stuff */
$tokenGeneric = SECRET_KEY.$_SERVER["SERVER_NAME"];
// We create a token which should match
$token = hash('sha256', $tokenGeneric.$receivedData);
// We check if token is ok !
if ($receivedToken != $token)
{
echo 'wrong Token !';
return false;
}
list($tokenDate, $userData) = explode("_", $receivedData);
// here we compare tokenDate with current time using VALIDITY_TIME to check if the token is expired
// if token expired we return false
// otherwise it's ok and we return a new token
return createToken(time()."#".$userData);
}
$check = checkToken($_GET['token'], $_GET['data']);
if ($check !== false)
echo json_encode(array("secureData" => "Oo")); // And we add the new token for the next request
Am I right?
Sorry for this long message and sorry for my english.
Thanks in advance for your help!
The problem in your code is: You are basing your entire system on $_GET in the original post is based on Cookies.. You should store the token in cookies (based on your original post, instead of using $_GET
By the way; a few tweaks:
list($tokenDate, $userData) = array_pad(explode("_", $receivedData));
In the next code I don't see how you use $login,$password
function auth($login, $password)
{
// we check user. For instance, it's ok, and we get his ID and his role.
$userID = 1;
$userRole = "admin";
// Concatenating data with TIME
$data = time()."_".$userID."-".$userRole;
$token = createToken($data);
echo json_encode($token);
}

Laravel 4 Auth::attempt returns false on correct values

I have login code that is supposed to work by attempting to authenticate the user using Laravel's Auth::attempt() method. This code works on another site of mine, I have altered it as instead of the Password in the database, it is stored as passwdEncrypted. I cannot change it as the database is in use by another application as well.
The code is below:
// check if in database
$isInDb = User::where('ReferenceCode', $username)->first();
if($isInDb) {
// is in database
// check if password is encrypted yet
if($isInDb->passwdEncrypted) {
// password is encrypted
if(Auth::attempt(array('ReferenceCode' => $username, 'passwdEncrypted' => $password))) {
// authenticated
$array = array();
$array['verified'] = 'true';
$array['type'] = Auth::user()->UserType;
return Response::json($array);
} else {
// not authenticated
$array = array();
$array['verified'] = 'false';
$array['type'] = $type;
return Response::json($array);
}
} else {
// password is not encrypted
// check if plain text password is correct
if($isInDb->Password == $password) {
// plain text password is correct
$hashed = Hash::make($password);
$arr = array('passwdEncrypted' => $hashed);
$updated = User::where('rsmsOnlineUserID', $isInDb->rsmsOnlineUserID)->update($arr);
if($updated) {
$newUser = User::find($isInDb->rsmsOnlineUserID);
echo $newUser->passwdEncrypted;
if(Auth::attempt(array('ReferenceCode' => $username, 'passwdEncrypted' => $password))) {
echo 'logged in';
} else {
dd(DB::getQueryLog());
echo 'could not log in';
}
} else {
echo 'did not update';
}
} else {
// plain text password is incorrect
$array = array();
$array['verified'] = 'false';
$array['type'] = $type;
return Response::json($array);
}
}
} else {
// not in database
return Respone::json(array('success' => 'false'));
}
What is happening: I can't log in, the username and password in the database is 1234, even if I hard code that, it does not work.
It first checks to see if the user is in the database, if it is, it checks to see if there is an encrypted password, if there is not, it will create one from the password given if it matches the plain text password in the database and then log the user in (I have no choice but to have the plain text password stored in the database, that is how they want it).
But it returns the {"verified":"false","type":"prospective_employee"} from the not authenticated part of the code. So neither of the Auth::attempt() blocks work.
I was logging them in manually but even Auth::login() won't work.
I have the following in my User model (with the main database table):
public function getAuthPassword()
{
return $this->Password;
}
/**
* Get the token value for the "remember me" session.
*
* #return string
*/
public function getRememberToken() {
return $this->remember_token;
}
public function setRememberToken($value) {
$this->remember_token = $value;
}
public function getRememberTokenName() {
return 'remember_token';
}
/**
* Get the e-mail address where password reminders are sent.
*
* #return string
*/
public function getReminderEmail()
{
return $this->email;
}
Please note that there is a field in the table called Password, but that is the plain text password, I need to authenticate against the passwdEncrypted field.
You cannot do this with Laravel, and for good reason, but it is ridiciously unsecure and dangerous.
I have no choice but to have the plain text password stored in the
database, that is how they want it
I dont understand - why are you storing BOTH an "unencrypted" and "encrypted" password? There is no point. You should only ever store encrypted passwords. There is no need for any other way, and you need to educate the people as to why.
This code works on another site of mine, I have altered it as instead
of the Password in the database, it is stored as passwdEncrypted. I
cannot change it as the database is in use by another application as
well.
The Laravel Auth code is hard coded to use the "password" column. You cannot simply change it to another colum. That is why your code is failing.
Since you are not using the password column, and since you are not using encrypted passwords, you might as well just create your own unsecure login system, customised to suit your requirements.

SSL Over Javascript

I've seen a few similar questions that don't quite seem to address my exact use case, and I THINK I've figured out the answer, but I'm a total noob when it comes to security, RSA, and pretty much everything associated with it. I have a basic familiarity with the concepts, but all of the actual implementations I've done up to this point were all about editing someone else's code rather than generating my own. Anyway, here's where I am:
I know that Javascript is an inherently bad place to do encryption. Someone could Man-in-the-Middle your response and mangle the JS so you'll end up sending unencrypted data over the wire. It SHOULD be done via an HTTPS SSL/TLS connection, but that kind of hosting costs money and so do the official signed certificates that should realistically go with the connection.
That being said, I think the way I'm going to do this circumvents the Man-in-the-Middle weakness of JS encryption by virtue of the fact that I'm only ever encrypting one thing (a password hash) for one RESTful service call and then only using that password hash to sign requests from the client in order to authenticate them as coming from the user the requests claim. This means the JS is only responsible for encrypting a password hash once at user account creation and if the server cannot decode that cipher then it knows it's been had.
I'm also going to save some client information, in particular the $_SERVER['REMOTE_ADDR'] to guarantee that someone doesn't M-i-t-M the registration exchange itself.
I'm using PHP's openssl_pkey_ functions to generate an asymmetric key, and the Cryptico library on the client side. My plan is for the user to send a "pre-registration" request to the REST service, which will cause the server to generate a key, store the private key and the client information in a database indexed by the email address, and then respond with the public key.
The client would then encrypt the user's password hash using the public key and send it to the REST service as another request type to complete the registration. The server would decrypt and save the password hash, invalidate the client information and the private key so no further registrations could be conducted using that information, and then respond with a 200 status code.
To login, a user would type in their email address and password, the password would be hashed as during registration, appended to the a request body, and hashed again to sign a request to a login endpoint which would try to append the stored hash to the request body and hash it to validate the signature against the one in the request and so authenticate the user. Further data requests to the service would follow the same authentication process.
Am I missing any glaring holes? Is is possible to spoof the $_SERVER['REMOTE_ADDR'] value to something specific? I don't need the IP address to be accurate or the same as when the user logs in, I just need to know that the same machine that 'pre-registered' and got a public key followed up and completed the registration instead of a hijacker completing the registration for them using a snooped public key. Of course, I guess if they can do that, they've hijacked the account beyond recovery at creation and the legitimate user wouldn't be able to complete the registration with their own password, which is ok too.
Bottom line, can someone still hack my service unless I fork out for a real SSL host? Did I skirt around Javascript's weaknesses as an encryption tool?
As I write and debug my code, I'll post it here if anyone wants to use it. Please let me know if I'm leaving my site open to any kind of attacks.
These are the functions that validate client requests against the hash in the headers, generate the private key, save it to the database, respond with the public key, and decrypt and check the password hash.
public function validate($requestBody = '',$signature = '',$url = '',$timestamp = '') {
if (is_array($requestBody)) {
if (empty($requestBody['signature'])) { return false; }
if (empty($requestBody['timestamp'])) { return false; }
if ($requestBody['requestBody'] === null) { return false; }
$signature = $requestBody['signature'];
$timestamp = $requestBody['timestamp'];
$requestBody = $requestBody['requestBody'];
}
if (($requestBody === null) || empty($signature) || empty($timestamp)) { return false; }
$user = $this->get();
if (count($user) !== 1 || empty($user)) { return false; }
$user = $user[0];
if ($signature !== md5("{$user['pwHash']}:{$this->primaryKey}:$requestBody:$url:$timestamp")) { return false; }
User::$isAuthenticated = $this->primaryKey;
return $requestBody;
}
public function register($emailAddress = '',$cipher = '') {
if (is_array($emailAddress)) {
if (empty($emailAddress['cipher'])) { return false; }
if (empty($emailAddress['email'])) { return false; }
$cipher = $emailAddress['cipher'];
$emailAddress = $emailAddress['email'];
}
if (empty($emailAddress) || empty($cipher)) { return false; }
$this->primaryKey = $emailAddress;
$user = $this->get();
if (count($user) !== 1 || empty($user)) { return false; }
$user = $user[0];
if (!openssl_private_decrypt(base64_decode($cipher),$user['pwHash'],$user['privateKey'])) { return false; }
if (md5($user['pwHash'].":/api/preRegister") !== $user['session']) { return false; }
$user['session'] = 0;
if ($this->put($user) !== 1) { return false; }
$this->primaryKey = $emailAddress;
User::$isAuthenticated = $this->primaryKey;
return $this->getProfile();
}
public function preRegister($emailAddress = '',$signature = '') {
if (is_array($emailAddress)) {
if (empty($emailAddress['signature'])) { return false; }
if (empty($emailAddress['email'])) { return false; }
$signature = $emailAddress['signature'];
$emailAddress = $emailAddress['email'];
}
if (empty($emailAddress) || empty($signature)) { return false; }
$this->primaryKey = $emailAddress;
$response = $this->makeUserKey($signature);
if (empty($response)) { return false; }
$response['emailAddress'] = $emailAddress;
return $response;
}
private function makeUserKey($signature = '') {
if (empty($signature)) { return false; }
$config = array();
$config['digest_alg'] = 'sha256';
$config['private_key_bits'] = 1024;
$config['private_key_type'] = OPENSSL_KEYTYPE_RSA;
$key = openssl_pkey_new($config);
if (!openssl_pkey_export($key,$privateKey)) { return false; }
if (!$keyDetails = openssl_pkey_get_details($key)) { return false; }
$keyData = array();
$keyData['publicKey'] = $keyDetails['key'];
$keyData['privateKey'] = $privateKey;
$keyData['session'] = $signature;
if (!$this->post($keyData)) { return false; }
$publicKey = openssl_get_publickey($keyData['publicKey']);
$publicKeyHash = md5($keyData['publicKey']);
if (!openssl_sign($publicKeyHash,$signedKey,$privateKey)) { return false; }
if (openssl_verify($publicKeyHash,$signedKey,$publicKey) !== 1) { return false; }
$keyData['signedKey'] = base64_encode($signedKey);
$keyData['rsa'] = base64_encode($keyDetails['rsa']['n']).'|'.bin2hex($keyDetails['rsa']['e']);
unset($keyData['privateKey']);
unset($keyData['session']);
return $keyData;
}
What you are trying to do is to replace the need for SSL certificates signed by a Certificate Authority with custom JavaScript. I'm not a security expert, but as far as I know the simple answer is that this is not possible.
The basic fact is that on the public internet, the server can't trust what a client says, and a client can't trust what the server says, exactly because of man in the middle attacks. The reason why certificate authorities are necessary to begin with is to establish some kind of impartial trust base. CA's are carefully vetted by the browser vendors, and it's the only trust currently available on the public internet, although it's certainly not perfect.
I am curious to know why a relatively inexpensive SSL certificate (like the 1-year from Digicert at $175 USD) is out of the question. Especially if this is for a business, $175/yr is a reasonable expense (it works out to about $12.60 USD/month).

Converting from MD5 Legacy Auth System to CakePHP

I have a site which runs off an MD5 hashing scheme for passwords. As a way of supporting this legacy system, I've this answer to manually override the login system for now. But this isn't really ideal, as MD5 is pretty much universally known to be awful at encryption. So in the interest of security, what's the best way to migrate users over to the safer CakePHP auth system without causing them undue grief?
Figured it out thanks to this answer (albeit lightly modified). Basically, it updates the user behind the scenes to use the new system if the current system doesn't match up with it.
/**
* Login method
*/
public function login() {
$this->layout = 'homepage';
// If the user is already logged in, redirect to their user page
if($this->Auth->user() != null) {
$this->redirect();
} else {
// If this is being POSTed, check for login information
if($this->request->is('post')) {
if($this->Auth->login($this->loginHelper($this->request->data))) {
// Redirect to origin path, ideally
} else {
$this->Session->setFlash('Invalid username or password, try again');
}
}
}
}
/**
* Update password method
* #param array The user's data array
* #param Returns either a user object if the user is valid or null otherwise
*/
private function loginHelper($data) {
$username = $this->data['User']['username'];
$plainText = $this->data['User']['password'];
$user = current($this->User->findByUsername($username));
$salted = Security::hash($plainText, null, true);
if ($salted === $user['password']) {
return $user; // user exists, password is correct
}
$md5ed = Security::hash($plainText, 'md5', null);
if ($md5ed === $user['password']) {
$this->User->id = $user['id'];
$this->User->saveField('password', $plainText);
return $user; // user exists, password now updated to blowfish
}
return null; // user's password does not exist.
}

Categories