Příkaz $newPassword = $this->passwords->hash( $values->newPassword) ⇒ pokaždé jiný řetězec

Karel Chramosil
Člen | 114
+
0
-

Dobrý den,

příkaz

 $newPassword = $this->passwords->hash( $values->newPassword);

mne generuje pokaždé jiný řetězec

<?php
/**
 * Presenter, který zajišťuje změnu hesla uživatele.
 */

namespace App\Presenters;

use App\Model\User;
use Nette;
use Nette\Application\UI\Form;
use Nette\Security\Passwords;

final class UserPresenter extends SecuredPresenter
{
	/**
	 * Továrna na vytvoření formuláře pro změnu hesla.
	 * @return Nette\Application\UI\Form
	 */

    private $database;
    /** @var Passwords */
    private $passwords;

	public function __construct(
	    Nette\Database\Explorer $database,
	    Nette\Security\Passwords $passwords
	) {
		$this->database = $database;
		$this->passwords = $passwords;
	}

	protected function createComponentPasswordForm(): Form
	{
		$form = new Form();
		$form->addPassword('oldPassword', 'Staré heslo:', 30)
		//$form->addText('oldPassword', 'Staré heslo:', 30);
			->addRule(Form::FILLED, 'Je nutné zadat staré heslo.');
		$form->addPassword('newPassword', 'Nové heslo:', 30)
		//$form->addText('newPassword', 'Nové heslo:', 30);
			->addRule(Form::MIN_LENGTH, 'Nové heslo musí mít alespoň %d znaků.', 5);
		$form->addPassword('confirmPassword', 'Potvrzení hesla:', 30)
		//$form->addText('confirmPassword', 'Nové heslo:', 30);
			->addRule(Form::FILLED, 'Nové heslo je nutné zadat ještě jednou pro potvrzení.')
			->addRule(Form::EQUAL, 'Zadaná nové hesla se musejí shodovat.', $form['newPassword']);
		$form->addSubmit('set', 'Změnit heslo');
		$form->onSuccess[] = [$this, 'passwordFormSubmitted'];
	return $form;
	}


	/**
	 * Zpracuje odeslaný formulář. Mění heslo uživatele.
	 * @param Nette\Application\UI\Form $form
	 */
	public function passwordFormSubmitted(Form $form)
	{
		$values = $form->getValues();
		$user = $this->getUser();

        $passwords = new Passwords(PASSWORD_BCRYPT, ['cost' => 12]);

        //$newPassword = $values->newPassword;
        $newPassword = $this->passwords->hash( $values->newPassword); // Zahashuje heslo

		try {
		    $row = $this->database->table('user')
                ->WHERE('id = ?', $user->getId());

			if($row){

                $this->database->table('user')
                    ->get($user->getId())
                    ->update(array('password'=>$newPassword));
                $this->flashMessage('Heslo bylo změněno.', 'success');
                $this->redirect('Homepage:');
            } else  {
                $this->flashMessage( 'Staré heslo není platné.', 'success');
            }
		} catch (NS\AuthenticationException $e) {
			$form->addError('Nelze se připojit do databáze.');
		}
	}

sloupec password v tabulce je velký 255

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `username` varchar(255) COLLATE utf8_czech_ci NOT NULL,
  `password` varchar(255) COLLATE utf8_czech_ci NOT NULL,
  `role` varchar(30) COLLATE utf8_czech_ci NOT NULL DEFAULT 'user',
  `titul` varchar(20) COLLATE utf8_czech_ci DEFAULT NULL,
  `jmeno` varchar(100) COLLATE utf8_czech_ci NOT NULL,
  `prijmeni` varchar(100) COLLATE utf8_czech_ci NOT NULL,
  `email` varchar(255) COLLATE utf8_czech_ci NOT NULL,
  `funkce` varchar(255) COLLATE utf8_czech_ci NOT NULL,
  `oddeleni` varchar(255) COLLATE utf8_czech_ci NOT NULL,
  `mobil` varchar(255) COLLATE utf8_czech_ci NOT NULL,
  `zapsal_id` int(11) DEFAULT NULL,
  `pocetprihlaseni` int(11) DEFAULT NULL,
  `remonte_host` varchar(30) COLLATE utf8_czech_ci DEFAULT NULL,
  `remonte_port` int(11) DEFAULT NULL,
  `active` tinyint(1) NOT NULL DEFAULT 0,
  `admin` tinyint(1) NOT NULL DEFAULT 0,
  `zakazan` tinyint(1) NOT NULL DEFAULT 0,
  `prihlasil_kdy` date DEFAULT NULL,
  `datum_zapsal` date NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci;

Děkuji předem za radu.

Karel Chramosil

Polki
Člen | 553
+
+9
-

To je očekávané chování.

Vychází to z bezpečnosti.

Dneska je standard, že se uživatel registruje a je identifikován pomocí E-mailu, hesla a případně dvoufázového ověření.
Tedy Ostatní věci se úspěšně mění a dají se skrýt, nebo podvrhnout. (To jde u hesla a E-mailu taky, ale musíš je nejdřív znát a zjistit heslo je těžší, než ostatní věci.)

No a teď si vezmi tu nejjednodušší variantu a to E-mail a heslo. Takový běžný uživatel třeba nezná různé Password peněženky atp. a tedy všude používá jedno a to samý heslo, aby si je nemusel nikam psát, ale taky aby si jich nemusel pamatovat 200k.

Pokud bych tedy měl E-mail a@b.cz a heslo Jahudka123
Tak heslo se přeloží (pomocí třeba MD5) do 623df8275c160d1a47a2b048ac950447

A teď si představ, že nějaký hacker odhalí bezpečnostní chybu na MySQL serverech a místo aby ji nahlásil, tak ji zneužije.
Pak dostane třeba taková data, jelikož všichni hashují pomocí MD5:

DB1:
Jmeno:                 	Heslo:
nobody@nowhere.cz      	900150983cd24fb0d6963f7d28e17f72
a@b.cz					623df8275c160d1a47a2b048ac950447

DB2:
Jmeno:                 	Heslo:
nobody@nowhere.cz      	202cb962ac59075b964b07152d234b70
a@b.cz					623df8275c160d1a47a2b048ac950447

DB3:
Jmeno:                 	Heslo:
nobody@nowhere.cz      	653a7682a525a85f38f9e3a173d2169f
a@b.cz					623df8275c160d1a47a2b048ac950447

Projede tyto data a zjistí, že díky tomu, že je použito MD5 čistý a uživatel a@b.cz má vždycky stejný hash, tak to znamená, že na všech těchto aplikacích používá uživatel s E-mailem a@b.cz stejné heslo, zatímco uživatel nobody@nowhere.cz má hash pokaždé jiný, takže používá různá hesla.

A teď se na to podívej z pohledu toho hackera. Které heslo budeš chtít prolomit?
Samozřejmě, že heslo uživatele a@b.cz, protože víš, že tento uživatel používá stejné heslo na vícero místech a tedy pravděpodobně má stejné heslo i na jiných účtech (například na E-mailu, což je pro hackera nejlepší, jelikož přes to hodně hesel resetuješ.)

O to horší to bude, pokud útočník narazí na to, že dva různí uživatelé mají stejný hash.

Okay. Tak si řekneš přidáme tam nějaký další systém, který to bude odlišovat. Tedy pohledáš na netu a najdeš něco o tzv. soli.
Pecka. Aplikujeme to všude a to tedy, že každá aplikace si při svém startu vygeneruje nějaký svůj unikátní token, který bude ta sůl.
Samotné hashování pak proběhne třeba takto md5($salt . $password)

Takže výsledek bude asi takový: (stejná hesla jako v minulém příkladu a taktéž MD5 hashovací funkce)

DB1:	(Salt: abc)
Jmeno:                 	Heslo:
nobody@nowhere.cz      	440ac85892ca43ad26d44c7ad9d47d3e
a@b.cz					ec2fc2278bfc6417c76bc641d83cc3bc

DB2:	(Salt: 123)
Jmeno:                 	Heslo:
nobody@nowhere.cz      	4297f44b13955235245b2497399d7a93
a@b.cz					14b11f4e641d1daca5fcd81d3b5b0f27

DB3:	(Salt: a2c)
Jmeno:                 	Heslo:
nobody@nowhere.cz      	895a827b6d1e511c5f6b11c7c28c1b18
a@b.cz					d4e7fa802a81c8cd96544f46bda4c319

No a tady vidíš, že i když uživatel a@b.cz používá nerozumně všude stejné heslo, tak díky soli je v každé aplikaci to heslo zobrazeno jinak. Tedy pokud potencionální hacker dostane tato data, tak mu ta data neřeknou absolutně nic i když bude znát salt daného webu, jelikož princip tvoření hashe je tak pseudonáhodný, že pokud platí (P != NP), tak nelze najít spojitost mezi hashi napříč těmi aplikacemi.

Problém nastane v tomto případě:

DB3:	(Salt: a2c)
Jmeno:                 	Heslo:
somebody@somewhere.cz  	d4e7fa802a81c8cd96544f46bda4c319
a@b.cz					d4e7fa802a81c8cd96544f46bda4c319

No.. V tomto případě i když je web uvědomělý a používá salt, tak nastal ten problém, že salt je statická pro konkrétní web a tedy když se na tom webu registruje více uživatelů a použije stejné heslo, tak v databázi se to projeví jako stejný hash, protože u obou těchto hesel se použije stejná salt.

Abych to nenatahoval víc, než to je, tak ještě doplním, že i kdyby jsi jako salt použil třeba sůl webu skonkatenovanou s E-mailem uživatele, tak se může klidně stát, že v databázi bude mít ten stejný uživatel 2 stejné hashe. Například pokud zadáš možnost, aby uživatel mohl například zpřístupnit svůj článek na heslo. No a jelikož se mu nechtějí vymýšlet nová hesla, tak si zadá heslo stejné jako k přihlašování a bum máš problém, jelikož už jsou zde 2řádky s totožným heslem md5('a2c' . 'a@b.cz' . 'Jahudka123')

No jak tedy tomuto problému předejít? Jednoduše. Při KAŽDÉM hashování generovat čistě náhodnou sůl a hashovat toto heslo pod touto čistě náhodnou solí. Kam ale tuhle sůl uložit? Odpověď je taky jednoduchá. Jak jsem psal výše neexistuje zjistitelná korelace mezi solí a heslem, takže když zveřejníš část hesla (což je ta sůl), tak pořád musíš hádat ten zbytek (uživatelem zadané heslo) aby jsi zjistil, jestli 2 hesla nejsou stejná.

Tedy můžu udělat toto:

  1. Vygeneruju random salt AtcIeYBDYq
  2. Zahashuju salt pomocí MD5 md5('AtcIeYBDYq' . 'Jahudka123')4680d8d9377e64367dd57be63f537e21
  3. Salt potřebuju někde uchovat, takže ji přidám na začátek toho hashe (vím, že salt má 10 znaků) 'AtcIeYBDYq' . '4680d8d9377e64367dd57be63f537e21' ⇒ AtcIeYBDYq4680d8d9377e64367dd57be63f537e21
  4. Výsledek uložím do databáze.
  5. Pro každé další heslo udělám to stejné, takže i když v té stejné aplikaci někdo zadá to stejné heslo, tak se mu vygeneruje jiná salt a tedy bude mít v databázi uložený jiný hash.

No a ověřování, jestli je heslo, pod kterým se uživatel chce přihlásit správné ověříme takto:

  1. Vezmeme heslo z databáze AtcIeYBDYq4680d8d9377e64367dd57be63f537e21
  2. Rozdělíme na prvních 10 znaků (salt) a zbytek ⇒ AtcIeYBDYq4680d8d9377e64367dd57be63f537e21
  3. Vezmeme salt a heslo, zahashujeme a zjistíme, jestli se to rovná výsledku: md5('AtcIeYBDYq' . 'Jahudka123') === '4680d8d9377e64367dd57be63f537e21'
  4. Pokud ano, hesla se shodují, pokud ne, hesla se neshodují.

Thats all. Díky tomuto systému to funguje tak, jak říkáš a tedy, že při každém hashování se stejné heslo přeloží do úplně jiného otisku, což zvyšuje bezpečnost tím, že i kdyby všichni uživatelé v aplikaci používali jen 1 a to samé heslo, tak pro útočníka to bude vypadat, že každý má heslo úplně jiné.

Nepoužívej prosím hashovací funkci MD5, kterou jsem uváděl v příkladech. Ta už je dávno prolomená. Můžeš ji používat třeba na ověřování integrity souborů, které posíláš po netu, ale NIKDY NE NA HESLA. Místo toho používej nejmodernější BCRYPT, která má práci se solí v sobě, nebo prostě třídu Passwords od Nette (předanou přes DI), která volá interně PHP funkce password_hash a password_verify s potřebnými parametry a ty se tedy nemusíš o nic dalšího starat.

Editoval Polki (11. 12. 2021 4:27)

Karel Chramosil
Člen | 114
+
0
-

Dobrý den,

Moc děkuji za pěkně vysvětlenou a rychlou odpověď. Už mne přihlašování funguje přesně podle dokumentce. Cpal jsem do $hashe zakodované password.

Odpověď určitě pomůže dalším uživatelům Nette.

Karel Cramosil