Kirjautuminen

Haku

Tehtävät

Keskustelu: Koodit: PHP: TCrypto: Symmetrinen salaus ja datan käsittely

timoh [17.06.2011 20:57:26]

#

Lisään tännekin, jos täällä kellään tarvetta tällaiselle esimerkkikoodille.

Esimerkki symmetrisestä salauksesta ja siihen liittyvästä datan käsittelystä. Esimerkkikoodina käyttökelpoinen "sessiodatan" tallentaminen/lukeminen evästeestä. Esimerkissä käytetty kryptaukseen PHP:n Mcrypt-laajennosta. Tosin esim. OpenSSL -funktioiden käyttö kryptauksen toteuttajana onnistuu helposti (kirjoittamalla OpenSSL-funktioita käyttävä TCrypto_CryptoInterface:n toteuttava luokka).

Ver 1.03:
-Fixattu handleri /dev/urandom:sta lukemisessa
-Varmistetaan ettei encryptattava datastringi sisällä null-tavuja lopussa
-Kommentteja lisätty

<?php
/**
 * "Esimerkkikirjasto" datan tallentamisesta ja lukemisesta key/value pareina
 * (evästeeseen).
 *
 * Mahdollisuus kryptata ja pakata data. Esimerkissä data tallennetaan
 * evästeeseen (helppo laajentaa myös datan tallennus esim. tietokantaan).
 *
 * Kryptaus on toteutettu Mcryptilla, AES-256 cbc-moodissa.
 * IV (Initializin Vector) luodaan käyttäen vahvaa satunnaisdataa.
 *
 * Huolehtii datan eheyden tarkistamisen sekä tarvittavien avainten
 * muodostamisen jne.
 *
 * HUOM. Jos data tallennetaan evästeeseen, mutta HTTPS-yhteyttä
 * ei ole, kryptaus ei estä kaikkia mahdollisia hyökkäyksiä (hyökkääjä voi esim.
 * toistaa validin käyttäjän oikeellisen datan). Tämä symmetrinen salaus ei
 * korvaa salattua tietoliikenneyhteyttä, vaikka se estääkin datan sisällön
 * näkemisen.
 *
 * Esimerkkejä:
 * $storage = new TCrypto_Storage_Cookie(); // Vaatii oletuksena HTTPS-yhteyden
 * // $storage = new TCrypto_Storage_Cookie(false); jos sallitaan suojaamaton yhteys
 * // $storage = new TCrypto_Storage_Cookie(true, 'my_cookie_name');
 * $crypto = new TCrypto_Crypto_McryptAes256Cbc();
 * $compress = new TCrypto_Compress_Gz();
 * $options = array('mac_key' => 'gJKjgk54r...', 'cipher_key' => 'yji*f34...'); // Avainten tulee olla ainakin 40 merkin mittaiset
 * // Options:
 * //(string) 'mac_key', (string) 'cipher_key', (array) 'entropy_pool', (int) 'max_lifetime', (bool) 'save_on_set'
 *
 * // Injektoidaan riippuvuudet.
 * // Pakollinen riippuvuus on $storage, muut valinnaisia
 * $tcrypto = new TCrypto($storage, $crypto, $compress, $options);
 * $tcrypto->setValue('key', 'Value'); // Value voi olla mikä vain serialisoituva "muuttuja"
 * $tcrypto->save();
 *
 * // Esim. seuraavalla sivulatauksella:
 * $tcrypto = new TCrypto($storage, $crypto, $compress, $options);
 * echo $tcrypto->getValue('key');
 * $tcrypto->removeValue('key'); // Poistetaan avain
 * $tcrypto->save();
 *
 * @author timoh <timoh6@gmail.com>
 * @license Public Domain
 * @version $Id: 1.03 $
 */

// Rajapinnat mitä vasten toteutetaan Crypto-, Storage- ja Compress -luokat.

interface TCrypto_CryptoInterface
{
    /*
     * Encrypts the data.
     *
     * @param string $data
     * @param string $iv
     * @param strig $key
     */
    public function encrypt($data, $iv, $key);

    /*
     * Decrypts the data.
     *
     * @param string $data
     * @param string $iv
     * @param strig $key
     */
    public function decrypt($data, $iv, $key);

    /*
     * Returns the needed IV length in bytes.
     *
     * @return int
     */
    public function getIvLen();

    /*
     * Returns the needed key length in bytes.
     *
     * @return int
     */
    public function getKeyLen();
}

interface TCrypto_StorageInterface
{
    /*
     * Loads the data from a storage (cookie, file, database etc.).
     */
    public function fetch();

    /*
     * Saves the data to a storage.
     *
     * @param string $data
     */
    public function save($data);

    /*
     * Removes the data from a storage.
     */
    public function remove();
}

interface TCrypto_CompressInterface
{
    public function compress($data);

    public function decompress($data);
}

// Yllä olevien rajapintojen toteuttavat esimerkkiluokat
// (Crypto, Storage ja Compress).

class TCrypto_Crypto_McryptAes256Cbc implements TCrypto_CryptoInterface
{
    protected $_td = null;

    public function __construct()
    {
        // "AES" cbc-moodissa.
        if (false === ($td = mcrypt_module_open('rijndael-128', '', 'cbc', '')))
        {
            return false;
        }

        $this->_td = $td;
    }

    public function encrypt($data, $iv, $key)
    {
        // Max. 2^32 lohkoa samalla avaimella (ei väliä onko
        // yhdessä pötkössä vai erillään pienemmisä osissa).
        if (mcrypt_generic_init($this->_td, $key, $iv) !== 0)
        {
            return false;
        }

        // Varmistetaan että $data ei sisällä "oikeassa reunassa" null-merkkiä
        // (ettei sekaannu Mcryptin käyttämän paddingin kanssa).
        $cipherText = mcrypt_generic($this->_td, $data);
        mcrypt_generic_deinit($this->_td);
        unset($data, $iv, $key);

        return $cipherText;
    }

    public function decrypt($data, $iv, $key)
    {
        if (mcrypt_generic_init($this->_td, $key, $iv) !== 0)
        {
            return false;
        }

        $plainText = mdecrypt_generic($this->_td, $data);
        // Stripataan mahdollinen padding pois.
        $plainText = rtrim($plainText, "\0");
        mcrypt_generic_deinit($this->_td);
        unset($data, $iv, $key);

        return $plainText;
    }

    public function getIvLen()
    {
        // AES:in käyttämä lohkokoko on 128 bittiä, tarvittava alustusvektori
        // on oltava saman kokoinen. Palautetaan tavuina.
        return 16;
    }

    public function getKeyLen()
    {
        // AES 256 vaatii avaimeksi 256 bittisen merkkijonon. 128-bittisellä
        // avaimella muodostuisi AES 128. Palautetaan tavuina.
        return 32;
    }

    public function __destruct()
    {
        if ($this->_td !== null)
        {
            mcrypt_module_close($this->_td);
        }
    }
}

class TCrypto_Storage_Cookie implements TCrypto_StorageInterface
{
    protected $_cookieName = 'my_cookie';
    protected $_requireSecure = true;

    public function __construct($secure = true, $name = null)
    {
        $this->_requireSecure = (bool) $secure;

        if ($name !== null)
        {
            $this->_cookieName = $name;
        }
    }

    /*
     * Returns the data from a cookie.
     *
     * @return mixed
     */
    public function fetch()
    {
        return isset($_COOKIE[$this->_cookieName]) ? self::decodeBase64UrlSafe($_COOKIE[$this->_cookieName]) : false;
    }

    /*
     * Saves data to a cookie.
     *
     * @param string $dataString
     * @return boolean
     */
    public function save($dataString)
    {
        $https = isset($_SERVER['HTTPS']) ? $_SERVER['HTTPS'] : '';
        if (strtolower($https) === 'off')
        {
            $https = '';
        }

        if ($this->_requireSecure === true && empty($https))
        {
            return false;
        }

        $dataString = self::encodeBase64UrlSafe($dataString);

        return setcookie($this->_cookieName, $dataString, 0, '/', '', $this->_requireSecure, true);
    }

    /*
     * Removes the cookie.
     *
     * @return boolean
     */
    public function remove()
    {
        return setcookie($this->_cookieName, '', time() - 31104000, '/', '', '', true);
    }

    /*
     * URL safe Base64 encoding (suitable for a cookie).
     *
     * @return mixed
     */
    public static function encodeBase64UrlSafe($value)
    {
        return str_replace(array('+', '/', '='), array('-', '_', ''), base64_encode($value));
    }

    /*
     * URL safe Base64 deconding (suitable for a cookie).
     *
     * @return mixed
     */
    public static function decodeBase64UrlSafe($value)
    {
        $value = str_replace(array('-', '_'), array('+', '/'), $value);
        if (false === ($value = base64_decode($value, true)))
        {
            return false;
        }

        $mod = strlen($value) % 4;
        if ((int) $mod > 0)
        {
            $value = str_pad($value, $mod, '=');
        }

        return $value;
    }
}

class TCrypto_Compress_Gz implements TCrypto_CompressInterface
{
    public function compress($data)
    {
        return gzdeflate($data, 9);
    }

    public function decompress($data)
    {
        return gzinflate($data);
    }
}

class TCryptoException extends Exception
{
}

// Varsinainen "työhevonen". TCrypto hoitaa datan käsittelyn käyttäen
// apunaan yllä luotuja Crypto-, Storage-, ja Compress-luokkia.
class TCrypto
{
    // Vähintään 40 merkkiä.
    protected $_macKey = '';

    // Vähintään 40 merkkiä, mikäli kryptaus on käytössä.
    protected $_cipherKey = '';

    // Datan max. elinaika sekunteina. Jos elinaika on umpeutunut, dataa hävitetään.
    protected $_macMaxLifetime = 3600;

    // Muuttujat "apuobjekteille". $_storageHandler on pakko antaa.
    // Jos data halutaan kryptata/kompressoida, tarvitaan objektit myös näille.
    protected $_storageHandler = null;
    protected $_cryptoHandler = null;
    protected $_compressHandler = null;

    // Varsinainen data (array key/value parit).
    protected $_data = null;

    // Mahdolliset lisäentropian lähteet (käytetään MAC- ja kryptausavaimissa).
    protected $_entropyPool = array();

    // Jos true, data tallennetaan data "storageen" heti setValue():n yhteydessä.
    // Muutoin vasta save() -kutsun jälkeen.
    protected $_saveOnSet = false;

    public function __construct(TCrypto_StorageInterface $storage, TCrypto_CryptoInterface $crypto = null,
                                TCrypto_CompressInterface $compress = null, array $options = array())
    {
        $this->_storageHandler = $storage;
        $this->_cryptoHandler = $crypto;
        $this->_compressHandler = $compress;

        $this->_setOptions($options);
        unset($options);

        // Nopea tsekkaus että $_macKey on ainakin 40 tavua.
        if (!isset($this->_macKey[39]))
        {
            throw new TCryptoException('Insufficient parameters: $_macKey must be at least 40 characters');
        }

        $this->_extractData();
    }

    /*
     * Set a key/value pair to be stored. If $_saveOnSet is true,
     * the data will be immediately saved to the storage.
     *
     * @param string $key
     * @param mixed $data
     */
    public function setValue($key, $data = null)
    {
        $this->_data[$key] = $data;

        if ($this->_saveOnSet === true)
        {
            $this->save();
        }
    }

    /*
     * @param string $key
     * @param mixed $default
     * @return mixed
     */
    public function getValue($key, $default = null)
    {
        return array_key_exists($key, $this->_data) ? $this->_data[$key] : $default;
    }

    /*
     * @return boolean
     */
    public function removeValue($key)
    {
        if (array_key_exists($key, $this->_data))
        {
            unset($this->_data[$key]);

            return true;
        }

        return false;
    }

    /*
     * Saves the data to a storage.
     *
     * @return boolean
     * @thows TCryptoException
     */
    public function save()
    {
        if (is_array($this->_data) && count($this->_data) > 0)
        {
            $data = serialize($this->_data);

            // Luodaan aikaleimat (luettaessa serverin aika täytyy olla näiden aikojen välissä)
            $timestamp = time();
            $macExpire = $timestamp + (int) $this->_macMaxLifetime;

            // Pakataan data, jos $_compressHandler on injektoitu mukaan.
            if ($this->_compressHandler !== null)
            {
                $data = $this->_compressHandler->compress($data);
            }

            // Encryptataan data, jos $_cryptoHandler on injektoitu mukaan.
            if ($this->_cryptoHandler !== null)
            {
                // Pyydetään $_cryptoHandler:lta tarvittava IV- ja avainpituus.
                $ivLen = $this->_cryptoHandler->getIvLen();
                $keyLen = $this->_cryptoHandler->getKeyLen();

                if (false === ($iv = $this->getRandomBytes($ivLen)))
                {
                    throw new TCryptoException('Could not get random bytes');
                }

                // Nopea tsekkaus että $_cipherKey on ainakin 40 tavua.
                if (!isset($this->_cipherKey[39]))
                {
                    throw new TCryptoException('Insufficient parameters: $_cipherKey must be at least 40 characters');
                }

                try
                {
                    // Mixataan $cryptoKey kokoon eri elementeistä (jotta saadaan joka kerta uniikki avain).
                    $cryptoKey = $this->_setupKey(array($timestamp, $macExpire, $iv, $this->_cipherKey));
                    $cryptoKey = $this->_hash($cryptoKey, $keyLen);
                    // Lisätään IV datapötkön alkuun (decryptatessa poimitaan IV tältä paikalta).
                    $data = $iv . $this->_cryptoHandler->encrypt($data, $iv, $cryptoKey);
                    unset($cryptoKey);
                }
                catch (TCryptoException $e)
                {
                    throw $e;
                }
            }

            // Pyydetään 8 tavua random-dataa, jotta MAC-avaimesta saadaan uniikki.
            if (false === ($randomBytes = $this->getRandomBytes(8)))
            {
                throw new TCryptoException('Could not get random bytes');
            }

            // Lopullinen datastringi on [MAC][aikaleima][expireaikaleima][satunnaismerkijono][data]
            // "data" sisältää IV:n, jos tarvetta (jos ollaan käytetty kryptausta).

            // Tiivistetään aikaleimat hieman pienempään tilaan,
            // sekä lisätään random-data ja varsinainen data.
            $dataString = base_convert($timestamp, 10, 36) .
                          base_convert($macExpire, 10, 36) .
                          $randomBytes .
                          $data;

            try
            {
                // $macKey muodostetaan vastaavasti kuin $cryptoKey, mutta muutamalla eri parametrilla.
                $macKey = $this->_setupKey(array($timestamp, $macExpire, $randomBytes, $this->_macKey));

                $mac = $this->_hmac($dataString, $macKey);
                $dataString = $mac . $dataString;
                unset($macKey);

                return $this->_storageHandler->save($dataString);
            }
            catch (TCryptoException $e)
            {
                throw $e;
            }
        }

        $this->destroy();
    }

    /*
     * Destroys the data both from memory and storage.
     */
    public function destroy()
    {
        $this->_data = array();

        return $this->_storageHandler->remove();
    }

    /*
     * Extracts the data from storage.
     */
    protected function _extractData()
    {
        $this->_data = array();
        // Haetaan data
        $liveData = $this->_storageHandler->fetch();
        $data = '';

        // Nopea tsekkaus onko $liveData true ja onko $liveData ainakin 53 tavua.
        if ($liveData !== false && isset($liveData[52]))
        {
            $currentMac = (string) substr($liveData, 0, 32);
            $timestamp = (int) base_convert((string) substr($liveData, 32, 6), 36, 10);
            $macExpire = (int) base_convert((string) substr($liveData, 38, 6), 36, 10);
            $randomBytes = (string) substr($liveData, 44, 8);

            // Tsekataan ollaanko aikaleimojen välissä.
            if (time() >= $timestamp && time() <= $macExpire)
            {
                // Data ilman MACia
                $dataString = (string) substr($liveData, 32);
                $macKey = $this->_setupKey(array($timestamp, $macExpire, $randomBytes, $this->_macKey), false);
                $mac = $this->_hmac($dataString, $macKey);

                // "Constant time" merkkijonovertailu. Varmistetaan että hyökkääjä
                // ei voi hyökätä MAC-vertailun aikavuotoa vastaan.
                if ($this->_compareString($currentMac, $mac) === true)
                {
                    // Data ilman MACia, aikaleimoja ja satunnaisdataa (sisältää vielä IV:n jos data on kryptattu).
                    $data = substr($dataString, 20);

                    if ($this->_cryptoHandler !== null)
                    {
                        $ivLen = $this->_cryptoHandler->getIvLen();
                        $keyLen = $this->_cryptoHandler->getKeyLen();

                        // Nopea tsekkaus sisältääkö data vähintään tarpeellisen määrän tavuja.
                        if (isset($data[$ivLen]))
                        {
                            $iv = (string) substr($data, 0, $ivLen);
                            $cryptoKey = $this->_setupKey(array($timestamp, $macExpire, $iv, $this->_cipherKey), false);
                            $cryptoKey = $this->_hash($cryptoKey, $keyLen);
                            $data = $this->_cryptoHandler->decrypt(substr($data, $ivLen), $iv, $cryptoKey);
                            unset($cryptoKey);
                        }
                    }

                    if ($this->_compressHandler !== null)
                    {
                        $data = $this->_compressHandler->decompress($data);
                    }

                    if ($data !== false)
                    {
                        $data = unserialize($data);
                        if ($data !== false && is_array($data))
                        {
                            // Jos data on saatu arrayna takaisin, lisätään se muistiin
                            // (muuttujat tulevat saatavilla getValue('xxx')...
                            foreach ($data as $k => $v)
                            {
                                $this->setValue($k, $v);
                            }

                            return;
                        }
                    }
                }
            }
        }

        // Käytettävää dataa ei ollut. Tyhjennetään muisti ja "storage".
        $this->destroy();
    }

    /*
     * Constructs a key string for HMAC/encryption
     *
     * @param array $fields
     * @param boolean $throwException
     * @return string
     * @thows TCryptoException
     */
    protected function _setupKey(array $fields = array(), $throwException = true)
    {
        $key = '';

        // $fields sisältää käytännössä aikaleimoja ja konkreettiset MAC- ja kryptoavaimet.
        if (empty($fields))
        {
            if ($throwException === true)
            {
                throw new TCryptoException('Key construction failed: $fields must not be empty');
            }
        }

        // Jos ollaan lisätty valinnaisia entropian lähteitä (IP-osoite tjms.),
        // lisätään ne avaimeen tässä.
        foreach ($this->_entropyPool as $field)
        {
            $key .= $field;
        }

        // Lisätään itse varsinainen "avainmateriaali" viimeisenä. Käytännössä
        // viimeinen lisättävä elementti tulee olla $_macKey tai $_cipherKey,
        // riippuen luodaanko cryptoavainta vai MAC-avainta. Tällä edesautetaan,
        // ettei vuodeta tietoa avaimesta.
        foreach ($fields as $field)
        {
            $key .= $field;
        }
        unset($field);

        return (string) $key;
    }

    protected function _setOptions(array $options = array())
    {
        if (isset($options['mac_key']))
        {
            $this->_macKey = (string) $options['mac_key'];
        }
        if (isset($options['cipher_key']))
        {
            $this->_cipherKey = (string) $options['cipher_key'];
        }
        if (isset($options['entropy_pool']))
        {
            // Esim. array($_SERVER['REMOTE_ADDR'])
            $this->_entropyPool = (array) $options['entropy_pool'];
        }
        if (isset($options['max_lifetime']))
        {
            $this->_macMaxLifetime = (int) $options['max_lifetime'];
        }
        if (isset($options['save_on_set']))
        {
            // Jos true, tallennetaan data "storageen" heti setValuen yhteydessä.
            $this->_saveOnSet = (bool) $options['save_on_set'];
        }

        unset($options);
    }

    // Erinäisiä apumetodeja.

    protected function _hash($data, $len = 32)
    {
        $data = hash('sha512', $data, true);

        // Kutistetaan $data (varmistetaan ettei vuodeta tietoa käytetystä avaimesta).
        return substr($data, 0, $len);
    }

    protected function _hmac($data, $key)
    {
        return hash_hmac('sha256', $data, $key, true);
    }

    // http://code.google.com/p/oauth/
    protected function _compareString($stringA, $stringB)
    {
        $stringA = (string) $stringA;
        $stringB = (string) $stringB;

        if (strlen($stringA) === 0 || strlen($stringB) === 0)
        {
            return false;
        }

        if (strlen($stringA) !== strlen($stringB))
        {
            return false;
        }

        $result = 0;
        $len = strlen($stringA);

        for ($i = 0; $i < $len; $i++)
        {
            $result |= ord($stringA{$i}) ^ ord($stringB{$i});
        }

        return $result === 0;
    }

    /*
     * Generate a random string of bytes.
     *
     * @param int $count
     * @return mixed
     */
    public static function getRandomBytes($count)
    {
        $count = (int) $count;
        $bytes = '';
        $hasBytes = false;

        // Ei käytetä openssl_random_pseudo_bytes() -funktiota jos PHP versio on
        // vanhempi kuin 5.3.4 (openssl_random_pseudo_bytes:n "blocking":n takia).
        if (PHP_VERSION >= '5.3.4' && function_exists('openssl_random_pseudo_bytes'))
        {
            $tmp = openssl_random_pseudo_bytes($count, $cryptoStrong);
            if ($tmp !== false && $cryptoStrong === true)
            {
                $bytes = $tmp;
                $hasBytes = true;
            }
        }

        // Vaaditaan vähintään PHP 5.3, jotta Windows-alustoilla saadaan
        // kunnollista satunnaisdataa.
        if ($hasBytes === false && PHP_VERSION >= '5.3')
        {
            $tmp = mcrypt_create_iv($count, MCRYPT_DEV_URANDOM);
            if ($tmp !== false)
            {
                $bytes = $tmp;
                $hasBytes = true;
            }
        }

        if ($hasBytes === false && file_exists('/dev/urandom') && is_readable('/dev/urandom') && (false !== ($fh = fopen('/dev/urandom', 'rb'))))
        {
            if (function_exists('stream_set_read_buffer'))
            {
                stream_set_read_buffer($fh, 0);
            }

            $tmp = fread($fh, $count);
            fclose($fh);
            if ($tmp !== false)
            {
                $bytes = $tmp;
            }
        }

        if (strlen($bytes) === $count)
        {
            return $bytes;
        }
        else
        {
            return false;
        }
    }
}

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta