diff --git a/src/Pluf/Sign.php b/src/Pluf/Sign.php new file mode 100644 index 0000000..cc71ff6 --- /dev/null +++ b/src/Pluf/Sign.php @@ -0,0 +1,193 @@ + + * $signed = Pluf_Sign::sign($mystring); + * // send the string over the wire + * $mystring = Pluf_Sign::unsign($signed); + * + * + * Usage to pack and sign an object: + *
+ * $signed = Pluf_Sign::dumps($myobject);
+ * // send the string over the wire
+ * $myobject = Pluf_Sign::loads($signed);
+ * 
+ * + * Based on the work by Simon Willison: + * http://github.com/simonw/django-openid/blob/master/django_openid/signed.py + */ +class Pluf_Sign +{ + /** + * Dump and sign an object. + * + * If you want to sign a small string, use directly the + * sign/unsign function as compression will not help and you will + * save the overhead of the serialize call. + * + * @param mixed Object + * @param string Key (null) + * @param bool Compress with gzdeflate (false) + * @param string Extra key not to use only the secret_key ('') + * @return string Signed string + */ + public static function dumps($obj, $key=null, $compress=false, $extra_key='') + { + $serialized = serialize($obj); + $is_compressed = false; // Flag for if it's been compressed or not + if ($compress) { + $compressed = gzdeflate($serialized, 9); + if (strlen($compressed) < (strlen($serialized) - 1)) { + $serialized = $compressed; + $is_compressed = true; + } + } + $base64d = Pluf_Utils::urlsafe_b64encode($serialized); + if ($is_compressed) { + $base64d = '.'.$base64d; + } + if ($key === null) { + $key = Pluf::f('secret_key'); + } + return self::sign($base64d, $key.$extra_key); + } + + /** + * Reverse of dumps, throw an Exception in case of bad signature. + * + * @param string Signed key + * @param string Key (null) + * @param string Extra key ('') + * @return mixed The dumped signed object + */ + public static function loads($s, $key=null, $extra_key='') + { + if ($key === null) { + $key = Pluf::f('secret_key'); + } + $base64d = self::unsign($s, $key.$extra_key); + $decompress = false; + if ($base64d[0] == '.') { + // It's compressed; uncompress it first + $base64d = substr($base64d, 1); + $decompress = true; + } + $serialized = Pluf_Utils::urlsafe_b64decode($base64d); + if ($decompress) { + $serialized = gzinflate($serialized); + } + return unserialize($serialized); + } + + /** + * Sign a string. + * + * If the key is not provided, it will use the secret_key + * available in the configuration file. + * + * The signature string is safe to use in URLs. So if the string to + * sign is too, you can use the signed string in URLs. + * + * @param string The string to sign + * @param string Optional key (null) + * @return string Signed string + */ + public static function sign($value, $key=null) + { + if ($key === null) { + $key = Pluf::f('secret_key'); + } + return $value.'.'.self::base64_hmac($value, $key); + } + + + /** + * Unsign a value. + * + * It will throw an exception in case of error in the process. + * + * @return string Signed string + * @param string Optional key (null) + * @param string The string + */ + public static function unsign($signed_value, $key=null) + { + if ($key === null) { + $key = Pluf::f('secret_key'); + } + if (false === strpos($signed_value, '.')) { + throw new Exception('Missing signature (no . found in value).'); + } + list($value, $sig) = explode('.', $signed_value, 2); + if (self::base64_hmac($value, $key) == $sig) { + return $value; + } else { + throw new Exception(sprintf('Signature failed: "%s".', $sig)); + } + } + + /** + * Calculate the URL safe base64 encoded SHA1 hmac of a string. + * + * @param string The string to sign + * @param string The key + * @return string The signature + */ + public static function base64_hmac($value, $key) + { + return Pluf_Utils::urlsafe_b64encode(self::hmac_sha1($key, $value)); + } + + + + /** + * HMAC-SHA1 function. + * + * @see http://us.php.net/manual/en/function.sha1.php#39492 + * + * @param string Key + * @param string Data + * @return string Calculated binary HMAC-SHA1 + */ + public static function hmac_sha1($key, $data) + { + if (strlen($key) > 64) { + $key = pack('H*', sha1($key)); + } + $key = str_pad($key, 64, chr(0x00)); + $ipad = str_repeat(chr(0x36), 64); + $opad = str_repeat(chr(0x5c), 64); + return pack('H*',sha1(($key^$opad).pack('H*',sha1(($key^$ipad).$data)))); + return bin2hex($hmac); + } +} \ No newline at end of file diff --git a/src/Pluf/Tests/Sign/Sign.php b/src/Pluf/Tests/Sign/Sign.php new file mode 100644 index 0000000..7b7e285 --- /dev/null +++ b/src/Pluf/Tests/Sign/Sign.php @@ -0,0 +1,117 @@ +assertEqual(Pluf_Sign::sign($s), + $s.'.'.Pluf_Sign::base64_hmac($s, + Pluf::f('secret_key') + ) + ); + } + + function testSignIsReversible() + { + $examples = array( + 'q;wjmbk;wkmb', + '3098247529087', + '3098247:529:087:', + 'jkw osanteuh ,rcuh nthu aou oauh ,ud du', + ); + foreach ($examples as $example) { + $this->assertTrue($example != Pluf_Sign::sign($example)); + $this->assertEqual($example, Pluf_Sign::unsign(Pluf_Sign::sign($example))); + } + } + + function testUnsignDetectsTampering() + { + $value = 'Another string'; + $signed_value = Pluf_Sign::sign($value); + $transforms = array( + strtoupper($signed_value), + $signed_value.'a', + 'a'.substr($signed_value, 1), + str_replace('w', '', $signed_value), + ); + $this->assertEqual($value, Pluf_Sign::unsign($signed_value)); + foreach ($transforms as $t) { + try { + Pluf_Sign::unsign($t); + $this->fail(); + } catch (Exception $e) { + $this->pass(); + } + } + } + + function testEncodeDecode() + { + $objects = array( + array('an', 'array'), + 'a string', + (object) array('a' => 'property'), + ); + foreach ($objects as $o) { + $this->assertTrue($o != Pluf_Sign::dumps($o)); + $this->assertEqual($o, Pluf_Sign::loads(Pluf_Sign::dumps($o))); + } + } + + function testDecodeDetectsTampering() + { + $value = array('foo'=> 'bar', 'baz'=> 1); + $encoded = Pluf_Sign::dumps($value); + $transforms = array( + strtoupper($encoded), + $encoded.'a', + 'a'.substr($encoded, 1), + str_replace('M', '', $encoded), + ); + $this->assertEqual($value, Pluf_Sign::loads($encoded)); + foreach ($transforms as $t) { + try { + Pluf_Sign::loads($t); + $this->fail(); + } catch (Exception $e) { + $this->pass(); + } + } + } +} \ No newline at end of file diff --git a/src/Pluf/Utils.php b/src/Pluf/Utils.php index 60d3425..bdc4e9c 100644 --- a/src/Pluf/Utils.php +++ b/src/Pluf/Utils.php @@ -259,4 +259,34 @@ class Pluf_Utils $code = proc_close($process); return $output; } + + /** + * URL safe base 64 encoding. + * + * Compatible with python base64's urlsafe methods. + * + * @link http://www.php.net/manual/en/function.base64-encode.php#63543 + */ + public static function urlsafe_b64encode($string) + { + return str_replace(array('+','/','='), + array('-','_',''), + base64_encode($string)); + } + + /** + * URL safe base 64 decoding. + */ + public static function urlsafe_b64decode($string) + { + $data = str_replace(array('-','_'), + array('+','/'), + $string); + $mod4 = strlen($data) % 4; + if ($mod4) { + $data .= substr('====', $mod4); + } + return base64_decode($data); + } + }