Oct 4, 2012

Symfony2 - saját jelszó kódoló (encoder) készítése

A napokban kezdtem el fejleszteni egy hobbi projektet Symfony2-ben és az ezzel kapcsolatos tapasztalataimat osztom majd meg a mostani - és lesznek majd később is Symfony2-vel kapcsolatos posztok - bejegyzésben.

Tehát Symfony2-ben van egy nagyon ügyes csomagunk (bundle - Symfony2-ben minden csomagokba van rendezve, fel lehet fogni egyfajta plugin-ként, amiben a forrástól kezdve a hozzá tartozó erőforrások [css, js...] is megtalálhatók) amit Security néven érhetünk el. Ez a könyvtár biztosítja számunkra (természetesen nem muszáj ezt használni) az authentikációt, authorizációt és ezáltal képes a teljes bejelentkezést lebonyolítani, némi konfiguráció árán.

A komponens konfigurációjában lehetőség van az encoder tulajdonságainak beállítására, amellyel megadhatjuk milyen fajta egyirányú kódolást (hash) szeretnénk alkalmazni (plusz még, hogy hány iteráción keresztül álljon elő ez a hash és hogy base64 formában szeretnénk-e tárolni) jelszavaink tárolásakor.

Alapértelmezett esetben elég szép számmal kapunk hash algoritmusokat, amelyeket használhatunk,. Viszont mi szeretnénk írni egy saját encoder-t, nem elégszünk meg az általánosan használt jelszó kódolási eljárásokkal (pl. md5, sha1).
Hogy miért nem? Az említett algoritmusok túlságosan gyorsak. Amennyiben valaki megszerez egy adatbázist és rendelkezik megfelelő szivárvány táblával, mai GPU-kal gyorsan sikerrel járhat a jelszavak törésében. Persze még mindig jobb az md5 vagy az sha1 használata, mint a plaintext.
Mivel javíthatunk felhasználóink jelszavainak kódolásán?
  • Megköveteljük, hogy a felhasználó milyen formájú, hosszúságú jelszavat adhat meg (pl. szükséges kis- és nagybetű, szám illetve, hogy x hosszúságú legyen. Lehet extrémebben is, mondjuk hogy ismert, egyszerű szavakat kiszűrünk, azokat nem tartalmazhatja a bevitt adat.)
  • Sózással. Az egyszerű jelszót kicsit megfűszerezzük egy hosszabb, véletlenszerű karakterekből álló lánccal.
  •  Kiegészítő eljárás bevonása a kódolásba: például a key stretching, azaz a kulcs nyújtása.
  • Valamilyen erősebb kódolású, nem feltétlenül gyors (egyirányú kódolási) algoritmust használunk.
Az első ponttal az a probléma, hogy a felhasználó "életét" nehezítjük meg. A sózás viszont csak abban az esetben hatékony, ha minden látogatóhoz külön, egyedi sót alkalmazunk. Amennyiben így teszünk, nem lehetetlenítjük el, de legalább megnehezítjük a támadó dolgát. Ebben az esetben a krekkernek minden egyes felhasználóhoz külön szivárvány táblát kell generálnia (tegyük fel, hogy hozzáférést szerzett az adatbázishoz, azzal együtt ugyebár a sóhoz is).

Kiegészítő eljárásként a kulcs ún. nyújtását említettem meg, amely technika arra hivatott, hogy megerősítse az egyébként gyenge kódolást, hogy hatékonyabb legyen egy esetleges brute force ellen (egyébként a Symfony2 is ezt a technikát használja). Az eljárás után egy megerősített kulcsot kapunk. Az funkció lényege tehát, hogy több ideig tart ellenőrizni egy bizonyos megfelelést a hash-re. Legegyszerűbben az algoritmust leíró pszeudo kód alapján érthető meg:

key = ""
for 1 to 65536 do
    key = hash(key + password + salt)

Az utolsó pont szintén a kulcs nyújtását használja fel alapgondolatként, itt konkrétan a bcrypt, nevű függvényre gondoltam. A kódolással kapcsolatos problémáink gyökere az algoritmus sebességén alapul. Amennyiben nekünk rövid ideig fog tartani az adott hash előállítása, akkor a jelszavainkat törő személynek is, ezért próbáljunk inkább lassabb funkciót használni, hogy még tovább tartson az esetleges szivárvány táblák előállítása. Ez irányú erőfeszítésünkre (lassítás) nyújt segítséget az előbb említett bcrypt egy cost (vagy ha úgy tetszik work factor) paraméterrel. Ő egy két számjegyű érték, amely a [04-31] intervallumban kell, hogy szerepeljen. A kiválasztott szám az iterációs lépések kettes alapú logaritmusa. Adjunk meg cost paraméternek a 12-t, ekkor az iterációk száma (key stretching - előző pszeudo kód) 212 lesz, azaz 4096 (log24096=12). Egyik kedvenc szkript nyelvünkön (a másik ugyebár a Python :]) a crypt funkcióval használható.

Az elméleti kiruccanás után térjünk át a Symfony2 specifikus részre. Nos, alap esetben a következőképpen fest a konfigurációnk (app/config/security.yml), ha nem kívánjuk kiegészíteni a kódolási mechanizmusunkat például a bcrypt-el:

encoders:
    Symfony\Component\Security\Core\User\User:
        algorithm: sha1
        iterations: 128
        encode_as_base64: false

A konfig magáért beszél, megadjuk a kívánt algoritmust, az iterációk számát és hogy base64 kódolásban szeretnénk-e letárolni az eredményt. Ahhoz, hogy saját encoder-t készítsünk, implementálnunk kell a Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface felületet:

namespace B3ha\UserBundle\Service;

use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;

class BCryptEncoder implements PasswordEncoderInterface
{
    /**
     * @var integer Must be in range 4-31
     */
    protected $cost;
 
    /**
     *
     * @param integer $cost
     */
    public function __construct($cost)
    {
        $cost = (int)$cost;
        if ($cost < 4 || 31 < $cost) {
            throw new \Symfony\Component\Config\Definition\Exception\Exception(
                'Cost parameter must be in range 4-31');
        }
        $this->cost = sprintf('%02d', $cost);
    }
 
    public function encodePassword($raw, $salt)
    {
        return crypt($raw, "\$2a\${$this->cost}\${$salt}");
    }

    public function isPasswordValid($encoded, $raw, $salt)
    {
        return $encoded === $this->encodePassword($raw, $salt);
    }
}

A fenti kódhoz nem is fűznék kommentárt, elég kicsi ahhoz, hogy érthető legyen. Ha készen vagyunk az implementálással, akkor a következő lépésként beköthetjük a saját kódolónkat:

encoders:
    B3ha\UserBundle\Entity\User:
        id: b3ha.user.bcrypt_encoder

Érdemes service-ként (ActualBundle/Resources/config/services.yml, majd a fő konfig fájlban ezt importálni) megvalósítani (akár a ServiceContainer-ről is lehet későbbiekben poszt) az encoder-t, ezt a lenti kód mutatja:

parameters:
    # Cost parameter must be in range 04-31
    b3ha.user.bcrypt_encoder.cost: 12

services:
    b3ha.user.bcrypt_encoder:
        class: B3ha\UserBundle\Service\BCryptEncoder
        arguments: [%b3ha.user.bcrypt_encoder.cost%]

Nem tértem ki minden részletre, mert ez a poszt inkább az elméleti részről és a Symfony2-ben már kicsit   jártasabb kódereknek szólt. Részletes leírásokat természetesen a symfony.com oldalon találhattok, ha kérdésetek, kiegészítésetek vagy javításotok lenne írjatok bátran.