[Ref] Cleanup on SelectValues::contributeTokens
[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 /**
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) {
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) {
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}