3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
20 * The length of the randomly-generated, per-session signing key.
22 * Expressed as number of bytes. (Ex: 128 bits = 16 bytes)
26 const PRIVATE_KEY_LENGTH
= 16;
30 * @see hash_hmac_algos()
32 const HASH_ALGO
= 'sha256';
35 * The minimum length of a generated signature/digest (expressed in base36 digits).
38 const HASH_LENGTH
= 25;
40 public static $_key = NULL;
42 public static $_sessionID = NULL;
45 * Generate a private key per session and store in session.
48 * private key for this session
50 public static function privateKey() {
52 $session = CRM_Core_Session
::singleton();
53 self
::$_key = $session->get('qfPrivateKey');
55 self
::$_key = base64_encode(random_bytes(self
::PRIVATE_KEY_LENGTH
));
56 $session->set('qfPrivateKey', self
::$_key);
63 * @return mixed|null|string
65 public static function sessionID() {
66 if (!self
::$_sessionID) {
67 $session = CRM_Core_Session
::singleton();
68 self
::$_sessionID = $session->get('qfSessionID');
69 if (!self
::$_sessionID) {
70 self
::$_sessionID = CRM_Core_Config
::singleton()->userSystem
->getSessionId();
71 $session->set('qfSessionID', self
::$_sessionID);
74 return self
::$_sessionID;
78 * Generate a form key based on form name, the current user session
79 * and a private key. Modelled after drupal's form API
82 * @param bool $addSequence
83 * Should we add a unique sequence number to the end of the key.
88 public static function get($name, $addSequence = FALSE) {
89 $key = self
::sign($name);
92 // now generate a random number between 1 and 10000 and add it to the key
93 // so that we can have forms in mutiple tabs etc
94 $key = $key . '_' . mt_rand(1, 10000);
100 * Validate a form key based on the form name.
103 * @param string $name
104 * @param bool $addSequence
107 * if valid, else null
109 public static function validate($key, $name, $addSequence = FALSE) {
110 if (!is_string($key)) {
115 list($k, $t) = explode('_', $key);
116 if ($t < 1 ||
$t > 10000) {
124 $expected = self
::sign($name);
125 if (!hash_equals($k, $expected)) {
132 * Check that the key is well-formed. This does not check that the key is
133 * currently a key that is in use or belongs to a real form/session.
138 * TRUE if the signature ($key) is well-formed.
140 public static function valid($key) {
141 // ensure that key is an alphanumeric string of at least HASH_LENGTH with
142 // an optional underscore+digits at the end.
143 return preg_match('#^[0-9a-zA-Z]{' . self
::HASH_LENGTH
. ',}+(_\d+)?$#', $key) ?
TRUE : FALSE;
147 * @param string $name
148 * The name of the form
150 * A signed digest of $name, computed with the per-session private key
152 private static function sign($name) {
153 $privateKey = self
::privateKey();
154 $sessionID = self
::sessionID();
156 if (strpos($sessionID, $delim) !== FALSE ||
strpos($name, $delim) !== FALSE) {
157 throw new \
RuntimeException("Failed to generate signature. Malformed session-id or form-name.");
159 // The "prefix" gives some advisory details to help with debugging.
160 $prefix = preg_replace('/[^a-zA-Z0-9]/', '', $name);
161 // Note: Unsure why $sessionID is included, but it's always been there, and it doesn't seem harmful.
162 return $prefix . base_convert(hash_hmac(self
::HASH_ALGO
, $sessionID . $delim . $name, $privateKey), 16, 36);