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;
}
}
}Aihe on jo aika vanha, joten et voi enää vastata siihen.