Příkaz $newPassword = $this->passwords->hash( $values->newPassword) ⇒ pokaždé jiný řetězec
- Karel Chramosil
- Člen | 114
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
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:
- Vygeneruju random salt
AtcIeYBDYq
- Zahashuju salt pomocí MD5
md5('AtcIeYBDYq' . 'Jahudka123')
⇒4680d8d9377e64367dd57be63f537e21
- 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
- Výsledek uložím do databáze.
- 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:
- Vezmeme heslo z databáze
AtcIeYBDYq4680d8d9377e64367dd57be63f537e21
- Rozdělíme na prvních 10 znaků (salt) a zbytek ⇒
AtcIeYBDYq
a4680d8d9377e64367dd57be63f537e21
- Vezmeme salt a heslo, zahashujeme a zjistíme, jestli se to rovná
výsledku:
md5('AtcIeYBDYq' . 'Jahudka123') === '4680d8d9377e64367dd57be63f537e21'
- 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
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