a tiny mvc framework for php using php-activerecord
1<?php
2/*
3 * secure cookie-based session storage, based on the EncryptedCookieStore rails
4 * plugin (http://github.com/FooBarWidget/encrypted_cookie_store)
5 *
6 * Copyright (c) 2010 joshua stein <jcs@jcs.org>
7 *
8 * process of storing session data with a given key:
9 * 1. create a random IV
10 * 2. encrypt the IV with the key in 128-bit AES in ECB mode
11 * 3. create a SHA1 HMAC (with the key) of the session data
12 * 4. encrypt the HMAC and session data together with the key in 256-bit AES in
13 * CFB mode
14 * 5. store the base64-encoded encrypted IV and encrypted HMAC+data as a cookie
15 *
16 * to read the encrypted data on the next visit:
17 * 1. base64-decode the IV and data
18 * 2. decrypt the IV with the key
19 * 3. decrypt the HMAC+data with the key and decrypted IV
20 * 4. verify that the HMAC of the decrypted data matches the decrypted HMAC
21 * 5. return the plaintext session data
22 *
23 */
24
25namespace HalfMoon;
26
27class EncryptedCookieSessionStore {
28 /* the most amount of data we can store in the cookie (post-encryption) */
29 public static $MAX_COOKIE_LENGTH = 4096;
30
31 /* cookie parameters */
32 private static $settings = array();
33
34 private $cookie_name = "";
35 private $key = null;
36
37 public function __construct($key) {
38 if (!function_exists("mcrypt_encrypt"))
39 throw new \HalfMoon\HalfMoonException("mcrypt extension not "
40 . "installed");
41 if (strlen($key) != 32)
42 throw new \HalfMoon\HalfMoonException("cookie encryption key must "
43 . "be 32 characters long");
44
45 /* disable php's own sending of session cookies since they will
46 * conflict with what we're generating here */
47 ini_set("session.use_cookies", "off");
48
49 /* load settings as they are from boot, since controllers may change
50 * them */
51 static::$settings = session_get_cookie_params();
52
53 $this->key = pack("H*", $key);
54 }
55
56 public static function set_lifetime($secs) {
57 static::$settings["lifetime"] = $secs;
58 }
59
60 public static function set_path($path) {
61 static::$settings["path"] = $path;
62 }
63
64 public static function set_domain($domain) {
65 static::$settings["domain"] = $domain;
66 }
67
68 public static function set_secure($secure) {
69 static::$settings["secure"] = $secure;
70 }
71
72 public static function set_httponly($httponly) {
73 static::$settings["httponly"] = $httponly;
74 }
75
76 public function open($savepath, $name) {
77 $this->cookie_name = $name;
78
79 return true;
80 }
81
82 public function read($id) {
83 if (!isset($_COOKIE[$this->cookie_name]))
84 return "";
85
86 if ($_COOKIE[$this->cookie_name] == "")
87 return "";
88
89 list($e_iv, $e_data) = explode("--", $_COOKIE[$this->cookie_name], 2);
90
91 if (strlen($e_iv) && strlen($e_data)) {
92 $e_iv = base64_decode($e_iv);
93 $e_data = base64_decode($e_data);
94
95 $iv = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $this->key, $e_iv,
96 MCRYPT_MODE_ECB);
97
98 $data_and_hmac = @mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $this->key,
99 $e_data, MCRYPT_MODE_CFB, $iv);
100
101 $pieces = explode("--", $data_and_hmac, 2);
102
103 if (count($pieces) == 2) {
104 list($hmac, $data) = $pieces;
105
106 $hmac = base64_decode($hmac);
107
108 if (!strlen($hmac))
109 throw new \HalfMoon\InvalidCookieData("no HMAC");
110
111 if (hash_hmac("sha1", $data, $this->key, $raw = true) === $hmac)
112 return $data;
113 else
114 throw new \HalfMoon\InvalidCookieData("invalid HMAC");
115 }
116 }
117
118 return "";
119 }
120
121 public function write($id, $data) {
122 if (headers_sent())
123 return false;
124
125 /* generate random iv for aes-256-cfb */
126 $iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256,
127 MCRYPT_MODE_CFB), MCRYPT_RAND);
128
129 /* encrypt the iv with aes-128-ecb */
130 $e_iv = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $this->key, $iv,
131 MCRYPT_MODE_ECB);
132
133 $hmac = hash_hmac("sha1", $data, $this->key, $raw_output = true);
134
135 /* encrypt the hmac and data with aes-256-cfb, using the random iv */
136 $e_data = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $this->key,
137 base64_encode($hmac) . "--" . $data, MCRYPT_MODE_CFB, $iv);
138
139 $cookie = base64_encode($e_iv) . "--" . base64_encode($e_data);
140
141 if (strlen($cookie) > \HalfMoon\EncryptedCookieSessionStore::$MAX_COOKIE_LENGTH)
142 throw new \HalfMoon\InvalidCookieData("cookie data too long ("
143 . strlen($cookie) . " > "
144 . \HalfMoon\EncryptedCookieSessionStore::$MAX_COOKIE_LENGTH . ")");
145
146 setcookie(
147 $this->cookie_name,
148 $cookie,
149 (static::$settings["lifetime"] ?
150 time() + static::$settings["lifetime"] : 0),
151 static::$settings["path"],
152 static::$settings["domain"],
153 static::$settings["secure"],
154 static::$settings["httponly"]
155 );
156
157 /* just to help in debugging */
158 $_COOKIE[$this->cookie_name] = $cookie;
159
160 return true;
161 }
162
163 public function destroy($id) {
164 @setcookie(
165 $this->cookie_name,
166 "",
167 (static::$settings["lifetime"] ?
168 time() + $settings["lifetime"] : 0),
169 static::$settings["path"],
170 static::$settings["domain"],
171 static::$settings["secure"],
172 static::$settings["httponly"]
173 );
174
175 return true;
176 }
177
178 public function gc($maxlife) {
179 return true;
180 }
181
182 public function close() {
183 return true;
184 }
185}
186
187?>