1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2014-2025
6: *
7: * This program is free software; you can redistribute it and/or
8: * modify it under the terms of the GNU General Public
9: * License as published by the Free Software Foundation; either
10: * version 2 of the License, or (at your option) any later version.
11: *
12: * This program is distributed in the hope that it will be useful,
13: * but WITHOUT ANY WARRANTY; without even the implied warranty of
14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15: * General Public License for more details.
16: *
17: * You should have received a copy of the GNU General Public
18: * License along with this program; if not, write to the Free
19: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20: *
21: */
22:
23: namespace SimpleID\Protocols\OpenID;
24:
25: use \SimpleID\Crypt\BigNum;
26: use \SimpleID\Crypt\Random;
27:
28: /**
29: * OpenID default modulus for Diffie-Hellman key exchange.
30: *
31: * @link http://openid.net/specs/openid-authentication-1_1.html#pvalue, http://openid.net/specs/openid-authentication-2_0.html#pvalue
32: */
33: define('OPENID_DH_DEFAULT_MOD', '155172898181473697471232257763715539915724801'.
34: '966915404479707795314057629378541917580651227423698188993727816152646631'.
35: '438561595825688188889951272158842675419950341258706556549803580104870537'.
36: '681476726513255747040765857479291291572334510643245094715007229621094194'.
37: '349783925984760375594985848253359305585439638443');
38:
39: /**
40: * OpenID default generator for Diffie-Hellman key exchange.
41: */
42: define('OPENID_DH_DEFAULT_GEN', '2');
43:
44: /**
45: * A class for Diffie-Hellman key exchange.
46: */
47: class DiffieHellman {
48:
49: /** @var BigNum the private key */
50: private $x;
51:
52: /** @var BigNum the public key */
53: private $y;
54:
55: /** @var BigNum the modulus - a large prime number */
56: protected $p;
57:
58: /** @var BigNum the generator - a primitive root modulo */
59: protected $g;
60:
61: /** @var string the hashing algorithm */
62: protected $algo;
63:
64: /**
65: * Creates a new instance.
66: *
67: * The modulus and generator are specified in the $dh_modulus and $dh_gen
68: * parameters. If these are set to NULL, the default from the OpenID
69: * specification are used.
70: *
71: * @param string $dh_modulus modulus
72: * @param string $dh_gen generator
73: * @param string $algo the hashing algorithm
74: */
75: function __construct($dh_modulus = NULL, $dh_gen = NULL, $algo = 'sha1') {
76: if ($dh_modulus != NULL) {
77: $this->p = new BigNum(base64_decode($dh_modulus), 256);
78: } else {
79: $this->p = new BigNum(OPENID_DH_DEFAULT_MOD);
80: }
81:
82: if ($dh_gen != NULL) {
83: $this->g = new BigNum(base64_decode($dh_gen), 256);
84: } else {
85: $this->g = new BigNum(OPENID_DH_DEFAULT_GEN);
86: }
87:
88: $this->algo = $algo;
89:
90: $this->generateKeyPair();
91: }
92:
93:
94: /**
95: * Generates the cryptographic values required for responding to association
96: * requests
97: *
98: * This involves generating a key pair for the OpenID provider, then calculating
99: * the shared secret. The shared secret is then used to encrypt the MAC key.
100: *
101: * @param string $mac_key the MAC key, in binary representation
102: * @param string $dh_consumer_public the consumer's public key, in Base64 representation
103: * @return array<string, string> an array containing (a) dh_server_public - the server's public key (in Base64), and (b)
104: * enc_mac_key encrypted MAC key (in Base64), encrypted using the Diffie-Hellman shared secret
105: */
106: public function associateAsServer($mac_key, $dh_consumer_public) {
107: // Generate the shared secret
108: $ZZ = $this->getSharedSecret($dh_consumer_public);
109:
110: return [
111: 'dh_server_public' => $this->getPublicKey(),
112: 'enc_mac_key' => $this->cryptMACKey($ZZ, $mac_key)
113: ];
114: }
115:
116: /**
117: * Complete association by obtaining the session MAC key from the key obtained
118: * from the Diffie-Hellman key exchange
119: *
120: * @param string $enc_mac_key the encrypted session MAC key, in Base64 represnetation
121: * @param string $dh_server_public the server's public key, in Base64 representation
122: * @return string the decrypted session MAC key, in Base64 representation
123: */
124: public function associateAsConsumer($enc_mac_key, $dh_server_public) {
125: // Retrieve the shared secret
126: $ZZ = $this->getSharedSecret($dh_server_public);
127:
128: // Decode the encrypted MAC key
129: $encrypted_mac_key = base64_decode($enc_mac_key);
130:
131: return $this->cryptMACKey($ZZ, $encrypted_mac_key);
132: }
133:
134: /**
135: * Returns the public key.
136: *
137: * @return string the public key in Base64
138: */
139: public function getPublicKey() {
140: $key = $this->y->val(256);
141: assert($key != false);
142: return base64_encode($key);
143: }
144:
145: /**
146: * Calculates the shared secret for Diffie-Hellman key exchange.
147: *
148: * This is the second step in the Diffle-Hellman key exchange process. The other
149: * party (in OpenID 1.0 terms, the consumer) has already generated the public
150: * key ($dh_consumer_public) and sent it to this party (the server).
151: *
152: * @param string $their_public the other party's public key, in Base64 representation
153: * @return BigNum the shared secret
154: *
155: * @see generateKeyPair()
156: * @link http://www.ietf.org/rfc/rfc2631.txt RFC 2631
157: */
158: protected function getSharedSecret($their_public) {
159: // Decode the keys
160: $their_y = new BigNum(base64_decode($their_public), 256);
161:
162: // Generate the shared secret = their public ^ my private mod p = my public ^ their private mod p
163: $ZZ = $their_y->powmod($this->x, $this->p);
164:
165: return $ZZ;
166: }
167:
168: /**
169: * Encrypts/decrypts and encodes the MAC key.
170: *
171: * @param BigNum $ZZ the Diffie-Hellman key exchange shared secret as a bignum
172: * @param string $mac_key a byte stream containing the MAC key
173: * @return string the encrypted MAC key in Base64 representation
174: */
175: protected function cryptMACKey($ZZ, $mac_key) {
176: // Encrypt/decrypt the MAC key using the shared secret and the hash function
177: $encrypted_mac_key = $this->xorCrypt($ZZ, $mac_key);
178:
179: // Encode the encrypted/decrypted MAC key
180: $enc_mac_key = base64_encode($encrypted_mac_key);
181:
182: return $enc_mac_key;
183: }
184:
185: /**
186: * Encrypts/decrypts using XOR.
187: *
188: * @param BigNum $key the encryption key. This is usually
189: * the shared secret (ZZ) calculated from the Diffie-Hellman key exchange
190: * @param string $plain_cipher the plaintext or ciphertext
191: * @return string the ciphertext or plaintext
192: */
193: protected function xorCrypt($key, $plain_cipher) {
194: $keystream = $key->val(256);
195: assert($keystream != false);
196: $hashed_key = hash($this->algo, $keystream, true);
197:
198: $cipher_plain = "";
199: for ($i = 0; $i < strlen($plain_cipher); $i++) {
200: $cipher_plain .= chr(ord($plain_cipher[$i]) ^ ord($hashed_key[$i]));
201: }
202:
203: return $cipher_plain;
204: }
205:
206: /**
207: * Generates a key pair for Diffie-Hellman key exchange.
208: *
209: * @return void
210: */
211: private function generateKeyPair() {
212: // Generate the private key - a random number which is less than p
213: $rand = $this->generateRandom($this->p);
214: $this->x = $rand->add(new BigNum(1));
215:
216: // Calculate the public key is g ^ private mod p
217: $this->y = $this->g->powmod($this->x, $this->p);
218: }
219:
220: /**
221: * Generates a random integer, which will be used to derive a private key
222: * for Diffie-Hellman key exchange. The integer must be less than $stop
223: *
224: * @param BigNum $stop a prime number as a bignum
225: * @return BigNum the random integer as a bignum
226: */
227: private function generateRandom($stop) {
228: static $duplicate_cache = [];
229: $rand = new Random();
230:
231: // Used as the key for the duplicate cache
232: $rbytes = $stop->val(256);
233: assert($rbytes != false);
234:
235: if (array_key_exists($rbytes, $duplicate_cache)) {
236: list($duplicate, $nbytes) = $duplicate_cache[$rbytes];
237: } else {
238: if ($rbytes[0] == "\x00") {
239: $nbytes = strlen($rbytes) - 1;
240: } else {
241: $nbytes = strlen($rbytes);
242: }
243:
244: $mxrand = new BigNum(256);
245:
246: // If we get a number less than this, then it is in the
247: // duplicated range.
248: $duplicate = $mxrand->powmod(new BigNum($nbytes), $stop);
249:
250: if (count($duplicate_cache) > 10) {
251: $duplicate_cache = [];
252: }
253:
254: $duplicate_cache[$rbytes] = [ $duplicate, $nbytes ];
255: }
256:
257: do {
258: $bytes = "\x00" . $rand->bytes($nbytes);
259: $n = new BigNum($bytes, 256);
260: // Keep looping if this value is in the low duplicated range
261: } while ($n->cmp($duplicate) < 0);
262:
263: return $n->mod($stop);
264: }
265: }
266:
267: ?>