I'm starting a REST API, and have begun researching hashing and shared salts/secrets.
I have successfully been able to generate a sha256 hash, sent it to my server via json and matched it to the stored hash.
Thats great and all, but i'm thinking, now that ive done this with json, the actual hash generated is still visible to everyone that wants it. I was under the impression that these hashes changed everytime you re-hashed a string using hash_hmac.
So how do i make sure that a random user wont find that little json snippet, grap the hashed key and start making API calls?
I might have misunderstood the concept completely, so any bumps is greatly appreciated.
Heres my "Client" page:
<?php
$key= hash_hmac('sha256', '66c74620db28603fe4bec7b0f3a8e53b', 'gwerganaevawe21_3faseghbamoirvQWD');
?>
<script>
$.getJSON( "domain.com/api/publicCourseSession.php?key=<?php echo $key;?>", function( data ) {
$.each( data, function( key, val ) {
console.log(val);
});
});
</script>
And here is my publicCourseSession.php:
header("Content-Type: application/json");
header('Access-Control-Allow-Origin: *');
$apikey =$_GET['key'];
session_start();
function hash_compare($a, $b) {
if (!is_string($a) || !is_string($b)) {
return false;
}
$len = strlen($a);
if ($len !== strlen($b)) {
return false;
}
$status = 0;
for ($i = 0; $i < $len; $i++) {
$status |= ord($a[$i]) ^ ord($b[$i]);
}
return $status === 0;
}
$currentnetwork = getCurrentNetwork();
$currentkey = getNetworkApiKey(getNetworkId($currentnetwork));
$currentsecret = getNetworkApiSecret(getNetworkId($currentnetwork));
$currentkey= hash_hmac('sha256', $currentkey, $currentsecret);
if (hash_compare($apikey,$currentkey)) {
$status='correct';
} else {
$status='not correct';
}
$arr[] = ["key"=>$apikey, "currentkey"=>$currentkey, "correctkeys"=>$status];
echo json_encode($arr);
You did well, that's step 1.
Thing is, you can't do security on the browser only - you should be providing the user with the secret key once he successfully authenticates himself using his credentials (like username/password). Once he has the secret key then he can use it to authenticate his requests and he no longer needs to send his username/password back and forth across the interwebs..
Goal of the secret key is to simplify authenticated requests once user started a session. To start a session you need to authenticate yourself first though, otherwise you'll be providing anyone with the secret key which defeats the purpose.
Also, key should be generated per user and expire after x amount of time so it can't be used indefinitely.
Hope this helps!
Related
Hello again StackExchange users. Todays problem is a little more complex then usual for my. I am developing a web based hosting panel for a server that my company just set up and they would like to manage web pages from the internet. The rest of the control panel is working great but this one third-party app is really bugging me, and that app is phpMyAdmin!
I want the user to be able to log into the "cPanel" of their website (note: we are not using cPanel or any previous software) and just click the phpMyAdmin link and be logged in already to their own specific database. When they login, I am passing their sql database account details into a cookie using the same encryption type that they have for phpMyAdmin. Viewing the cookie shows that the cookie is formatted correctly and they should be logged onto the system
The problem is when they click on the link to go to the phpMyAdmin page, they still get the login form even though the cookie is already set and appears to be correct. There is no error message or even a please relog in message with the system.
I have included my code for my CookieAuth class below so you guys can look at it. It's really frustrating since the cookies seem to be the only way I can log them in properly and there's not many documented examples on how to do this anywhere else.
class CookieAuth {
private $_cookie_iv = null;
public function createPMAsession($username, $password, $blowfish_key) {
setCookie('pmaUser-1', $this->cookieEncrypt($username, $blowfish_key), time()+3600, "/phpmyadmin/");
setCookie('pmaAuth-1', $this->cookieEncrypt($password, $blowfish_key), null, "/phpmyadmin/");
}
private function _useOpenSSL() {
return (function_exists('openssl_encrypt') && function_exists('openssl_decrypt') && function_exists('openssl_random_pseudo_bytes') && PHP_VERSION_ID >= 50304);
}
public function enlargeKey($key) {
while(strlen($key) < 16) {
$key .= $key;
}
return substr($key, 0, 16);
}
public function getMAC($key) {
$length = strlen($key);
if($length > 16) {
return substr($key, 0, 16);
}
return $this->enlargeKey($length == 1 ? $key : substr($key, 0, -1));
}
public function getAES($key) {
$length = strlen($key);
if($length > 16) {
return substr($key, 0, 16);
}
return $this->enlargeKey($length == 1 ? $key : substr($key, 0, -1));
}
public function cookieEncrypt($data, $key) {
$mac = $this->getMAC($key);
$aes = $this->getAES($key);
$iv = $this->createIV();
if($this->_useOpenSSL()) {
$result = openssl_encrypt(
$data,
'AES-128-CBC',
$key,
0,
$iv
);
} else {
$cipher = new Crypt\AES(Crypt\Base::MODE_CBC);
$cipher->setIV($iv);
$cipher->setKey($aes);
$result = base64_encode($cipher->encrypt($data));
}
$iv = base64_encode($iv);
return json_encode(
array(
'iv' => $iv,
'mac' => hash_hmac('sha1', $iv . $result, $mac),
'payload' => $result,
)
);
}
public function getIVSize() {
if($this->_useOpenSSL()) {
return openssl_cipher_iv_length('AES-128-CBC');
}
$cipher = new Crypt\AES(Crypt\Base::MODE_CBC);
return $cipher->block_size;
}
public function createIV() {
if(!is_null($this->_cookie_iv)) {
return $this->_cookie_iv;
}
if($this->_useOpenSSL()) {
return openssl_random_pseudo_bytes($this->getIVSize());
} else {
return Crypt\Random::string($this->getIVSize());
}
}
public function setIV($vector) {
$this->_cookie_iv = $vector;
}
}
Thanks for taking the time to read my problem and I hope someone can point me in the right direction.
This sounds exactly like what the auth_type signon was designed to address. Is there a particular reason you're not using that?
Failing that, the documentation shows how you can pass the username and password as part of the query string, which may not be ideal from a security standpoint, but might give you a starting place to make your modifications.
Have you tried POSTing the proper username and password directly to index.php? I'm not certain whether this will work because of the security token, but it's worth a try.
I feel really dumb now. Using signon session you have to declare the session variable for phpMyAdmin in your config. I was apparently unaware of this and upon changing this session variable I was able to single sign on users.
If you want to use the Signon authentication mode using signon.php as documented in examples folder do not forget to change the auth_type in config.inc.php
Change
$cfg['Servers'][$i]['auth_type'] = 'cookie';
To
$cfg['Servers'][$i]['auth_type'] = 'signon';
Also add the following
$cfg['Servers'][$i]['SignonSession'] = 'SignonSession';
$cfg['Servers'][$i]['SignonURL'] = 'examples/signon.php';
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);
}
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).
I have a question about tokens. I understand that they are random characters used for security purposes but just how do they work and what do they protect against?
Authentification mechanism creates a token when form displayed, and was stored it on server side.
Also auth mechanism adds token as hidden input to form. When you send it, auth system check is it in server-side storage.
If token found, authentification process will continue and token was removing.
It protects from spamming form action script.
Example using with logout url:
<?php
// Generate token
$logout_token = md5(microtime().random(100, 999));
session_start();
// Store token in session
if (!is_array($_SESSION['logout_tokens']) {
$_SESSION['logout_tokens'] = array();
}
$_SESSION['logout_tokens'][] = $logout_token;
?>
logout
Script, that processing logout:
<?php
$done = false;
if (!empty($_GET['logout_token'])) {
// Get token from url
$logout_token = $_GET['logout_token'];
session_start();
if (!is_array($_SESSION['logout_tokens']) {
$_SESSION['logout_tokens'] = array();
}
// Search get token in session (server-side storage)
if (($key = array_search($logout_token, $_SESSION['logout_tokens'], true)) !== false) {
// Remove used token from storage
unset($_SESSION['logout_tokens'][$key]);
// Do logout
$done = true;
}
}
if ($done === false) {
echo "Something went wrong.";
}
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.