crypt() is erg traag

Overzicht Reageren

Sponsored by: Vacatures door Monsterboard

Karin Gijssen

Karin Gijssen

04/08/2014 22:13:33
Quote Anchor link
Ik ben een site aan het maken met geregistreerde gebruikers. Wachtwoordhashing had ik met sha1 gedaan, maar ik heb begrepen dat dat niet veilig genoeg is. Nu heb ik crypt() gebruikt met een random gegenereerde salt. Werkt prima, alleen....heel traag. Registreren, inloggen en wachtwoord veranderen duurt ruim 10 seconden. Ik lees dat crypt_blowfish slow hashing is, maar hoort het echt zo traag te zijn?

Hier mijn inlogscriptje:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<? php
// controle inloggegevens
    $sql =  "SELECT klantID, wachtwoord, voornaam, salt " .
            "FROM tblKlant " .
            "WHERE email = '" . mysql_real_escape_string($_POST["txtEmail"]) . "'";
    $resultaat = mysql_query($sql);
    if(mysql_num_rows($resultaat) > 0)
    {

        // e-mailadres gevonden. Wachtwoord controleren.
        $rs = mysql_fetch_array($resultaat);
        if($rs["wachtwoord"] == crypt(mysql_real_escape_string($_POST["txtPassword"]), $rs["salt"])) // wachtwoord juist
        {
            // session setten en redirect
            
        }
        else
        {
            // wachtwoord onjuist
            $strErrInlog = "<span class=\"warning\">De combinatie van gebruikersnaam<br />en wachtwoord is niet juist.</span><br /><br />";
        }

?>
 
PHP hulp

PHP hulp

24/04/2024 08:18:34
 
Dos Moonen

Dos Moonen

04/08/2014 22:25:56
Quote Anchor link
1) wie heeft dit geschreven?
2) waarom heeft <antwoord op vraag 1> bedacht dat het op regel 11 nodig is om $_POST["txtPassword"] door mysql_real_escape_string() te halen terwijl het nooit in een query gebruikt zal worden?
3) de tweede parameter voor crypt() moet $rs['wachtwoord'] zijn
4) weet <antwoord op vraag 1> van het bestaan van password_hash() en password_verify() af? (php implementatie voor PHP versies 5.3.7 tot 5.5 bestaat, die de password_hash() documentatie en zoek naar "userland")

De tweede parameter van crypt() bepaald het algoritme. Aangezien je geen voorbeeld hash (crypt() slaat salt in de hash op) gegeven hebt zou ik niet weten of er ook echt crypt_blowfish gebruikt wordt.
 
Karin Gijssen

Karin Gijssen

04/08/2014 22:40:09
Quote Anchor link
@Dos
A1)ik
A2)ok....dus dat is hier niet nodig (ik ben beginnend PHPer)
A3)Ik wil weten of het wachtwoord uit de database, $rs['wachtwoord'], overeenkomt met het ingevoerde wachtwoord $_POST["txtPassword"].
A4)Ja, wel van gehoord, maar kreeg ik niet aan de praat. Is dat veel sneller dan?

Dit is een voorbeeldhash: $2a$17$NsQYncP5tPv/Q1zBQeqjRuR3r6oZeefzVeFZ4B3gWie5TeaNjuYuy
Het eerste stuk is de gegenereerde salt.
 
Dos Moonen

Dos Moonen

04/08/2014 23:09:59
Quote Anchor link
"A3)Ik wil weten of het wachtwoord uit de database, $rs['wachtwoord'], overeenkomt met het ingevoerde wachtwoord $_POST["txtPassword"]."
Dat snapte ik al, dus ik weet niet wat je probeert te vertellen, of hoe dat een reactie is op mijn opmerking.

"A4)Ja, wel van gehoord, maar kreeg ik niet aan de praat. Is dat veel sneller dan?"
Nee, achter de schermen wordt de zelfde crypt_blowfish gebruikt, maar het gebruik er van is duidelijker.

uitleg voorbeeldhash:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
$ // scheidingsteken
2a // blowfish, vanaf PHP 5.3.7 anders dan ervoor omdat er een security issue is gefixt, 2x is de oude, 2y is de nieuwe.
$ // scheidingsteken
17 // 2^17 = 131072 rondes, 12-14 is een betere waarde (speel wat met dit getal en de microtime() functie zodat het hashen 100-300 miliseconde duurt)
$ // scheidingsteken
NsQYncP5tPv/Q1zBQeqjRuR3r6oZeefzVeFZ4B3gWie5TeaNjuYuy // komt neer op de base64 representatie van de salt en hash, maar dan met een iets ander alfabet
 
Karin Gijssen

Karin Gijssen

04/08/2014 23:30:24
Quote Anchor link
Dank je Dos, ik zal nog eens kijken naar password_hash en even gaan spelen me de waardes 12-14.

Wat ik bedoelde met A3) is dat jij zegt dat ik $rs['wachtwoord'] als tweede parameter moet meegeven aan crypt. Bedoel je dit? if($rs["wachtwoord"] == crypt($_POST["txtPassword"], $rs['wachtwoord']))
Dat snap ik niet. Ik heb bij het aanmaken van het wachtwoord een salt gegenereerd. Die heb ik samen met het ingevoerde wachtwoord gehasht. Dus bij het checken van het wachtwoord moet ik weer het ingevoerde wachtwoord en de opgeslagen salt hashen. Met crypt($_POST["txtPassword"], $rs['wachtwoord']) hash ik het ingevoerde wachtwoord en het opgeslagen (gehashte) wachtwoord. Maar misschien begrijp ik het niet goed.


Toevoeging op 04/08/2014 23:46:19:

Ik heb de waarde veranderd en het gaat nu een heel stuk sneller! Dank!
 
Dos Moonen

Dos Moonen

05/08/2014 07:14:54
Quote Anchor link
crypt() verwerkt de salt in de uiteindelijke hash, die hash is dus ook een salt aangezien crypt het niet-salt gedeelte dan negeert.
 
Willem vp

Willem vp

05/08/2014 10:07:24
Quote Anchor link
1) Je zou de password-encryptie ook helemaal kunnen overlaten aan MySQL.
2) Stop met het gebruik van de mysql-functies, want die gaan op termijn verdwijnen. Gebruik in plaats daarvan de mysqli-functies.
3) Zelf vind het altijd fijner om prepared statements te gebruiken. Je hebt dan niet meer het gedoe met mysql_real_escape_string en wanneer je een query meermaals moet uitvoeren is het ook iets efficiënter.

Als code zou je dan zoiets krijgen:
(let op dat de gecrypte wachtwoorden waarschijnlijk niet meer overeenkomen met de PHP-versie)
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
   $dbh
= new mysqli($hostname,$user,$password,$database);
   $sql = "
      SELECT klantID, voornaam
      FROM tblKlant
      WHERE email = ?
      AND wachtwoord = SHA2(CONCAT(salt,?),256)
   "
;
   $stmt = $dbh->prepare($sql);
   $stmt->bind_param("ss", $_POST["txtEmail"], $_POST["txtPassword"]);
   $stmt->execute();
   $stmt->bind_result($klantid,$voornaam);
   $result = $stmt->fetch();
   $stmt->close();
   if ($result)
   {

      // session setten en redirect
   }
   else
   {
      // wachtwoord onjuist
      $strErrInlog = "<span class=\"warning\">De combinatie van gebruikersnaam<br />en wachtwoord is niet juist.</span><br /><br />";
   }

?>

Nog even wat uitleg bij de code:

In regel 2 maak je een connectie met de database (deze komt eigenlijk in de plaats van je oude mysql_connect).

In regel 3-8 staat het SQL-statement. Daarin zie je twee vraagtekens. Bij het preparen van het statement weet MySQL dat we straks op die plaatsen onze eigen parameters gaan invoeren. De eerste parameter wordt dan het email-adres en de tweede het door de gebruiker ingevoerde wachtwoord.

Aan de WHERE heb ik een extra conditie toegevoegd. Die crypt het wachtwoord en kijkt of het overeenkomt met wat er in de database staat. De CONCAT(salt,?) plakt de salt uit de database aan het ingevoerde (text) wachtwoord van de gebruiker. Die gehele string wordt vervolgens gecrypt met SHA2 (256-bits).

In regel 9 wordt het SQL-statement geprepared. MySQL doet dan het voorbereidende werk om de query uit te voeren, maar wacht nog tot je daar daadwerkelijk opdracht toe geeft.

In regel 10 worden de parameters aan de query gebonden: daar zeg je eigenlijk dat MySQL op de plaats van het eerste vraagteken $_POST["txtEmail"] moet invullen en op de plaats van het tweede vraagteken $_POST["txtPassword"]. Met de "ss" geef je aan dat beide parameters strings zijn. De strings hoef je niet meer zelf te escapen; bij prepared statements gebeurt dat automatisch.

In regel 11 wordt de query uitgevoerd.

In regel 12 worden de velden die je in de query opvraagt (in dit geval dus klantID en voornaam) gekoppeld aan PHP-variabelen.

In regel 13 wordt het eerste resultaat-record opgevraagd. Als het wachtwoord correct was, bevatten $klantID en $voornaam nu de waardes die zijn opgehaald uit de database. Als het wachtwoord niet correct was, of als het email-adres niet bestaat, geeft $stmt->fetch() een waarde terug die niet TRUE is, en wordt de foutmelding getoond.

In regel 14 wordt het prepared statement afgesloten. In dit geval ben je slechts geïnteresseerd in het eerste record (als dat er is). Normaal gesproken voer je de close() pas uit als je alle resultaten hebt verwerkt.

In regel 15 en verder kijk je naar het resultaat van de query. Heb je een TRUE-waarde gekregen dan is de combinatie van email-adres en wachtwoord correct en kun je de sessie opbouwen enzovoort.

Let overigens op dat je op het veld 'email' in je tabel een unique index zet; hiermee voorkom je dat hetzelfde email-adres meermaals (met verschillende wachtwoorden) wordt opgeslagen in je database.
Gewijzigd op 05/08/2014 10:17:41 door Willem vp
 
Karin Gijssen

Karin Gijssen

05/08/2014 10:48:46
Quote Anchor link
Dank je voor de uitgebreide en duidelijke uitleg Willem. Dat gaat wel lukken zo.
Is hashen met mysql veiliger of sneller dan via php?

Het email veld is geen unique index, maar bij registratie check ik of het wachtwoord al bestaat. De gebruiker wordt dan niet toegevoegd en krijgt de melding dat er al een gebruiker met dat emailadres is.
 
Dos Moonen

Dos Moonen

05/08/2014 11:19:49
Quote Anchor link
"Is hashen met mysql veiliger of sneller dan via php?"
Dat betekend dat je de wachtwoorden hashed op een manier die niet geschikt is voor het hashen van wachtwoorden. Wachtwoorden wil je met een relatief traag algoritme hashen, wat willem voorstelt is juist heel snel. Dus minder veilig.
 
Karin Gijssen

Karin Gijssen

05/08/2014 11:24:06
Quote Anchor link
Dos Moonen op 05/08/2014 07:14:54:
crypt() verwerkt de salt in de uiteindelijke hash, die hash is dus ook een salt aangezien crypt het niet-salt gedeelte dan negeert.


Even kijken of ik het nu goed begrijp: $rs['wachtwoord'] is de salt met het gehashte wachtwoord. Als ik dit in de vergelijking gebruik, negeert hij het gehaste wachtwoord en haalt hij dus eigenlijk de salt uit dat veld. Nu sla ik de salt apart op in de database, maar dat hoeft dus helemaal niet.
Gewijzigd op 05/08/2014 11:27:08 door Karin Gijssen
 
Willem vp

Willem vp

05/08/2014 11:31:43
Quote Anchor link
> Is hashen met mysql veiliger of sneller dan via php?

Geen idee of het sneller is. Op mijn systeem is het onmeetbaar snel in ieder geval. ;-) Overigens is snelheid niet echt een argument, want in dit geval zou het misschien zelfs wenselijk zijn als het hashen niet al te snel gaat.

Of het veiliger is: Ja. Het is in ieder geval veiliger dan blowfish, wat je nu gebruikt. Als je nog veiliger wilt zijn, moet je naar een 512-bits SHA (verander dan in de query de 256 in 512), maar dat kost meer opslagruimte. Zowel SHA-256 als SHA-512 zijn voor zover ik weet nog niet 'gekraakt'.

Het grote voordeel van het laten crypten door MySQL is dat je dat allemaal niet in je PHP-code hoeft af te handelen. Ook je gecrypte wachtwoord en zo hoef je niet op te halen. Je voert het opgegeven wachtwoord aan MySQL en ziet wel of er iets terugkomt. Lekker simpel. Hou ik van. ;-)

Overigens: als je je opslagruimte wilt beperken, zou je gebruik kunnen maken van het feit dat de door MySQL gegenereerde SHA-code hexadecimaal is. In plaats van het opslaan in een CHAR of VARCHAR-veld, kun je het wachtwoord (en eventueel de salt, als die ook hexadecimaal is) opslaan in een BINARY-veld. Je moet dan wel de MySQL-functies HEX en UNHEX gebruiken bij het ophalen en opslaan van de gecrypte wachtwoorden.

Toevoeging op 05/08/2014 11:37:21:

Dos Moonen op 05/08/2014 11:19:49:
Wachtwoorden wil je met een relatief traag algoritme hashen, wat willem voorstelt is juist heel snel. Dus minder veilig.

Eigenlijk wil je de penalty alleen toepassen wanneer iemand een fout wachtwoord invoert. Het is natuurlijk niet gebruikersvriendelijk om bij mensen die een correct wachtwoord invoeren een trage hash te gebruiken. ;-)

Je zou wanneer het wachtwoord niet correct blijkt te zijn een sleep(1) (of zelfs meer) kunnen uitvoeren.

Ik zie zelfs meer in het bijhouden van het aantal mislukte pogingen en het tijdstip van de laatste mislukte poging in een apart veld. Als het aantal mislukte pogingen meer is dan bijvoorbeeld 5, en het tijdstip van de laatste mislukte poging meer dan 5 minuten geleden, sta je voor die gebruiker vijf minuten geen inlogpoging meer toe (en daarna kun je de teller resetten). Dat heeft volgens mij meer effect dan een tragere hash-functie.
 
Karin Gijssen

Karin Gijssen

05/08/2014 11:40:45
Quote Anchor link
@Dos: helemaal goed! Ik haal de salt nu niet meer apart op (en heb de mysql_real_escape_string verwijderd) en de vergelijking aangepast. Onderstaand script werkt nu prima.
@Willem: ik ga nog aan de gang met mysqli, maar dat is voor een nieuwe dag.

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
$sql
=  "SELECT klantID, wachtwoord, voornaam " .
            "FROM tblKlant " .
            "WHERE email = '" . mysql_real_escape_string($_POST["txtEmail"]) . "'";
    $resultaat = mysql_query($sql);
    if(mysql_num_rows($resultaat) > 0)
    {

        // e-mailadres gevonden. Wachtwoord controleren.
        $rs = mysql_fetch_array($resultaat);
        if($rs["wachtwoord"] == crypt($_POST["txtPassword"], $rs['wachtwoord'])) // wachtwoord juist
        {
            set session en redirect
        }
        else
        {
            // wachtwoord onjuist
            $strErrInlog = "<span class=\"warning\">De combinatie van gebruikersnaam<br />en wachtwoord is niet juist.</span><br /><br />";
        }
 
Ward van der Put
Moderator

Ward van der Put

05/08/2014 13:16:57
Quote Anchor link
Als $rs["wachtwoord"] de in de database opgeslagen hash van het wachtwoord is, dan kan crypt($_POST["txtPassword"], $rs['wachtwoord']) (op regel 9 hierboven) toch nooit werken voor nieuwe wachtwoorden? Je krijgt dan $b == crypt($a, $b) als een kip-en-ei-probleem: de hash $b wordt bepaald door de salt, maar de salt $b is de hash $b. Of zie ik dat verkeerd?
 
Karin Gijssen

Karin Gijssen

05/08/2014 13:27:29
Quote Anchor link
@Ward
Dit is alleen om het wachtwoord te controleren. Een nieuw wachtwoord wordt gevormd door een random gegenereerde salt ($salt) en het ingevoerde wachtwoord: crypt($_POST["txtWachtwoord"], $salt)

Toevoeging op 05/08/2014 13:33:22:

@Dos: in dit geval staat dit wel in een query. Moet ik hier dan wel de mysql_real_escape_string rond $_POST["txtWachtwoord"] zetten?
 
- SanThe -

- SanThe -

05/08/2014 14:11:51
Quote Anchor link
Als het wachtwoord in de query gewoon de invoer van de bezoeker is moet je het altijd beveiligen. Maar als het al gecrypt of gehashed is dan is dat niet nodig.
 
Dos Moonen

Dos Moonen

05/08/2014 18:18:09
Quote Anchor link
"@Dos: in dit geval staat dit wel in een query. Moet ik hier dan wel de mysql_real_escape_string rond $_POST["txtWachtwoord"] zetten?"
Tijdens het registreren/wijzigen van je wachtwoord? Nee, het resultaat van crypt_blowfish bevat nooit iets dat mysql_real_escape_string() zal vervangen.
 



Overzicht Reageren

 
 

Om de gebruiksvriendelijkheid van onze website en diensten te optimaliseren maken wij gebruik van cookies. Deze cookies gebruiken wij voor functionaliteiten, analytische gegevens en marketing doeleinden. U vindt meer informatie in onze privacy statement.