public function write($ary, $now = 0) { // for this sample, we'll strip out meta data unset($ary[SecureString1::PREFIX_VERSION]); unset($ary[SecureString1::PREFIX_CREATED]); unset($ary[SecureString1::PREFIX_KEYID]); unset($ary[SecureString1::PREFIX_SALT]); unset($ary[SecureString1::PREFIX_MAC]); $payload = http_build_query($ary); // this is a "ok" 24-bit salt represent as 6 characters // (the substr is to remove padding chars which aren't useful here) // // A BETTER solution is use the DevUrandom // $salt = substr(SecureString1::b64_urlencode(pack("L", mt_rand())), 0, 6); $str = SecureString1::create($payload, $salt, $this->keys, $this->kid, $now); // grab cookie or URL from environment // since this is sample we'll just pull from global env $GLOBALS['userdata'] = $str; }
public function _write($now) { $this->dirty = FALSE; $ary = $this->payload; // we'll strip out meta data unset($ary[SecureString1::PREFIX_VERSION]); unset($ary[SecureString1::PREFIX_CREATED]); unset($ary[SecureString1::PREFIX_KEYID]); unset($ary[SecureString1::PREFIX_SALT]); unset($ary[SecureString1::PREFIX_MAC]); $payload = http_build_query($ary); $salt = SecureString1::b64_urlencode($this->random->bytes(6)); $str = SecureString1::create($payload, $salt, $this->keys, $this->kid, $now); switch ($this->encoding) { case self::ENCODE_B64: $str = SecureString1::b64_urlencode($str); break; case self::ENCODE_URL: $str = urlencode($str); break; } return $str; }
// ok here's some keys..... ideally from a config file // notice how the plain-text secret is md5'd to scramble the bits $keys = array(1 => hash('md5', "this is my secret key 1", true)); $mac = SecureString1::create($qs, $keys, 1); print $mac . "\n"; // if we do it again, the MAC is different (due to the salting) $mac = SecureString1::create($qs, $keys, 1); print $mac . "\n"; // the SecureString perserves query string formating // so you can break it apart just like this: parse_str($mac, $parts); // and now you have all the meta data in addition to your // data. print_r($parts); // Nice thing about PHP is that the associative array // keeps order. So we can reassemble the parts as is // and it still validates // print SecureString1::validate(http_build_query($parts), $keys) ? "same\n" : "different\n"; // like wise we can tamper with one element // and it won't re-validate $parts['cow'] = 'juice'; print_r($parts); print SecureString1::validate(http_build_query($parts), $keys) ? "same\n" : "different\n"; // and of course we can just directly tamper with it $bad = $mac; $bad[20] = '*'; // or anything, or add data or truncate print SecureString1::validate($bad, $keys) ? "same\n" : "different\n"; print_r(substr(SecureString1::b64_urlencode(pack("L", mt_rand())), 0, 6)); print "\n";
/** * what happens when the SS meta data fields are in the * payload? * * Not sure I like this behavior, but I'm just documenting it. * parse_str overwrites the original value when a key is duplicated * parse_str has all sorts of other issues. */ public function testPhpParseStrBehavior() { $keys = array(1 => hash('md5', 'this is my secret key', true)); $salt = '1234567890'; $macd = SecureString1::create(SecureString1::PREFIX_VERSION . '100' . SecureString1::PREFIX_CREATED . 'blah' . SecureString1::PREFIX_KEYID . '666', $salt, $keys, 1); $this->assertTrue(SecureString1::validate($macd, $keys)); parse_str($macd, $parts); $this->assertEquals($parts['_ver'], 1); $this->assertEquals($parts['_kid'], 1); $this->assertNotEquals($parts['_now'], 'blah'); }
public function testSmoke() { $keys = array(1 => md5('whatever!')); $kid = 1; $random = new NotRandom(); $cname = 'foo'; $domain = 'client9.com'; $path = '/'; $secure = true; $httponly = true; $expiration = 86400; $sc = new SecureCookie($keys, $kid, $random, $cname, $domain, $path, $secure, $httponly, $expiration, $expiration, SecureCookie::ENCODE_NONE); // new cookie is not dirty $this->assertFalse($sc->dirty); // test non-exsistant property $this->assertEquals($sc->get('foo', 1), 1); $this->assertFalse($sc->dirty); // set and get property $sc->set('foo', 2); $this->assertEquals($sc->get('foo'), 2); $this->assertTrue($sc->dirty); // set and then unset $sc->set('ding', 'bat'); $this->assertEquals($sc->get('ding'), 'bat'); $sc->set('ding', null); $this->assertTrue(is_null($sc->get('ding'))); // doing a read should wipe everything out $sc->read(); $this->assertEquals($sc->get('foo', 1), 1); $sc->set('lead', 'gold'); // fake time in seconds $now = 123456789; $str = $sc->_write($now); // since keys, salt, and time are all fixed this should // always be the same $expected = "lead=gold&_now=123456789&_slt=MTIzNDU2&_kid=1&_ver=1&_mac=fExl2Kj9ASCkVGvA7EfoOkc10RE5ehIwAXdzNJR6Jxc."; $this->assertEquals($str, $expected); // // TEST URL ENCODING // // write $sc = new SecureCookie($keys, $kid, $random, $cname, $domain, $path, $secure, $httponly, $expiration, $expiration, SecureCookie::ENCODE_URL); $sc->set('lead', 'gold'); // new cookie is not dirty $this->assertTrue($sc->dirty); $now = 123456789; $str = $sc->_write($now); $this->assertEquals($str, urlencode($expected)); // written out cookie is not dirty $this->assertFalse($sc->dirty); // read 1x $sc = new SecureCookie($keys, $kid, $random, $cname, $domain, $path, $secure, $httponly, $expiration, $expiration, SecureCookie::ENCODE_NONE); $_COOKIE[$cname] = urlencode($expected); $ok = $sc->read($now + 1); $this->assertEquals(SecureCookie::REASON_OK, $ok); // read 2x $sc = new SecureCookie($keys, $kid, $random, $cname, $domain, $path, $secure, $httponly, $expiration, $expiration, SecureCookie::ENCODE_NONE); $_COOKIE[$cname] = urlencode(urlencode($expected)); $ok = $sc->read($now + 1); $this->assertEquals(SecureCookie::REASON_OK, $ok); // BASE 64 // write 64 $sc = new SecureCookie($keys, $kid, $random, $cname, $domain, $path, $secure, $httponly, $expiration, $expiration, SecureCookie::ENCODE_B64); $sc->set('lead', 'gold'); $now = 123456789; $str = $sc->_write($now); $this->assertEquals($str, SecureString1::b64_urlencode($expected)); // read base 64 $sc = new SecureCookie($keys, $kid, $random, $cname, $domain, $path, $secure, $httponly, $expiration, $expiration, SecureCookie::ENCODE_NONE); $_COOKIE[$cname] = SecureString1::b64_urlencode($expected); $ok = $sc->read($now + 1); $this->assertEquals(SecureCookie::REASON_OK, $ok); // TAINT $str = SecureString1::b64_urlencode($expected); $str[10] = '*'; // invalid char $_COOKIE[$cname] = $str; $ok = $sc->read($now + 1); $this->assertEquals(SecureCookie::REASON_INVALID, $ok); // // KEY PROBLEMS // // $sc = new SecureCookie($keys, $kid, $random, $cname, $domain, $path, $secure, $httponly, $expiration, $expiration, SecureCookie::ENCODE_NONE); $_COOKIE[$cname] = $expected; $ok = $sc->read($now + 1); $this->assertEquals(SecureCookie::REASON_OK, $ok); // test if the key is different $keys2 = array(1 => md5('different key')); $sc = new SecureCookie($keys2, $kid, $random, $cname, $domain, $path, $secure, $httponly, $expiration, $expiration, SecureCookie::ENCODE_NONE); $_COOKIE[$cname] = $expected; $ok = $sc->read($now + 1); $this->assertEquals(SecureCookie::REASON_INVALID, $ok); // let's try again but this time with a bad key id $keys2 = array(2 => md5('foo')); $sc = new SecureCookie($keys2, $kid, $random, $cname, $domain, $path, $secure, $httponly, $expiration, $expiration, SecureCookie::ENCODE_NONE); $_COOKIE[$cname] = $expected; $ok = $sc->read($now + 1); $this->assertEquals(SecureCookie::REASON_INVALID, $ok); // // EXPIRATION AND CLOCK SKEW // // $sc = new SecureCookie($keys, $kid, $random, $cname, $domain, $path, $secure, $httponly, $expiration, $expiration, SecureCookie::ENCODE_NONE); $_COOKIE[$cname] = $expected; $ok = $sc->read($now + $expiration + 1); $this->assertEquals(SecureCookie::REASON_EXPIRED, $ok); // clock skew $sc = new SecureCookie($keys, $kid, $random, $cname, $domain, $path, $secure, $httponly, $expiration, $expiration, SecureCookie::ENCODE_NONE); $_COOKIE[$cname] = $expected; $ok = $sc->read($now - $expiration); $this->assertEquals(SecureCookie::REASON_CLOCK_SKEW, $ok); }