Merge pull request #19296 from eileenmcnaughton/fbool
[civicrm-core.git] / CRM / Core / Key.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17 class CRM_Core_Key {
18
19 /**
20 * The length of the randomly-generated, per-session signing key.
21 *
22 * Expressed as number of bytes. (Ex: 128 bits = 16 bytes)
23 *
24 * @var int
25 */
26 const PRIVATE_KEY_LENGTH = 16;
27
28 /**
29 * @var string
30 * @see hash_hmac_algos()
31 */
32 const HASH_ALGO = 'sha256';
33
34 /**
35 * The minimum length of a generated signature/digest (expressed in base36 digits).
36 * @var int
37 */
38 const HASH_LENGTH = 25;
39
40 public static $_key = NULL;
41
42 public static $_sessionID = NULL;
43
44 /**
45 * Generate a private key per session and store in session.
46 *
47 * @return string
48 * private key for this session
49 */
50 public static function privateKey() {
51 if (!self::$_key) {
52 $session = CRM_Core_Session::singleton();
53 self::$_key = $session->get('qfPrivateKey');
54 if (!self::$_key) {
55 self::$_key = base64_encode(random_bytes(self::PRIVATE_KEY_LENGTH));
56 $session->set('qfPrivateKey', self::$_key);
57 }
58 }
59 return self::$_key;
60 }
61
62 /**
63 * @return mixed|null|string
64 */
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 = session_id();
71 $session->set('qfSessionID', self::$_sessionID);
72 }
73 }
74 return self::$_sessionID;
75 }
76
77 /**
78 * Generate a form key based on form name, the current user session
79 * and a private key. Modelled after drupal's form API
80 *
81 * @param string $name
82 * @param bool $addSequence
83 * Should we add a unique sequence number to the end of the key.
84 *
85 * @return string
86 * valid formID
87 */
88 public static function get($name, $addSequence = FALSE) {
89 $key = self::sign($name);
90
91 if ($addSequence) {
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);
95 }
96 return $key;
97 }
98
99 /**
100 * Validate a form key based on the form name.
101 *
102 * @param string $key
103 * @param string $name
104 * @param bool $addSequence
105 *
106 * @return string
107 * if valid, else null
108 */
109 public static function validate($key, $name, $addSequence = FALSE) {
110 if (!is_string($key)) {
111 return NULL;
112 }
113
114 if ($addSequence) {
115 list($k, $t) = explode('_', $key);
116 if ($t < 1 || $t > 10000) {
117 return NULL;
118 }
119 }
120 else {
121 $k = $key;
122 }
123
124 $expected = self::sign($name);
125 if (!hash_equals($k, $expected)) {
126 return NULL;
127 }
128 return $key;
129 }
130
131 /**
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.
134 *
135 * @param string $key
136 *
137 * @return bool
138 * TRUE if the signature ($key) is well-formed.
139 */
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;
144 }
145
146 /**
147 * @param string $name
148 * The name of the form
149 * @return string
150 * A signed digest of $name, computed with the per-session private key
151 */
152 private static function sign($name) {
153 $privateKey = self::privateKey();
154 $sessionID = self::sessionID();
155 $delim = chr(0);
156 if (strpos($sessionID, $delim) !== FALSE || strpos($name, $delim) !== FALSE) {
157 throw new \RuntimeException("Failed to generate signature. Malformed session-id or form-name.");
158 }
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);
163
164 }
165
166 }