Permalänk
Medlem

PHP inloggnings klass

Då jag ser många fortfarande använda mysql_* osäkra variabler, sparar lösenord i klartext etc tänkte jag visa klassen jag började bygga på idag. Försöker även lära mig själv PHP då jag mest programmerat i C# så tänkte att man kan lika gärna göra någonting vettigt av det hela samtidigt.

Just nu består klassen av

-> konstruktorn
--- Körs när du skapar ett objekt av klassen db. Just nu är det endast anslutningen till SQL servern som upprättas när objektet skapas.
-> register
--- Körs när du vill sätta in ny information om en användare. Just nu är det endast det mest simpla som sätts in: användarnamn, det hashade lösenordet och e-post adressen.
-> login
--- Körs när du vill autentisera en användare. Kollar först om användaren finns, finns den inte returnerar den false och skriver ut att inget användarnamn/lösenord hittats. Egentligen är det bara att användarnamnet inte hittades men ingen god idé att ge denna information till en potentiell hackare. Sedan hashar den lösenordet som användaren skrivit in och jämför med den som är sparad i databasen redan. Är båda samma returnerar den true och skriver ut att inloggningen lyckades.

cryptology.php är en klass som är public domain och är inte min egen. Den innehåller funktionerna för hashningen och saltandet av lösenorden. Mer information hittar ni på http://crackstation.net/hashing-security.htm

PDO är klassen som används för databashantering istället för mysql_* funktionerna. PDO är mycket säkrare samt fungerar till andra databaser än mysql.

OBS! Koden är väldigt grundläggande och saknar sessions, koll om användarnamnet är upptaget redan etc. Kommer försöka uppdatera den i mån jag har tid över på jobbet.

För att skapa SQL databasen skapar du en tabell som heter users och har fyra kolumner: userid, username, hash och email
Koden:

db.php

<?php include("cryptography.php"); class db { /* Databas variabler */ private $dbName, $dbUser, $dbPass, $dbHost, $connection; /* Variabler som hanterar användarkonton */ private $hash, $username, $password, $email, $validate; /* Meddelande hantering */ public $errors, $messages; /* När objektet skapas skapas också anslutningen till databasen */ public function __construct($dbHost, $dbName, $dbUser, $dbPass) { $this->dbHost = $dbHost; $this->dbUser = $dbUser; $this->dbPass = $dbPass; $this->dbName = $dbName; try { $this->connection = new PDO("mysql:host=$this->dbHost;dbname=$this->dbName", $this->dbUser, $this->dbPass); $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { $this->errors = "Kunde inte ansluta. Följande sträng är den tekniska informationen: " . $e->getMessage(); } } /* Registrering av t.ex. en användare */ public function register($username, $password, $email) { if (!$this->connection) { $this->errors = "Ingen anslutning till databasen kunde hittas. Försök igen senare."; return false; } $this->hash = create_hash($password); // Använd cryptography.php för att skapa sha256 hash + salt sträng $this->username = $username; $this->email = $email; /* Namngivna platshållare */ $data = array("username" => $this->username, "hash" => $this->hash, "email" => $this->email); $exec = $this->connection->prepare(" INSERT INTO `users` (username, hash, email) VALUE (:username, :hash, :email) "); try { $exec->execute($data); } catch(PDOException $e) { $this->errors = "Någonting gick fel. Följande sträng är den tekniska informationen: " . $e->getMessage(); return false; } $this->messages = "Registreringen lyckades!"; return $exec; } public function login($username, $password) { if (!$this->connection) { $this->errors = "Ingen anslutning till databasen kunde hittas. Försök igen senare."; return false; } $this->username = $username; $this->password = $password; $data = array("username" => $this->username); $exec = $this->connection->prepare(" SELECT username, hash FROM `users` WHERE username = :username "); try { $exec->execute($data); } catch(PDOException $e) { $this->errors = "Någonting gick fel. Följande sträng är en tekniska informationen: " . $e->getMessage(); } $checkRows = $exec->rowCount(); if (!$checkRows == 1) { $this->errors = "Fel användarnamn/lösenord"; return false; } while ($row = $exec->fetch(PDO::FETCH_ASSOC)) { $this->hash = $row['hash']; } $this->validate = validate_password($this->password, $this->hash); if ($this->validate) { $this->messages = "Inloggningen lyckades."; } else { $this->errors = "Inloggningen misslyckades. Fel användarnamn/lösenord"; } return $this->validate; } }

cryptology.php

<?php /* * Password hashing with PBKDF2. * Author: havoc AT defuse.ca * www: https://defuse.ca/php-pbkdf2.htm */ // These constants may be changed without breaking existing hashes. define("PBKDF2_HASH_ALGORITHM", "sha256"); define("PBKDF2_ITERATIONS", 1000); define("PBKDF2_SALT_BYTES", 24); define("PBKDF2_HASH_BYTES", 24); define("HASH_SECTIONS", 4); define("HASH_ALGORITHM_INDEX", 0); define("HASH_ITERATION_INDEX", 1); define("HASH_SALT_INDEX", 2); define("HASH_PBKDF2_INDEX", 3); function create_hash($password) { // format: algorithm:iterations:salt:hash $salt = base64_encode(mcrypt_create_iv(PBKDF2_SALT_BYTES, MCRYPT_DEV_URANDOM)); return PBKDF2_HASH_ALGORITHM . ":" . PBKDF2_ITERATIONS . ":" . $salt . ":" . base64_encode(pbkdf2( PBKDF2_HASH_ALGORITHM, $password, $salt, PBKDF2_ITERATIONS, PBKDF2_HASH_BYTES, true )); } function validate_password($password, $good_hash) { $params = explode(":", $good_hash); if(count($params) < HASH_SECTIONS) return false; $pbkdf2 = base64_decode($params[HASH_PBKDF2_INDEX]); return slow_equals( $pbkdf2, pbkdf2( $params[HASH_ALGORITHM_INDEX], $password, $params[HASH_SALT_INDEX], (int)$params[HASH_ITERATION_INDEX], strlen($pbkdf2), true ) ); } // Compares two strings $a and $b in length-constant time. function slow_equals($a, $b) { $diff = strlen($a) ^ strlen($b); for($i = 0; $i < strlen($a) && $i < strlen($b); $i++) { $diff |= ord($a[$i]) ^ ord($b[$i]); } return $diff === 0; } /* * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt * $algorithm - The hash algorithm to use. Recommended: SHA256 * $password - The password. * $salt - A salt that is unique to the password. * $count - Iteration count. Higher is better, but slower. Recommended: At least 1000. * $key_length - The length of the derived key in bytes. * $raw_output - If true, the key is returned in raw binary format. Hex encoded otherwise. * Returns: A $key_length-byte key derived from the password and salt. * * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt * * This implementation of PBKDF2 was originally created by https://defuse.ca * With improvements by http://www.variations-of-shadow.com */ function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false) { $algorithm = strtolower($algorithm); if(!in_array($algorithm, hash_algos(), true)) die('PBKDF2 ERROR: Invalid hash algorithm.'); if($count <= 0 || $key_length <= 0) die('PBKDF2 ERROR: Invalid parameters.'); $hash_length = strlen(hash($algorithm, "", true)); $block_count = ceil($key_length / $hash_length); $output = ""; for($i = 1; $i <= $block_count; $i++) { // $i encoded as 4 bytes, big endian. $last = $salt . pack("N", $i); // first iteration $last = $xorsum = hash_hmac($algorithm, $last, $password, true); // perform the other $count - 1 iterations for ($j = 1; $j < $count; $j++) { $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true)); } $output .= $xorsum; } if($raw_output) return substr($output, 0, $key_length); else return bin2hex(substr($output, 0, $key_length)); } ?>

Hur använder man koden?

Gjorde ett snabbt exempel:

test.php

<?php echo "<!DOCTYPE html><html><head></head><body>"; include("db.php"); $connect = new db("localhost", "c1db", "c1user", "password"); $register = $connect->register("admin", "123456", "email@domain.com"); if ($register) { echo $connect->messages; } else { echo $connect->errors; } echo "<br>"; $login = $connect->login("admin", "123456"); if ($login) { echo $connect->messages; } else { echo $connect->errors; } echo "</body></html>"; ?>

Skriv gärna om det är något ni undrar eller anmärkte på, koden är antagligen inte helt perfekt.

Visa signatur

AW3423DW QD-OLED - Ryzen 5800x - MSI Gaming Trio X 3090 - 64GB 3600@cl16 - Samsung 980 Pro 2TB/WD Black SN850 2TB

Permalänk
Inaktiv

Det är ju något sånt här man egentligen bör göra. Däremot vill jag påpeka en del saker:

"Då jag ser många fortfarande använda mysql_* osäkra variabler, sparar lösenord i klartext etc tänkte jag visa klassen jag började bygga på idag."

Till att börja med: Din klass använder ju fortfarande mysql_* metoderna underliggandes, vad blir det för skillnad(bortsett ifrån att en klass är mycket bättre att handskas med)? Hur gör du för att inte spara lösenord i klartext?

Det mest rekommenderade sättet är ju dock att abstrahera detta yttligare, med en "AppServer". Dvs du kör dessa klasser på en server, som pratar med databaser internt osv. Sen kör du själva "hemsidan" du vill utveckla på en annan server, som pratar med "appservern" via SSL (så att du i princip kör: http://appserver/authenticate med användarnamn och lösenord i post-vars, och sidan returnerar JSON/YAML-data med information om hur det gick, och om det gick skickar den även med en unik variabel som ändras vid varje request (för att förhindra maninthemiddleattacker). På detta sättet har du bara en PHP-klass på klientsidan som pratar med appservern, och appserver gör sin grej (via php eller annat) med databasen. Ingen viktig information körs då på klientsidan (alltså klienthemside-servern , inte webbläsaren) .

Permalänk
Inaktiv
Skrivet av anon214934:

Till att börja med: Din klass använder ju fortfarande mysql_* metoderna underliggandes, vad blir det för skillnad(bortsett ifrån att en klass är mycket bättre att handskas med)? Hur gör du för att inte spara lösenord i klartext?

Han använder ju dock PDO till skillnad från "mysql_*" metoderna.

Tycker du ska använda dig av till exempel BCrypt som är till för kryptering till skillnad från SHA-256 som är till för hashing.

Permalänk
Skrivet av anon214934:

Hur gör du för att inte spara lösenord i klartext?

Som jag förstår det krypterar han lösenordet med sha256+ salt för att sedan dekryptera det vid inloggning så att han kan jämföra dem.

Antagligen så är det för att kunna "hitta" lösenordet igen om det är borttappat, vet inte riktigt användningsområdet men i mitt tycke är det mycket enklare att bara envägskyptera skiten med sha1($lösen, $salt) sen jämföra inloggningen med den lagrade hashen i databasen.

Menar typ såhär:

public function register($username, $password, $email) { if (!$this->connection) { $this->errors = "Ingen anslutning till databasen kunde hittas. Försök igen senare."; return false; } $salt = "vadsomhelst"; // ÄNDRAT: vi behöver natriumklorid (salt är ett slumpmässigt värde som bara du vet) $this->hash = sha1($password.$salt); // ÄNDRAT: Använd inbyggda sha1 eller starkare om du är nojig $this->username = $username; $this->email = $email; /* Namngivna platshållare */ $data = array("username" => $this->username, "hash" => $this->hash, "email" => $this->email); $exec = $this->connection->prepare(" INSERT INTO `users` (username, hash, email) VALUE (:username, :hash, :email) "); try { $exec->execute($data); } catch(PDOException $e) { $this->errors = "Någonting gick fel. Följande sträng är den tekniska informationen: " . $e->getMessage(); return false; } $this->messages = "Registreringen lyckades!"; return $exec; } public function login($username, $password) { if (!$this->connection) { $this->errors = "Ingen anslutning till databasen kunde hittas. Försök igen senare."; return false; } $salt = "vadsomhelst"; // ÄNDRAT: vi behöver natriumklorid (salt ett slumpmässigt värde som bara du vet) $this->username = $username; $this->password = $password; $this->encryptedpass = sha1($password.$salt); //ÄNDRAT $data = array("username" => $this->username); $exec = $this->connection->prepare(" SELECT username, hash FROM `users` WHERE username = :username "); try { $exec->execute($data); } catch(PDOException $e) { $this->errors = "Någonting gick fel. Följande sträng är en tekniska informationen: " . $e->getMessage(); } $checkRows = $exec->rowCount(); if (!$checkRows == 1) { $this->errors = "Fel användarnamn/lösenord"; return false; } while ($row = $exec->fetch(PDO::FETCH_ASSOC)) { $this->hash = $row['hash']; } /* ÄNDRAT Kollar om det angivna lösenordet som angetts (och sen krypterats) stämmer med det registrerade lösenordet i hashform. */ if($this->hash == $this->encryptedpass) { $this->validate = 1; } else { $this->validate = 0; } if ($this->validate) { $this->messages = "Inloggningen lyckades."; } else { $this->errors = "Inloggningen misslyckades. Fel användarnamn/lösenord"; } return $this->validate; }

Permalänk
Medlem
Skrivet av anon214934:

Det är ju något sånt här man egentligen bör göra. Däremot vill jag påpeka en del saker:

"Då jag ser många fortfarande använda mysql_* osäkra variabler, sparar lösenord i klartext etc tänkte jag visa klassen jag började bygga på idag."

Till att börja med: Din klass använder ju fortfarande mysql_* metoderna underliggandes, vad blir det för skillnad(bortsett ifrån att en klass är mycket bättre att handskas med)? Hur gör du för att inte spara lösenord i klartext?

Det mest rekommenderade sättet är ju dock att abstrahera detta yttligare, med en "AppServer". Dvs du kör dessa klasser på en server, som pratar med databaser internt osv. Sen kör du själva "hemsidan" du vill utveckla på en annan server, som pratar med "appservern" via SSL (så att du i princip kör: http://appserver/authenticate med användarnamn och lösenord i post-vars, och sidan returnerar JSON/YAML-data med information om hur det gick, och om det gick skickar den även med en unik variabel som ändras vid varje request (för att förhindra maninthemiddleattacker). På detta sättet har du bara en PHP-klass på klientsidan som pratar med appservern, och appserver gör sin grej (via php eller annat) med databasen. Ingen viktig information körs då på klientsidan (alltså klienthemside-servern , inte webbläsaren) .

Använder PDO, menar du att PDO använder mysql_* funktionerna underliggandes? Spelar ju ändå ingen roll, då det är prepared statements, transactions och allt det andra man vill åt genom att använda PDO som inte går med mysql_* samt skyddar mot SQL injektioner och man slipper hålla på med mysql_real_escape_string överallt. För att inte spara i klartext används create_hash från cryptology.php som hashar och saltar lösenordet.

Det du pratar om låter dock intressant. Ge gärna en länk som går igenom det. I en MITM attack spelar det väl ändå ingen roll om det finns ett unikt token vid varje request?

Havsmonstret: Det är ju hashing/salting som utförs.

nordrassil: Lösenordet dekrypteras aldrig, hur skulle det gå till eftersom det krypteras one-way? Lösenordet som ges vid inloggning hashas och använder salten som finns sparad och sedan jämförs den inmatade hashen med det som finns sparat i databasen. Eller är jag helt ute och snurrar? Tack för inputen hur som! Vad jag läst dock så är md5/sha1 något man helst ska hålla sig borta ifrån.

Visa signatur

AW3423DW QD-OLED - Ryzen 5800x - MSI Gaming Trio X 3090 - 64GB 3600@cl16 - Samsung 980 Pro 2TB/WD Black SN850 2TB

Permalänk
Inaktiv
Skrivet av celoz:

Havsmonstret: Det är ju hashing/salting som utförs.

Ursäkta, fick för mig att du använde SHA-256 men såg nu att du använder PBKDF2. Mitt fel

EDIT: Du har visst skrivit en kommentar i din kod att du skapar en SHA-256 hash och sen kom det någon som använde sig av SHA-1 istället, men i själva verket använder du PBKDF2.