'hello', * 'param2' => 'world', * ); * $token = $signer->sign($params); * ... * assertTrue($signer->validate($token, $params)); * @endcode */ class CRM_Utils_Signer { /** * Expected length of the salt * * @var int */ const SALT_LEN = 4; /** * Instantiate a signature-processor * * @param $secret * String, private. * @param $paramNames * Array, fields which should be part of the signature. */ public function __construct($secret, $paramNames) { sort($paramNames); // ensure consistent serialization of payloads $this->secret = $secret; $this->paramNames = $paramNames; $this->signDelim = "_"; // chosen to be valid in URLs but not in salt or md5 $this->defaultSalt = CRM_Utils_String::createRandom(self::SALT_LEN, CRM_Utils_String::ALPHANUMERIC); } /** * Generate a signature for a set of key-value pairs * * @param $params * Array, key-value pairs. * @param $salt * String, the salt (if known) or NULL (for auto-generated). * @return string, the full public token representing the signature */ public function sign($params, $salt = NULL) { $message = array(); $message['secret'] = $this->secret; $message['payload'] = array(); if (empty($salt)) { $message['salt'] = $this->createSalt(); } else { $message['salt'] = $salt; } // recall: paramNames is pre-sorted for stability foreach ($this->paramNames as $paramName) { if (isset($params[$paramName])) { if (is_numeric($params[$paramName])) { $params[$paramName] = (string) $params[$paramName]; } } else { // $paramName is not included or ===NULL $params[$paramName] = ''; } $message['payload'][$paramName] = $params[$paramName]; } $token = $message['salt'] . $this->signDelim . md5(serialize($message)); return $token; } /** * Determine whether a token represents a proper signature for $params * * @param $token * String, the full public token representing the signature. * @param $params * Array, key-value pairs. * * @throws Exception * @return bool, TRUE iff all $paramNames for the submitted validate($params) and the original sign($params) */ public function validate($token, $params) { list ($salt, $signature) = explode($this->signDelim, $token); if (strlen($salt) != self::SALT_LEN) { throw new Exception("Invalid salt [$token]=[$salt][$signature]"); } $newToken = $this->sign($params, $salt); return ($token == $newToken); } /** * @return string */ public function createSalt() { // It would be more secure to generate a new value but liable to run this // many times on certain admin pages; so instead we'll re-use the hash. return $this->defaultSalt; } }