public function crypt($password) { if (count($this->args) == 0) { $this->args[] = base64_encode(MWCryptRand::generate(16, true)); } if (function_exists('hash_pbkdf2')) { $hash = hash_pbkdf2($this->params['algo'], $password, base64_decode($this->args[0]), (int) $this->params['rounds'], (int) $this->params['length'], true); if (!is_string($hash)) { throw new PasswordError('Error when hashing password.'); } } else { $hashLenHash = hash($this->params['algo'], '', true); if (!is_string($hashLenHash)) { throw new PasswordError('Error when hashing password.'); } $hashLen = strlen($hashLenHash); $blockCount = ceil($this->params['length'] / $hashLen); $hash = ''; $salt = base64_decode($this->args[0]); for ($i = 1; $i <= $blockCount; ++$i) { $roundTotal = $lastRound = hash_hmac($this->params['algo'], $salt . pack('N', $i), $password, true); for ($j = 1; $j < $this->params['rounds']; ++$j) { $lastRound = hash_hmac($this->params['algo'], $lastRound, $password, true); $roundTotal ^= $lastRound; } $hash .= $roundTotal; } $hash = substr($hash, 0, $this->params['length']); } $this->hash = base64_encode($hash); }
/** * Updates the underlying hash by encrypting it with the newest secret. * * @throws MWException If the configuration is not valid * @return bool True if the password was updated */ public function update() { if (count($this->args) != 2 || $this->params == $this->getDefaultParams()) { // Hash does not need updating return false; } // Decrypt the underlying hash $underlyingHash = openssl_decrypt(base64_decode($this->args[1]), $this->params['cipher'], $this->config['secrets'][$this->params['secret']], 0, base64_decode($this->args[0])); // Reset the params $this->params = $this->getDefaultParams(); // Check the key size with the new params $iv = MWCryptRand::generate(openssl_cipher_iv_length($this->params['cipher']), true); $this->hash = base64_encode(openssl_encrypt($underlyingHash, $this->params['cipher'], $this->config['secrets'][$this->params['secret']], 0, $iv)); $this->args = array(base64_encode($iv)); return true; }
/** * @param string $password Password to encrypt * * @throws PasswordError If bcrypt has an unknown error * @throws MWException If bcrypt is not supported by PHP */ public function crypt($password) { if (!defined('CRYPT_BLOWFISH')) { throw new MWException('Bcrypt is not supported.'); } // Either use existing hash or make a new salt // Bcrypt expects 22 characters of base64-encoded salt // Note: bcrypt does not use MIME base64. It uses its own base64 without any '=' padding. // It expects a 128 bit salt, so it will ignore anything after the first 128 bits if (!isset($this->args[0])) { $this->args[] = substr(strtr(base64_encode(MWCryptRand::generate(16, true)), '+', '.'), 0, 22); } $hash = crypt($password, sprintf('$2y$%02d$%s', (int) $this->params['rounds'], $this->args[0])); if (!is_string($hash) || strlen($hash) <= 13) { throw new PasswordError('Error when hashing password.'); } // Strip the $2y$ $parts = explode($this->getDelimiter(), substr($hash, 4)); $this->params['rounds'] = (int) $parts[0]; $this->args[0] = substr($parts[1], 0, 22); $this->hash = substr($parts[1], 22); }
/** * Set a value in the session, encrypted * * This relies on the secrecy of $wgSecretKey (by default), or $wgSessionSecret. * * @param string|int $key * @param mixed $value */ public function setSecret($key, $value) { global $wgSessionInsecureSecrets; list($encKey, $hmacKey) = $this->getSecretKeys(); $serialized = serialize($value); // The code for encryption (with OpenSSL) and sealing is taken from // Chris Steipp's OATHAuthUtils class in Extension::OATHAuth. // Encrypt // @todo: import a pure-PHP library for AES instead of doing $wgSessionInsecureSecrets $iv = \MWCryptRand::generate(16, true); if (function_exists('openssl_encrypt')) { $ciphertext = openssl_encrypt($serialized, 'aes-256-ctr', $encKey, OPENSSL_RAW_DATA, $iv); if ($ciphertext === false) { throw new UnexpectedValueException('Encryption failed: ' . openssl_error_string()); } } elseif (function_exists('mcrypt_encrypt')) { $ciphertext = mcrypt_encrypt('rijndael-128', $encKey, $serialized, 'ctr', $iv); if ($ciphertext === false) { throw new UnexpectedValueException('Encryption failed'); } } elseif ($wgSessionInsecureSecrets) { $ex = new \Exception('No encryption is available, storing data as plain text'); $this->logger->warning($ex->getMessage(), ['exception' => $ex]); $ciphertext = $serialized; } else { throw new \BadMethodCallException('Encryption is not available. You really should install the PHP OpenSSL extension, ' . 'or failing that the mcrypt extension. But if you really can\'t and you\'re willing ' . 'to accept insecure storage of sensitive session data, set ' . '$wgSessionInsecureSecrets = true in LocalSettings.php to make this exception go away.'); } // Seal $sealed = base64_encode($iv) . '.' . base64_encode($ciphertext); $hmac = hash_hmac('sha256', $sealed, $hmacKey, true); $encrypted = base64_encode($hmac) . '.' . $sealed; // Store $this->set($key, $encrypted); }
/** * MW specific salt, cached from last run * @return string Binary string */ protected function getSaltUsingCache() { if ($this->salt == '') { $lastSalt = $this->cache->get($this->cacheKey); if ($lastSalt === false) { // If we don't have a previous value to use as our salt, we use // 16 bytes from MWCryptRand, which will use a small amount of // entropy from our pool. Note, "XTR may be deterministic or keyed // via an optional “salt value” (i.e., a non-secret random // value)..." - http://eprint.iacr.org/2010/264.pdf. However, we // use a strongly random value since we can. $lastSalt = MWCryptRand::generate(16); } // Get a binary string that is hashLen long $this->salt = hash($this->algorithm, $lastSalt, true); } return $this->salt; }
/** * Set a value in the session, encrypted * * This relies on the secrecy of $wgSecretKey (by default), or $wgSessionSecret. * * @param string|int $key * @param mixed $value */ public function setSecret($key, $value) { list($encKey, $hmacKey) = $this->getSecretKeys(); $serialized = serialize($value); // The code for encryption (with OpenSSL) and sealing is taken from // Chris Steipp's OATHAuthUtils class in Extension::OATHAuth. // Encrypt // @todo: import a pure-PHP library for AES instead of doing $wgSessionInsecureSecrets $iv = \MWCryptRand::generate(16, true); $algorithm = self::getEncryptionAlgorithm(); switch ($algorithm[0]) { case 'openssl': $ciphertext = openssl_encrypt($serialized, $algorithm[1], $encKey, OPENSSL_RAW_DATA, $iv); if ($ciphertext === false) { throw new \UnexpectedValueException('Encryption failed: ' . openssl_error_string()); } break; case 'mcrypt': // PKCS7 padding $blocksize = mcrypt_get_block_size($algorithm[1], $algorithm[2]); $pad = $blocksize - strlen($serialized) % $blocksize; $serialized .= str_repeat(chr($pad), $pad); $ciphertext = mcrypt_encrypt($algorithm[1], $encKey, $serialized, $algorithm[2], $iv); if ($ciphertext === false) { throw new \UnexpectedValueException('Encryption failed'); } break; case 'insecure': $ex = new \Exception('No encryption is available, storing data as plain text'); $this->logger->warning($ex->getMessage(), ['exception' => $ex]); $ciphertext = $serialized; break; default: throw new \LogicException('invalid algorithm'); } // Seal $sealed = base64_encode($iv) . '.' . base64_encode($ciphertext); $hmac = hash_hmac('sha256', $sealed, $hmacKey, true); $encrypted = base64_encode($hmac) . '.' . $sealed; // Store $this->set($key, $encrypted); }
public function testSecrets() { $logger = new \TestLogger(); $session = TestUtils::getDummySession(null, -1, $logger); // Simple defaulting $this->assertEquals('defaulted', $session->getSecret('test', 'defaulted')); // Bad encrypted data $session->set('test', 'foobar'); $logger->setCollect(true); $this->assertEquals('defaulted', $session->getSecret('test', 'defaulted')); $logger->setCollect(false); $this->assertSame([[LogLevel::WARNING, 'Invalid sealed-secret format']], $logger->getBuffer()); $logger->clearBuffer(); // Tampered data $session->setSecret('test', 'foobar'); $encrypted = $session->get('test'); $session->set('test', $encrypted . 'x'); $logger->setCollect(true); $this->assertEquals('defaulted', $session->getSecret('test', 'defaulted')); $logger->setCollect(false); $this->assertSame([[LogLevel::WARNING, 'Sealed secret has been tampered with, aborting.']], $logger->getBuffer()); $logger->clearBuffer(); // Unserializable data $iv = \MWCryptRand::generate(16, true); list($encKey, $hmacKey) = \TestingAccessWrapper::newFromObject($session)->getSecretKeys(); $ciphertext = openssl_encrypt('foobar', 'aes-256-ctr', $encKey, OPENSSL_RAW_DATA, $iv); $sealed = base64_encode($iv) . '.' . base64_encode($ciphertext); $hmac = hash_hmac('sha256', $sealed, $hmacKey, true); $encrypted = base64_encode($hmac) . '.' . $sealed; $session->set('test', $encrypted); \MediaWiki\suppressWarnings(); $this->assertEquals('defaulted', $session->getSecret('test', 'defaulted')); \MediaWiki\restoreWarnings(); }