| 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: | ?> |