Merge pull request #19067 from eileenmcnaughton/weight
[civicrm-core.git] / CRM / Core / Key.php
CommitLineData
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 */
17class 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 /**
35 * The length of a generated signature/digest (expressed in hex digits).
36 * @var int
37 */
38 const HASH_LENGTH = 64;
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) {
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 *
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) {
92 // now generate a random number between 1 and 100K 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 /**
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
c9a7484e 124 if (!hash_equals($k, self::sign($name))) {
6a488035
TO
125 return NULL;
126 }
127 return $key;
128 }
129
a0ee3941 130 /**
49b215d2 131 * The original version of this function, added circa 2010 and untouched
132 * since then, seemed intended to check for a 32-digit hex string followed
133 * optionally by an underscore and 4-digit number. But it had a bug where
134 * the optional part was never checked ever. So have decided to remove that
135 * second check to keep it simple since it seems like pseudo-security.
136 *
137 * @param string $key
a0ee3941
EM
138 *
139 * @return bool
c9a7484e 140 * TRUE if the signature ($key) is well-formed.
a0ee3941 141 */
00be9182 142 public static function valid($key) {
c9a7484e
TO
143 // ensure that hash is a hex number (of expected length)
144 return preg_match('#[0-9a-f]{' . self::HASH_LENGTH . '}#i', $key) ? TRUE : FALSE;
145 }
146
147 /**
148 * @param string $name
149 * The name of the form
150 * @return string
151 * A signed digest of $name, computed with the per-session private key
152 */
153 private static function sign($name) {
154 $privateKey = self::privateKey();
155 $sessionID = self::sessionID();
156 $delim = chr(0);
157 if (strpos($sessionID, $delim) !== FALSE || strpos($name, $delim) !== FALSE) {
158 throw new \RuntimeException("Failed to generate signature. Malformed session-id or form-name.");
159 }
160 // Note: Unsure why $sessionID is included, but it's always been there, and it doesn't seem harmful.
161 return hash_hmac(self::HASH_ALGO, $sessionID . $delim . $name, $privateKey);
162
6a488035 163 }
96025800 164
6a488035 165}