/** * Derives authentication and encryption keys from the secret, using a slow * key derivation function if the secret is a password. * * @param string $salt * * @throws Ex\EnvironmentIsBrokenException * * @return DerivedKeys */ public function deriveKeys($salt) { if (Core::ourStrlen($salt) !== Core::SALT_BYTE_SIZE) { throw new Ex\EnvironmentIsBrokenException('Bad salt.'); } if ($this->secret_type === self::SECRET_TYPE_KEY) { $akey = Core::HKDF(Core::HASH_FUNCTION_NAME, $this->secret->getRawBytes(), Core::KEY_BYTE_SIZE, Core::AUTHENTICATION_INFO_STRING, $salt); $ekey = Core::HKDF(Core::HASH_FUNCTION_NAME, $this->secret->getRawBytes(), Core::KEY_BYTE_SIZE, Core::ENCRYPTION_INFO_STRING, $salt); return new DerivedKeys($akey, $ekey); } elseif ($this->secret_type === self::SECRET_TYPE_PASSWORD) { /* Our PBKDF2 polyfill is vulnerable to a DoS attack documented in * GitHub issue #230. The fix is to pre-hash the password to ensure * it is short. We do the prehashing here instead of in pbkdf2() so * that pbkdf2() still computes the function as defined by the * standard. */ $prehash = \hash(Core::HASH_FUNCTION_NAME, $this->secret, true); $prekey = Core::pbkdf2(Core::HASH_FUNCTION_NAME, $prehash, $salt, self::PBKDF2_ITERATIONS, Core::KEY_BYTE_SIZE, true); $akey = Core::HKDF(Core::HASH_FUNCTION_NAME, $prekey, Core::KEY_BYTE_SIZE, Core::AUTHENTICATION_INFO_STRING, $salt); /* Note the cryptographic re-use of $salt here. */ $ekey = Core::HKDF(Core::HASH_FUNCTION_NAME, $prekey, Core::KEY_BYTE_SIZE, Core::ENCRYPTION_INFO_STRING, $salt); return new DerivedKeys($akey, $ekey); } else { throw new Ex\EnvironmentIsBrokenException('Bad secret type.'); } }
/** * Convert a hexadecimal string into a binary string without cache-timing * leaks * * @param string $hex_string * @return string (raw binary) */ public static function hexToBin($hex_string) { $hex_pos = 0; $bin = ''; $hex_len = Core::ourStrlen($hex_string); $state = 0; $c_acc = 0; while ($hex_pos < $hex_len) { $c = \ord($hex_string[$hex_pos]); $c_num = $c ^ 48; $c_num0 = $c_num - 10 >> 8; $c_alpha = ($c & ~32) - 55; $c_alpha0 = ($c_alpha - 10 ^ $c_alpha - 16) >> 8; if (($c_num0 | $c_alpha0) === 0) { throw new \RangeException('Encoding::hexToBin() only expects hexadecimal characters'); } $c_val = $c_num0 & $c_num | $c_alpha & $c_alpha0; if ($state === 0) { $c_acc = $c_val * 16; } else { $bin .= \chr($c_acc | $c_val); } $state = $state ? 0 : 1; ++$hex_pos; } return $bin; }
public function testOurSubstrTrailingEmptyStringBugNormal() { // Same as above but with a non-weird string. $str = 'AAAAAAAAAAAAAAAA'; if (ini_get('mbstring.func_overload') == 7) { $this->assertSame(16, strlen($str)); } else { $this->assertSame(16, strlen($str)); } $this->assertSame(16, Core::ourStrlen($str)); $this->assertSame('', Core::ourSubstr($str, 16)); }
/** * Runs the runtime tests. * * @throws Ex\EnvironmentIsBrokenException */ public static function runtimeTest() { // 0: Tests haven't been run yet. // 1: Tests have passed. // 2: Tests are running right now. // 3: Tests have failed. static $test_state = 0; if ($test_state === 1 || $test_state === 2) { return; } if ($test_state === 3) { /* If an intermittent problem caused a test to fail previously, we * want that to be indicated to the user with every call to this * library. This way, if the user first does something they really * don't care about, and just ignores all exceptions, they won't get * screwed when they then start to use the library for something * they do care about. */ throw new Ex\EnvironmentIsBrokenException('Tests failed previously.'); } try { $test_state = 2; Core::ensureFunctionExists('openssl_get_cipher_methods'); if (\in_array(Core::CIPHER_METHOD, \openssl_get_cipher_methods()) === false) { throw new Ex\EnvironmentIsBrokenException('Cipher method not supported. This is normally caused by an outdated ' . 'version of OpenSSL (and/or OpenSSL compiled for FIPS compliance). ' . 'Please upgrade to a newer version of OpenSSL that supports ' . Core::CIPHER_METHOD . ' to use this library.'); } RuntimeTests::AESTestVector(); RuntimeTests::HMACTestVector(); RuntimeTests::HKDFTestVector(); RuntimeTests::testEncryptDecrypt(); if (Core::ourStrlen(Key::createNewRandomKey()->getRawBytes()) != Core::KEY_BYTE_SIZE) { throw new Ex\EnvironmentIsBrokenException(); } if (Core::ENCRYPTION_INFO_STRING == Core::AUTHENTICATION_INFO_STRING) { throw new Ex\EnvironmentIsBrokenException(); } } catch (Ex\EnvironmentIsBrokenException $ex) { // Do this, otherwise it will stay in the "tests are running" state. $test_state = 3; throw $ex; } // Change this to '0' make the tests always re-run (for benchmarking). $test_state = 1; }
/** * Decrypts a ciphertext to a string with either a key or a password. * * @param string $ciphertext * @param KeyOrPassword $secret * @param bool $raw_binary * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\WrongKeyOrModifiedCiphertextException * * @return string */ private static function decryptInternal($ciphertext, KeyOrPassword $secret, $raw_binary) { RuntimeTests::runtimeTest(); if (!$raw_binary) { try { $ciphertext = Encoding::hexToBin($ciphertext); } catch (Ex\BadFormatException $ex) { throw new Ex\WrongKeyOrModifiedCiphertextException('Ciphertext has invalid hex encoding.'); } } if (Core::ourStrlen($ciphertext) < Core::MINIMUM_CIPHERTEXT_SIZE) { throw new Ex\WrongKeyOrModifiedCiphertextException('Ciphertext is too short.'); } // Get and check the version header. $header = Core::ourSubstr($ciphertext, 0, Core::HEADER_VERSION_SIZE); if ($header !== Core::CURRENT_VERSION) { throw new Ex\WrongKeyOrModifiedCiphertextException('Bad version header.'); } // Get the salt. $salt = Core::ourSubstr($ciphertext, Core::HEADER_VERSION_SIZE, Core::SALT_BYTE_SIZE); if ($salt === false) { throw new Ex\EnvironmentIsBrokenException(); } // Get the IV. $iv = Core::ourSubstr($ciphertext, Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE, Core::BLOCK_BYTE_SIZE); if ($iv === false) { throw new Ex\EnvironmentIsBrokenException(); } // Get the HMAC. $hmac = Core::ourSubstr($ciphertext, Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE, Core::MAC_BYTE_SIZE); if ($hmac === false) { throw new Ex\EnvironmentIsBrokenException(); } // Get the actual encrypted ciphertext. $encrypted = Core::ourSubstr($ciphertext, Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + Core::BLOCK_BYTE_SIZE, Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE - Core::SALT_BYTE_SIZE - Core::BLOCK_BYTE_SIZE - Core::HEADER_VERSION_SIZE); if ($encrypted === false) { throw new Ex\EnvironmentIsBrokenException(); } // Derive the separate encryption and authentication keys from the key // or password, whichever it is. $keys = $secret->deriveKeys($salt); if (self::verifyHMAC($hmac, $header . $salt . $iv . $encrypted, $keys->getAuthenticationKey())) { $plaintext = self::plainDecrypt($encrypted, $keys->getEncryptionKey(), $iv, Core::CIPHER_METHOD); return $plaintext; } else { throw new Ex\WrongKeyOrModifiedCiphertextException('Integrity check failed.'); } }
public function testCreateNewRandomKey() { $key = Key::createNewRandomKey(); $this->assertSame(32, Core::ourStrlen($key->getRawBytes())); }
/** * INTERNAL USE ONLY: Decodes, verifies the header and checksum, and returns * the encoded byte string. * * @param string $expected_header * @param string $string * * @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException * @throws \Defuse\Crypto\Exception\BadFormatException * * @return string */ public static function loadBytesFromChecksummedAsciiSafeString($expected_header, $string) { // Headers must be a constant length to prevent one type's header from // being a prefix of another type's header, leading to ambiguity. if (Core::ourStrlen($expected_header) !== self::SERIALIZE_HEADER_BYTES) { throw new Ex\EnvironmentIsBrokenException('Header must be 4 bytes.'); } $bytes = Encoding::hexToBin($string); /* Make sure we have enough bytes to get the version header and checksum. */ if (Core::ourStrlen($bytes) < self::SERIALIZE_HEADER_BYTES + self::CHECKSUM_BYTE_SIZE) { throw new Ex\BadFormatException('Encoded data is shorter than expected.'); } /* Grab the version header. */ $actual_header = Core::ourSubstr($bytes, 0, self::SERIALIZE_HEADER_BYTES); if ($actual_header !== $expected_header) { throw new Ex\BadFormatException('Invalid header.'); } /* Grab the bytes that are part of the checksum. */ $checked_bytes = Core::ourSubstr($bytes, 0, Core::ourStrlen($bytes) - self::CHECKSUM_BYTE_SIZE); /* Grab the included checksum. */ $checksum_a = Core::ourSubstr($bytes, Core::ourStrlen($bytes) - self::CHECKSUM_BYTE_SIZE, self::CHECKSUM_BYTE_SIZE); /* Re-compute the checksum. */ $checksum_b = \hash(self::CHECKSUM_HASH_ALGO, $checked_bytes, true); /* Check if the checksum matches. */ if (!Core::hashEquals($checksum_a, $checksum_b)) { throw new Ex\BadFormatException("Data is corrupted, the checksum doesn't match"); } return Core::ourSubstr($bytes, self::SERIALIZE_HEADER_BYTES, Core::ourStrlen($bytes) - self::SERIALIZE_HEADER_BYTES - self::CHECKSUM_BYTE_SIZE); }
/** * Write to a stream; prevent partial writes * * @param resource $stream * @param string $buf * @param int $num (number of bytes) * @return string * @throws Ex\CannotPerformOperationException */ public static final function writeBytes($stream, $buf, $num = null) { $bufSize = Core::ourStrlen($buf); if ($num === null) { $num = $bufSize; } if ($num > $bufSize) { throw new Ex\CannotPerformOperationException('Trying to write more bytes than the buffer contains.'); } if ($num < 0) { throw new Ex\CannotPerformOperationException('Tried to write less than 0 bytes'); } $remaining = $num; while ($remaining > 0) { $written = \fwrite($stream, $buf, $remaining); if ($written === false) { throw new Ex\CannotPerformOperationException('Could not write to the file'); } $buf = Core::ourSubstr($buf, $written, null); $remaining -= $written; } return $num; }
/** * Decrypts a ciphertext (legacy -- before version tagging) * * $ciphertext is the ciphertext to decrypt. * $key is the key that the ciphertext was encrypted with. * You MUST catch exceptions thrown by this function. Read the docs. * * @param string $ciphertext * @param string $key * @return string * @throws Ex\CannotPerformOperationException * @throws Ex\CryptoTestFailedException * @throws Ex\InvalidCiphertextException */ public static function legacyDecrypt($ciphertext, $key) { RuntimeTests::runtimeTest(); $config = self::getVersionConfigFromHeader(Core::LEGACY_VERSION, Core::LEGACY_VERSION); // Extract the HMAC from the front of the ciphertext. if (Core::ourStrlen($ciphertext) <= $config->macByteSize()) { throw new Ex\InvalidCiphertextException("Ciphertext is too short."); } $hmac = Core::ourSubstr($ciphertext, 0, $config->macByteSize()); if ($hmac === false) { throw new Ex\CannotPerformOperationException(); } $ciphertext = Core::ourSubstr($ciphertext, $config->macByteSize()); if ($ciphertext === false) { throw new Ex\CannotPerformOperationException(); } // Regenerate the same authentication sub-key. $akey = Core::HKDF($config->hashFunctionName(), $key, $config->keyByteSize(), $config->authenticationInfoString(), null, $config); if (self::verifyHMAC($hmac, $ciphertext, $akey, $config)) { // Regenerate the same encryption sub-key. $ekey = Core::HKDF($config->hashFunctionName(), $key, $config->keyByteSize(), $config->encryptionInfoString(), null, $config); // Extract the initialization vector from the ciphertext. Core::EnsureFunctionExists("openssl_cipher_iv_length"); $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); if ($ivsize === false || $ivsize <= 0) { throw new Ex\CannotPerformOperationException("Could not get the IV length from OpenSSL"); } if (Core::ourStrlen($ciphertext) <= $ivsize) { throw new Ex\InvalidCiphertextException("Ciphertext is too short."); } $iv = Core::ourSubstr($ciphertext, 0, $ivsize); if ($iv === false) { throw new Ex\CannotPerformOperationException(); } $ciphertext = Core::ourSubstr($ciphertext, $ivsize); if ($ciphertext === false) { throw new Ex\CannotPerformOperationException(); } $plaintext = self::plainDecrypt($ciphertext, $ekey, $iv, $config); return $plaintext; } else { /* * We throw an exception instead of returning false because we want * a script that doesn't handle this condition to CRASH, instead * of thinking the ciphertext decrypted to the value false. */ throw new Ex\InvalidCiphertextException("Integrity check failed."); } }
public function getRawBytes() { if (is_null($this->key_bytes) || Core::ourStrlen($this->key_bytes) < self::MIN_SAFE_KEY_BYTE_SIZE) { throw new CannotPerformOperationException("An attempt was made to use an uninitialzied or too-short key"); } return $this->key_bytes; }