Commit | Line | Data |
---|---|---|
6a488035 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
bc77d7c0 | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
6a488035 | 5 | | | |
bc77d7c0 TO |
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 | | |
6a488035 | 9 | +--------------------------------------------------------------------+ |
d25dd0ee | 10 | */ |
6a488035 TO |
11 | |
12 | /** | |
13 | * | |
14 | * @package CRM | |
ca5cec67 | 15 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
6a488035 TO |
16 | */ |
17 | class CRM_Core_Key { | |
92a8ec76 TO |
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 | ||
c9a7484e TO |
28 | /** |
29 | * @var string | |
30 | * @see hash_hmac_algos() | |
31 | */ | |
32 | const HASH_ALGO = 'sha256'; | |
33 | ||
34 | /** | |
40b1007a | 35 | * The minimum length of a generated signature/digest (expressed in base36 digits). |
c9a7484e TO |
36 | * @var int |
37 | */ | |
40b1007a | 38 | const HASH_LENGTH = 25; |
c9a7484e | 39 | |
518fa0ee | 40 | public static $_key = NULL; |
6a488035 | 41 | |
518fa0ee | 42 | public static $_sessionID = NULL; |
6a488035 TO |
43 | |
44 | /** | |
fe482240 | 45 | * Generate a private key per session and store in session. |
6a488035 | 46 | * |
a6c01b45 CW |
47 | * @return string |
48 | * private key for this session | |
6a488035 | 49 | */ |
00be9182 | 50 | public static function privateKey() { |
6a488035 TO |
51 | if (!self::$_key) { |
52 | $session = CRM_Core_Session::singleton(); | |
53 | self::$_key = $session->get('qfPrivateKey'); | |
54 | if (!self::$_key) { | |
92a8ec76 | 55 | self::$_key = base64_encode(random_bytes(self::PRIVATE_KEY_LENGTH)); |
6a488035 TO |
56 | $session->set('qfPrivateKey', self::$_key); |
57 | } | |
58 | } | |
59 | return self::$_key; | |
60 | } | |
61 | ||
a0ee3941 EM |
62 | /** |
63 | * @return mixed|null|string | |
64 | */ | |
00be9182 | 65 | public static function sessionID() { |
6a488035 TO |
66 | if (!self::$_sessionID) { |
67 | $session = CRM_Core_Session::singleton(); | |
68 | self::$_sessionID = $session->get('qfSessionID'); | |
69 | if (!self::$_sessionID) { | |
4d4d87f1 | 70 | self::$_sessionID = CRM_Core_Config::singleton()->userSystem->getSessionId(); |
6a488035 TO |
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 | * | |
c490a46a | 81 | * @param string $name |
6a0b768e TO |
82 | * @param bool $addSequence |
83 | * Should we add a unique sequence number to the end of the key. | |
6a488035 | 84 | * |
a6c01b45 CW |
85 | * @return string |
86 | * valid formID | |
6a488035 | 87 | */ |
00be9182 | 88 | public static function get($name, $addSequence = FALSE) { |
c9a7484e | 89 | $key = self::sign($name); |
6a488035 TO |
90 | |
91 | if ($addSequence) { | |
b4355b4f | 92 | // now generate a random number between 1 and 10000 and add it to the key |
6a488035 TO |
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 | /** | |
fe482240 | 100 | * Validate a form key based on the form name. |
6a488035 | 101 | * |
c490a46a | 102 | * @param string $key |
6a488035 | 103 | * @param string $name |
77b97be7 EM |
104 | * @param bool $addSequence |
105 | * | |
a6c01b45 CW |
106 | * @return string |
107 | * if valid, else null | |
6a488035 | 108 | */ |
00be9182 | 109 | public static function validate($key, $name, $addSequence = FALSE) { |
6a488035 TO |
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 | ||
40b1007a TO |
124 | $expected = self::sign($name); |
125 | if (!hash_equals($k, $expected)) { | |
6a488035 TO |
126 | return NULL; |
127 | } | |
128 | return $key; | |
129 | } | |
130 | ||
a0ee3941 | 131 | /** |
b4355b4f | 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. | |
49b215d2 | 134 | * |
135 | * @param string $key | |
a0ee3941 EM |
136 | * |
137 | * @return bool | |
c9a7484e | 138 | * TRUE if the signature ($key) is well-formed. |
a0ee3941 | 139 | */ |
00be9182 | 140 | public static function valid($key) { |
b4355b4f | 141 | // ensure that key is an alphanumeric string of at least HASH_LENGTH with |
142 | // an optional underscore+digits at the end. | |
40b1007a | 143 | return preg_match('#^[0-9a-zA-Z]{' . self::HASH_LENGTH . ',}+(_\d+)?$#', $key) ? TRUE : FALSE; |
c9a7484e TO |
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 | } | |
40b1007a TO |
159 | // The "prefix" gives some advisory details to help with debugging. |
160 | $prefix = preg_replace('/[^a-zA-Z0-9]/', '', $name); | |
c9a7484e | 161 | // Note: Unsure why $sessionID is included, but it's always been there, and it doesn't seem harmful. |
40b1007a | 162 | return $prefix . base_convert(hash_hmac(self::HASH_ALGO, $sessionID . $delim . $name, $privateKey), 16, 36); |
c9a7484e | 163 | |
6a488035 | 164 | } |
96025800 | 165 | |
6a488035 | 166 | } |