* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
class CRM_Core_Key {
+
+ /**
+ * The length of the randomly-generated, per-session signing key.
+ *
+ * Expressed as number of bytes. (Ex: 128 bits = 16 bytes)
+ *
+ * @var int
+ */
+ const PRIVATE_KEY_LENGTH = 16;
+
+ /**
+ * @var string
+ * @see hash_hmac_algos()
+ */
+ const HASH_ALGO = 'sha256';
+
+ /**
+ * The minimum length of a generated signature/digest (expressed in base36 digits).
+ * @var int
+ */
+ const HASH_LENGTH = 25;
+
public static $_key = NULL;
public static $_sessionID = NULL;
$session = CRM_Core_Session::singleton();
self::$_key = $session->get('qfPrivateKey');
if (!self::$_key) {
- self::$_key = md5(uniqid(mt_rand(), TRUE)) . md5(uniqid(mt_rand(), TRUE));
+ self::$_key = base64_encode(random_bytes(self::PRIVATE_KEY_LENGTH));
$session->set('qfPrivateKey', self::$_key);
}
}
* valid formID
*/
public static function get($name, $addSequence = FALSE) {
- $privateKey = self::privateKey();
- $sessionID = self::sessionID();
- $key = md5($sessionID . $name . $privateKey);
+ $key = self::sign($name);
if ($addSequence) {
- // now generate a random number between 1 and 100K and add it to the key
+ // now generate a random number between 1 and 10000 and add it to the key
// so that we can have forms in mutiple tabs etc
$key = $key . '_' . mt_rand(1, 10000);
}
$k = $key;
}
- $privateKey = self::privateKey();
- $sessionID = self::sessionID();
- if ($k != md5($sessionID . $name . $privateKey)) {
+ $expected = self::sign($name);
+ if (!hash_equals($k, $expected)) {
return NULL;
}
return $key;
}
/**
- * The original version of this function, added circa 2010 and untouched
- * since then, seemed intended to check for a 32-digit hex string followed
- * optionally by an underscore and 4-digit number. But it had a bug where
- * the optional part was never checked ever. So have decided to remove that
- * second check to keep it simple since it seems like pseudo-security.
+ * Check that the key is well-formed. This does not check that the key is
+ * currently a key that is in use or belongs to a real form/session.
*
* @param string $key
*
* @return bool
+ * TRUE if the signature ($key) is well-formed.
*/
public static function valid($key) {
- // ensure that key contains a 32 digit hex string
- return (bool) preg_match('#[0-9a-f]{32}#i', $key);
+ // ensure that key is an alphanumeric string of at least HASH_LENGTH with
+ // an optional underscore+digits at the end.
+ return preg_match('#^[0-9a-zA-Z]{' . self::HASH_LENGTH . ',}+(_\d+)?$#', $key) ? TRUE : FALSE;
+ }
+
+ /**
+ * @param string $name
+ * The name of the form
+ * @return string
+ * A signed digest of $name, computed with the per-session private key
+ */
+ private static function sign($name) {
+ $privateKey = self::privateKey();
+ $sessionID = self::sessionID();
+ $delim = chr(0);
+ if (strpos($sessionID, $delim) !== FALSE || strpos($name, $delim) !== FALSE) {
+ throw new \RuntimeException("Failed to generate signature. Malformed session-id or form-name.");
+ }
+ // The "prefix" gives some advisory details to help with debugging.
+ $prefix = preg_replace('/[^a-zA-Z0-9]/', '', $name);
+ // Note: Unsure why $sessionID is included, but it's always been there, and it doesn't seem harmful.
+ return $prefix . base_convert(hash_hmac(self::HASH_ALGO, $sessionID . $delim . $name, $privateKey), 16, 36);
+
}
}