/** * 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; }
/** * 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.'); } }
/** * @expectedException \Defuse\Crypto\Exception\BadFormatException * @expectedExceptionMessage not a hex string */ public function testBadHexEncoding() { $header = Core::secureRandom(Core::HEADER_VERSION_SIZE); $str = Encoding::saveBytesToChecksummedAsciiSafeString($header, Core::secureRandom(Core::KEY_BYTE_SIZE)); $str[0] = 'Z'; Encoding::loadBytesFromChecksummedAsciiSafeString($header, $str); }
public function __construct($config_array) { $expected_keys = array("cipher_method", "block_byte_size", "key_byte_size", "salt_byte_size", "mac_byte_size", "hash_function_name", "encryption_info_string", "authentication_info_string"); if (sort($expected_keys) !== true) { throw Ex\CannotPerformOperationException("sort() failed."); } $actual_keys = array_keys($config_array); if (sort($actual_keys) !== true) { throw Ex\CannotPerformOperationException("sort() failed."); } if ($expected_keys !== $actual_keys) { throw new Ex\CannotPerformOperationException("Trying to instantiate a bad configuration."); } $this->cipher_method = $config_array["cipher_method"]; $this->block_byte_size = $config_array["block_byte_size"]; $this->key_byte_size = $config_array["key_byte_size"]; $this->salt_byte_size = $config_array["salt_byte_size"]; $this->mac_byte_size = $config_array["mac_byte_size"]; $this->hash_function_name = $config_array["hash_function_name"]; $this->encryption_info_string = $config_array["encryption_info_string"]; $this->authentication_info_string = $config_array["authentication_info_string"]; Core::ensureFunctionExists('openssl_get_cipher_methods'); if (\in_array($this->cipher_method, \openssl_get_cipher_methods()) === false) { throw new Ex\CannotPerformOperationException("Configuration contains an invalid OpenSSL cipher method."); } if (!\is_int($this->block_byte_size) || $this->block_byte_size <= 0) { throw new Ex\CannotPerformOperationException("Configuration contains an invalid block byte size."); } if (!\is_int($this->key_byte_size) || $this->key_byte_size <= 0) { throw new Ex\CannotPerformOperationException("Configuration contains an invalid key byte size."); } if ($this->salt_byte_size !== false) { if (!is_int($this->salt_byte_size) || $this->salt_byte_size <= 0) { throw new Ex\CannotPerformOperationException("Configuration contains an invalid salt byte size."); } } if (!\is_int($this->mac_byte_size) || $this->mac_byte_size <= 0) { throw new Ex\CannotPerformOperationException("Configuration contains an invalid MAC byte size."); } if (\in_array($this->hash_function_name, \hash_algos()) === false) { throw new Ex\CannotPerformOperationException("Configuration contains an invalid hash function name."); } if (!\is_string($this->encryption_info_string) || $this->encryption_info_string === "") { throw new Ex\CannotPerformOperationException("Configuration contains an invalid encryption info string."); } if (!\is_string($this->authentication_info_string) || $this->authentication_info_string === "") { throw new Ex\CannotPerformOperationException("Configuration contains an invalid authentication info string."); } }
public function testOurSubstrOutOfBorders() { // See: https://secure.php.net/manual/en/function.mb-substr.php#50275 // We want to be like substr, so confirm that behavior. $this->assertSame(false, substr('abc', 5, 2)); // Confirm that mb_substr does not have that behavior. if (function_exists('mb_substr')) { if (ini_get('mbstring.func_overload') == 0) { $this->assertSame('', \mb_substr('abc', 5, 2)); } else { $this->assertSame(false, \mb_substr('abc', 5, 2)); } // YES, THE BEHAVIOR OF mb_substr IS REALLY THIS INSANE!!!! } // Check if we actually have that behavior. $this->assertSame(false, Core::ourSubstr('abc', 5, 2)); }
/** * Verifies an HMAC without leaking information through side-channels. * * @param string $correct_hmac * @param string $message * @param string $key * * @throws Ex\EnvironmentIsBrokenException * * @return bool */ protected static function verifyHMAC($correct_hmac, $message, $key) { $message_hmac = \hash_hmac(Core::HASH_FUNCTION_NAME, $message, $key, true); return Core::hashEquals($correct_hmac, $message_hmac); }
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); }
/** * Test HKDF against test vectors. * * @throws Ex\EnvironmentIsBrokenException */ private static function HKDFTestVector() { // HKDF test vectors from RFC 5869 // Test Case 1 $ikm = \str_repeat("\v", 22); $salt = Encoding::hexToBin('000102030405060708090a0b0c'); $info = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9'); $length = 42; $okm = Encoding::hexToBin('3cb25f25faacd57a90434f64d0362f2a' . '2d2d0a90cf1a5a4c5db02d56ecc4c5bf' . '34007208d5b887185865'); $computed_okm = Core::HKDF('sha256', $ikm, $length, $info, $salt); if ($computed_okm !== $okm) { throw new Ex\EnvironmentIsBrokenException(); } // Test Case 7 $ikm = \str_repeat("\f", 22); $length = 42; $okm = Encoding::hexToBin('2c91117204d745f3500d636a62f64f0a' . 'b3bae548aa53d423b0d1f27ebba6f5e5' . '673a081d70cce7acfc48'); $computed_okm = Core::HKDF('sha1', $ikm, $length, '', null); if ($computed_okm !== $okm) { throw new Ex\EnvironmentIsBrokenException(); } }
/** * 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; }
/** * Get the encryption configuration based on the version in a header. * * @param string $header The header to read the version number from. * @param string $min_ver_header The header of the minimum version number allowed. * @return array * @throws Ex\InvalidCiphertextException */ public static function getVersionConfigFromHeader($header, $min_ver_header) { if (Core::ourSubstr($header, 0, 2) !== Core::ourSubstr(Core::HEADER_MAGIC, 0, 2)) { throw new Ex\InvalidCiphertextException("Ciphertext has a bad magic number."); } $major = \ord($header[2]); $minor = \ord($header[3]); $min_major = \ord($min_ver_header[2]); $min_minor = \ord($min_ver_header[3]); if ($major < $min_major || $major === $min_major && $minor < $min_minor) { throw new Ex\InvalidCiphertextException("Ciphertext is requesting an insecure fallback."); } $config = self::getVersionConfigFromMajorMinor($major, $minor); return $config; }
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; }