From 32e3999378a7f651732183248baeb1e96bfa74f7 Mon Sep 17 00:00:00 2001 From: Netkas Date: Sun, 26 Jun 2022 09:50:42 -0400 Subject: [PATCH] Added third-party dependency 'defuse\php-encryption' --- src/installer/installer | 6 + .../ThirdParty/defuse/php-encryption/Core.php | 457 ++++++++++ .../defuse/php-encryption/Crypto.php | 439 ++++++++++ .../defuse/php-encryption/DerivedKeys.php | 50 ++ .../defuse/php-encryption/Encoding.php | 269 ++++++ .../Exception/BadFormatException.php | 7 + .../Exception/CryptoException.php | 7 + .../EnvironmentIsBrokenException.php | 7 + .../php-encryption/Exception/IOException.php | 7 + .../WrongKeyOrModifiedCiphertextException.php | 7 + .../ThirdParty/defuse/php-encryption/File.php | 778 ++++++++++++++++++ .../ThirdParty/defuse/php-encryption/Key.php | 94 +++ .../defuse/php-encryption/KeyOrPassword.php | 149 ++++ .../php-encryption/KeyProtectedByPassword.php | 145 ++++ .../defuse/php-encryption/RuntimeTests.php | 228 +++++ .../ThirdParty/defuse/php-encryption/VERSION | 1 + src/ncc/autoload.php | 19 +- 17 files changed, 2669 insertions(+), 1 deletion(-) create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/Core.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/Crypto.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/DerivedKeys.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/Encoding.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/Exception/BadFormatException.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/Exception/CryptoException.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/Exception/EnvironmentIsBrokenException.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/Exception/IOException.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/Exception/WrongKeyOrModifiedCiphertextException.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/File.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/Key.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/KeyOrPassword.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/KeyProtectedByPassword.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/RuntimeTests.php create mode 100644 src/ncc/ThirdParty/defuse/php-encryption/VERSION diff --git a/src/installer/installer b/src/installer/installer index 228b449..391109f 100644 --- a/src/installer/installer +++ b/src/installer/installer @@ -9,3 +9,9 @@ # Dependency: # PHP 8.0+ # ------------------------------------------------------------------ + +$NCC_INSTALL_PATH='/etc/ncc' +$NCC_SOURCE_PATH='src/ncc' +$NCC_VERSION_NUMBER='1.0.0' +$NCC_UPDATE_SOURCE='https://updates.nosial.com/ncc/check_updates?current_version=$VERSION' +$NCC_CREATE_SYSTEM='True' diff --git a/src/ncc/ThirdParty/defuse/php-encryption/Core.php b/src/ncc/ThirdParty/defuse/php-encryption/Core.php new file mode 100644 index 0000000..8eba7bf --- /dev/null +++ b/src/ncc/ThirdParty/defuse/php-encryption/Core.php @@ -0,0 +1,457 @@ + 0, + 'Trying to increment a nonce by a nonpositive amount' + ); + + Core::ensureTrue( + $inc <= PHP_INT_MAX - 255, + 'Integer overflow may occur' + ); + + /* + * We start at the rightmost byte (big-endian) + * So, too, does OpenSSL: http://stackoverflow.com/a/3146214/2224584 + */ + for ($i = Core::BLOCK_BYTE_SIZE - 1; $i >= 0; --$i) { + $sum = \ord($ctr[$i]) + $inc; + + /* Detect integer overflow and fail. */ + Core::ensureTrue(\is_int($sum), 'Integer overflow in CTR mode nonce increment'); + + $ctr[$i] = \pack('C', $sum & 0xFF); + $inc = $sum >> 8; + } + return $ctr; + } + + /** + * Returns a random byte string of the specified length. + * + * @param int $octets + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string + */ + public static function secureRandom($octets) + { + self::ensureFunctionExists('random_bytes'); + try { + return \random_bytes($octets); + } catch (\Exception $ex) { + throw new Ex\EnvironmentIsBrokenException( + 'Your system does not have a secure random number generator.' + ); + } + } + + /** + * Computes the HKDF key derivation function specified in + * http://tools.ietf.org/html/rfc5869. + * + * @param string $hash Hash Function + * @param string $ikm Initial Keying Material + * @param int $length How many bytes? + * @param string $info What sort of key are we deriving? + * @param string $salt + * + * @throws Ex\EnvironmentIsBrokenException + * @psalm-suppress UndefinedFunction - We're checking if the function exists first. + * + * @return string + */ + public static function HKDF($hash, $ikm, $length, $info = '', $salt = null) + { + static $nativeHKDF = null; + if ($nativeHKDF === null) { + $nativeHKDF = \is_callable('\\hash_hkdf'); + } + if ($nativeHKDF) { + if (\is_null($salt)) { + $salt = ''; + } + return \hash_hkdf($hash, $ikm, $length, $info, $salt); + } + + $digest_length = Core::ourStrlen(\hash_hmac($hash, '', '', true)); + + // Sanity-check the desired output length. + Core::ensureTrue( + !empty($length) && \is_int($length) && $length >= 0 && $length <= 255 * $digest_length, + 'Bad output length requested of HDKF.' + ); + + // "if [salt] not provided, is set to a string of HashLen zeroes." + if (\is_null($salt)) { + $salt = \str_repeat("\x00", $digest_length); + } + + // HKDF-Extract: + // PRK = HMAC-Hash(salt, IKM) + // The salt is the HMAC key. + $prk = \hash_hmac($hash, $ikm, $salt, true); + + // HKDF-Expand: + + // This check is useless, but it serves as a reminder to the spec. + Core::ensureTrue(Core::ourStrlen($prk) >= $digest_length); + + // T(0) = '' + $t = ''; + $last_block = ''; + for ($block_index = 1; Core::ourStrlen($t) < $length; ++$block_index) { + // T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??) + $last_block = \hash_hmac( + $hash, + $last_block . $info . \chr($block_index), + $prk, + true + ); + // T = T(1) | T(2) | T(3) | ... | T(N) + $t .= $last_block; + } + + // ORM = first L octets of T + /** @var string $orm */ + $orm = Core::ourSubstr($t, 0, $length); + Core::ensureTrue(\is_string($orm)); + return $orm; + } + + /** + * Checks if two equal-length strings are the same without leaking + * information through side channels. + * + * @param string $expected + * @param string $given + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return bool + */ + public static function hashEquals($expected, $given) + { + static $native = null; + if ($native === null) { + $native = \function_exists('hash_equals'); + } + if ($native) { + return \hash_equals($expected, $given); + } + + // We can't just compare the strings with '==', since it would make + // timing attacks possible. We could use the XOR-OR constant-time + // comparison algorithm, but that may not be a reliable defense in an + // interpreted language. So we use the approach of HMACing both strings + // with a random key and comparing the HMACs. + + // We're not attempting to make variable-length string comparison + // secure, as that's very difficult. Make sure the strings are the same + // length. + Core::ensureTrue(Core::ourStrlen($expected) === Core::ourStrlen($given)); + + $blind = Core::secureRandom(32); + $message_compare = \hash_hmac(Core::HASH_FUNCTION_NAME, $given, $blind); + $correct_compare = \hash_hmac(Core::HASH_FUNCTION_NAME, $expected, $blind); + return $correct_compare === $message_compare; + } + /** + * Throws an exception if the constant doesn't exist. + * + * @param string $name + * @return void + * + * @throws Ex\EnvironmentIsBrokenException + */ + public static function ensureConstantExists($name) + { + Core::ensureTrue( + \defined($name), + 'Constant '.$name.' does not exists' + ); + } + + /** + * Throws an exception if the function doesn't exist. + * + * @param string $name + * @return void + * + * @throws Ex\EnvironmentIsBrokenException + */ + public static function ensureFunctionExists($name) + { + Core::ensureTrue( + \function_exists($name), + 'function '.$name.' does not exists' + ); + } + + /** + * Throws an exception if the condition is false. + * + * @param bool $condition + * @param string $message + * @return void + * + * @throws Ex\EnvironmentIsBrokenException + */ + public static function ensureTrue($condition, $message = '') + { + if (!$condition) { + throw new Ex\EnvironmentIsBrokenException($message); + } + } + + /* + * We need these strlen() and substr() functions because when + * 'mbstring.func_overload' is set in php.ini, the standard strlen() and + * substr() are replaced by mb_strlen() and mb_substr(). + */ + + /** + * Computes the length of a string in bytes. + * + * @param string $str + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return int + */ + public static function ourStrlen($str) + { + static $exists = null; + if ($exists === null) { + $exists = \extension_loaded('mbstring') && \ini_get('mbstring.func_overload') !== false && (int)\ini_get('mbstring.func_overload') & MB_OVERLOAD_STRING; + } + if ($exists) { + $length = \mb_strlen($str, '8bit'); + Core::ensureTrue($length !== false); + return $length; + } else { + return \strlen($str); + } + } + + /** + * Behaves roughly like the function substr() in PHP 7 does. + * + * @param string $str + * @param int $start + * @param int $length + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string|bool + */ + public static function ourSubstr($str, $start, $length = null) + { + static $exists = null; + if ($exists === null) { + $exists = \extension_loaded('mbstring') && \ini_get('mbstring.func_overload') !== false && (int)\ini_get('mbstring.func_overload') & MB_OVERLOAD_STRING; + } + + // This is required to make mb_substr behavior identical to substr. + // Without this, mb_substr() would return false, contra to what the + // PHP documentation says (it doesn't say it can return false.) + $input_len = Core::ourStrlen($str); + if ($start === $input_len && !$length) { + return ''; + } + + if ($start > $input_len) { + return false; + } + + // mb_substr($str, 0, NULL, '8bit') returns an empty string on PHP 5.3, + // so we have to find the length ourselves. Also, substr() doesn't + // accept null for the length. + if (! isset($length)) { + if ($start >= 0) { + $length = $input_len - $start; + } else { + $length = -$start; + } + } + + if ($length < 0) { + throw new \InvalidArgumentException( + "Negative lengths are not supported with ourSubstr." + ); + } + + if ($exists) { + $substr = \mb_substr($str, $start, $length, '8bit'); + // At this point there are two cases where mb_substr can + // legitimately return an empty string. Either $length is 0, or + // $start is equal to the length of the string (both mb_substr and + // substr return an empty string when this happens). It should never + // ever return a string that's longer than $length. + if (Core::ourStrlen($substr) > $length || (Core::ourStrlen($substr) === 0 && $length !== 0 && $start !== $input_len)) { + throw new Ex\EnvironmentIsBrokenException( + 'Your version of PHP has bug #66797. Its implementation of + mb_substr() is incorrect. See the details here: + https://bugs.php.net/bug.php?id=66797' + ); + } + return $substr; + } + + return \substr($str, $start, $length); + } + + /** + * Computes the PBKDF2 password-based key derivation function. + * + * The PBKDF2 function is defined in RFC 2898. Test vectors can be found in + * RFC 6070. This implementation of PBKDF2 was originally created by Taylor + * Hornby, with improvements from http://www.variations-of-shadow.com/. + * + * @param string $algorithm The hash algorithm to use. Recommended: SHA256 + * @param string $password The password. + * @param string $salt A salt that is unique to the password. + * @param int $count Iteration count. Higher is better, but slower. Recommended: At least 1000. + * @param int $key_length The length of the derived key in bytes. + * @param bool $raw_output If true, the key is returned in raw binary format. Hex encoded otherwise. + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string A $key_length-byte key derived from the password and salt. + */ + public static function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false) + { + // Type checks: + if (! \is_string($algorithm)) { + throw new \InvalidArgumentException( + 'pbkdf2(): algorithm must be a string' + ); + } + if (! \is_string($password)) { + throw new \InvalidArgumentException( + 'pbkdf2(): password must be a string' + ); + } + if (! \is_string($salt)) { + throw new \InvalidArgumentException( + 'pbkdf2(): salt must be a string' + ); + } + // Coerce strings to integers with no information loss or overflow + $count += 0; + $key_length += 0; + + $algorithm = \strtolower($algorithm); + Core::ensureTrue( + \in_array($algorithm, \hash_algos(), true), + 'Invalid or unsupported hash algorithm.' + ); + + // Whitelist, or we could end up with people using CRC32. + $ok_algorithms = [ + 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', + 'ripemd160', 'ripemd256', 'ripemd320', 'whirlpool', + ]; + Core::ensureTrue( + \in_array($algorithm, $ok_algorithms, true), + 'Algorithm is not a secure cryptographic hash function.' + ); + + Core::ensureTrue($count > 0 && $key_length > 0, 'Invalid PBKDF2 parameters.'); + + if (\function_exists('hash_pbkdf2')) { + // The output length is in NIBBLES (4-bits) if $raw_output is false! + if (! $raw_output) { + $key_length = $key_length * 2; + } + return \hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output); + } + + $hash_length = Core::ourStrlen(\hash($algorithm, '', true)); + $block_count = \ceil($key_length / $hash_length); + + $output = ''; + for ($i = 1; $i <= $block_count; $i++) { + // $i encoded as 4 bytes, big endian. + $last = $salt . \pack('N', $i); + // first iteration + $last = $xorsum = \hash_hmac($algorithm, $last, $password, true); + // perform the other $count - 1 iterations + for ($j = 1; $j < $count; $j++) { + /** + * @psalm-suppress InvalidOperand + */ + $xorsum ^= ($last = \hash_hmac($algorithm, $last, $password, true)); + } + $output .= $xorsum; + } + + if ($raw_output) { + return (string) Core::ourSubstr($output, 0, $key_length); + } else { + return Encoding::binToHex((string) Core::ourSubstr($output, 0, $key_length)); + } + } +} diff --git a/src/ncc/ThirdParty/defuse/php-encryption/Crypto.php b/src/ncc/ThirdParty/defuse/php-encryption/Crypto.php new file mode 100644 index 0000000..6bed553 --- /dev/null +++ b/src/ncc/ThirdParty/defuse/php-encryption/Crypto.php @@ -0,0 +1,439 @@ +deriveKeys($salt); + $ekey = $keys->getEncryptionKey(); + $akey = $keys->getAuthenticationKey(); + $iv = Core::secureRandom(Core::BLOCK_BYTE_SIZE); + + $ciphertext = Core::CURRENT_VERSION . $salt . $iv . self::plainEncrypt($plaintext, $ekey, $iv); + $auth = \hash_hmac(Core::HASH_FUNCTION_NAME, $ciphertext, $akey, true); + $ciphertext = $ciphertext . $auth; + + if ($raw_binary) { + return $ciphertext; + } + return Encoding::binToHex($ciphertext); + } + + /** + * 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. + /** @var string $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. + /** @var string $salt */ + $salt = Core::ourSubstr( + $ciphertext, + Core::HEADER_VERSION_SIZE, + Core::SALT_BYTE_SIZE + ); + Core::ensureTrue(\is_string($salt)); + + // Get the IV. + /** @var string $iv */ + $iv = Core::ourSubstr( + $ciphertext, + Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE, + Core::BLOCK_BYTE_SIZE + ); + Core::ensureTrue(\is_string($iv)); + + // Get the HMAC. + /** @var string $hmac */ + $hmac = Core::ourSubstr( + $ciphertext, + Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE, + Core::MAC_BYTE_SIZE + ); + Core::ensureTrue(\is_string($hmac)); + + // Get the actual encrypted ciphertext. + /** @var string $encrypted */ + $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 + ); + Core::ensureTrue(\is_string($encrypted)); + + // 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.' + ); + } + } + + /** + * Raw unauthenticated encryption (insecure on its own). + * + * @param string $plaintext + * @param string $key + * @param string $iv + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string + */ + protected static function plainEncrypt($plaintext, $key, $iv) + { + Core::ensureConstantExists('OPENSSL_RAW_DATA'); + Core::ensureFunctionExists('openssl_encrypt'); + /** @var string $ciphertext */ + $ciphertext = \openssl_encrypt( + $plaintext, + Core::CIPHER_METHOD, + $key, + OPENSSL_RAW_DATA, + $iv + ); + + Core::ensureTrue(\is_string($ciphertext), 'openssl_encrypt() failed'); + + return $ciphertext; + } + + /** + * Raw unauthenticated decryption (insecure on its own). + * + * @param string $ciphertext + * @param string $key + * @param string $iv + * @param string $cipherMethod + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string + */ + protected static function plainDecrypt($ciphertext, $key, $iv, $cipherMethod) + { + Core::ensureConstantExists('OPENSSL_RAW_DATA'); + Core::ensureFunctionExists('openssl_decrypt'); + + /** @var string $plaintext */ + $plaintext = \openssl_decrypt( + $ciphertext, + $cipherMethod, + $key, + OPENSSL_RAW_DATA, + $iv + ); + Core::ensureTrue(\is_string($plaintext), 'openssl_decrypt() failed.'); + + return $plaintext; + } + + /** + * Verifies an HMAC without leaking information through side-channels. + * + * @param string $expected_hmac + * @param string $message + * @param string $key + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return bool + */ + protected static function verifyHMAC($expected_hmac, $message, $key) + { + $message_hmac = \hash_hmac(Core::HASH_FUNCTION_NAME, $message, $key, true); + return Core::hashEquals($message_hmac, $expected_hmac); + } +} diff --git a/src/ncc/ThirdParty/defuse/php-encryption/DerivedKeys.php b/src/ncc/ThirdParty/defuse/php-encryption/DerivedKeys.php new file mode 100644 index 0000000..4dd1d61 --- /dev/null +++ b/src/ncc/ThirdParty/defuse/php-encryption/DerivedKeys.php @@ -0,0 +1,50 @@ +akey; + } + + /** + * Returns the encryption key. + * @return string + */ + public function getEncryptionKey() + { + return $this->ekey; + } + + /** + * Constructor for DerivedKeys. + * + * @param string $akey + * @param string $ekey + */ + public function __construct($akey, $ekey) + { + $this->akey = $akey; + $this->ekey = $ekey; + } +} diff --git a/src/ncc/ThirdParty/defuse/php-encryption/Encoding.php b/src/ncc/ThirdParty/defuse/php-encryption/Encoding.php new file mode 100644 index 0000000..33e126e --- /dev/null +++ b/src/ncc/ThirdParty/defuse/php-encryption/Encoding.php @@ -0,0 +1,269 @@ +> 4; + $hex .= \pack( + 'CC', + 87 + $b + ((($b - 10) >> 8) & ~38), + 87 + $c + ((($c - 10) >> 8) & ~38) + ); + } + return $hex; + } + + /** + * Converts a hexadecimal string into a byte string without leaking + * information through side channels. + * + * @param string $hex_string + * + * @throws Ex\BadFormatException + * @throws Ex\EnvironmentIsBrokenException + * + * @return string + * @psalm-suppress TypeDoesNotContainType + */ + 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 Ex\BadFormatException( + 'Encoding::hexToBin() input is not a hex string.' + ); + } + $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0); + if ($state === 0) { + $c_acc = $c_val * 16; + } else { + $bin .= \pack('C', $c_acc | $c_val); + } + $state ^= 1; + ++$hex_pos; + } + return $bin; + } + + /** + * Remove trialing whitespace without table look-ups or branches. + * + * Calling this function may leak the length of the string as well as the + * number of trailing whitespace characters through side-channels. + * + * @param string $string + * @return string + */ + public static function trimTrailingWhitespace($string = '') + { + $length = Core::ourStrlen($string); + if ($length < 1) { + return ''; + } + do { + $prevLength = $length; + $last = $length - 1; + $chr = \ord($string[$last]); + + /* Null Byte (0x00), a.k.a. \0 */ + // if ($chr === 0x00) $length -= 1; + $sub = (($chr - 1) >> 8 ) & 1; + $length -= $sub; + $last -= $sub; + + /* Horizontal Tab (0x09) a.k.a. \t */ + $chr = \ord($string[$last]); + // if ($chr === 0x09) $length -= 1; + $sub = (((0x08 - $chr) & ($chr - 0x0a)) >> 8) & 1; + $length -= $sub; + $last -= $sub; + + /* New Line (0x0a), a.k.a. \n */ + $chr = \ord($string[$last]); + // if ($chr === 0x0a) $length -= 1; + $sub = (((0x09 - $chr) & ($chr - 0x0b)) >> 8) & 1; + $length -= $sub; + $last -= $sub; + + /* Carriage Return (0x0D), a.k.a. \r */ + $chr = \ord($string[$last]); + // if ($chr === 0x0d) $length -= 1; + $sub = (((0x0c - $chr) & ($chr - 0x0e)) >> 8) & 1; + $length -= $sub; + $last -= $sub; + + /* Space */ + $chr = \ord($string[$last]); + // if ($chr === 0x20) $length -= 1; + $sub = (((0x1f - $chr) & ($chr - 0x21)) >> 8) & 1; + $length -= $sub; + } while ($prevLength !== $length && $length > 0); + return (string) Core::ourSubstr($string, 0, $length); + } + + /* + * SECURITY NOTE ON APPLYING CHECKSUMS TO SECRETS: + * + * The checksum introduces a potential security weakness. For example, + * suppose we apply a checksum to a key, and that an adversary has an + * exploit against the process containing the key, such that they can + * overwrite an arbitrary byte of memory and then cause the checksum to + * be verified and learn the result. + * + * In this scenario, the adversary can extract the key one byte at + * a time by overwriting it with their guess of its value and then + * asking if the checksum matches. If it does, their guess was right. + * This kind of attack may be more easy to implement and more reliable + * than a remote code execution attack. + * + * This attack also applies to authenticated encryption as a whole, in + * the situation where the adversary can overwrite a byte of the key + * and then cause a valid ciphertext to be decrypted, and then + * determine whether the MAC check passed or failed. + * + * By using the full SHA256 hash instead of truncating it, I'm ensuring + * that both ways of going about the attack are equivalently difficult. + * A shorter checksum of say 32 bits might be more useful to the + * adversary as an oracle in case their writes are coarser grained. + * + * Because the scenario assumes a serious vulnerability, we don't try + * to prevent attacks of this style. + */ + + /** + * INTERNAL USE ONLY: Applies a version header, applies a checksum, and + * then encodes a byte string into a range of printable ASCII characters. + * + * @param string $header + * @param string $bytes + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string + */ + public static function saveBytesToChecksummedAsciiSafeString($header, $bytes) + { + // Headers must be a constant length to prevent one type's header from + // being a prefix of another type's header, leading to ambiguity. + Core::ensureTrue( + Core::ourStrlen($header) === self::SERIALIZE_HEADER_BYTES, + 'Header must be ' . self::SERIALIZE_HEADER_BYTES . ' bytes.' + ); + + return Encoding::binToHex( + $header . + $bytes . + \hash( + self::CHECKSUM_HASH_ALGO, + $header . $bytes, + true + ) + ); + } + + /** + * INTERNAL USE ONLY: Decodes, verifies the header and checksum, and returns + * the encoded byte string. + * + * @param string $expected_header + * @param string $string + * + * @throws Ex\EnvironmentIsBrokenException + * @throws Ex\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. + Core::ensureTrue( + Core::ourStrlen($expected_header) === self::SERIALIZE_HEADER_BYTES, + 'Header must be 4 bytes.' + ); + + /* If you get an exception here when attempting to load from a file, first pass your + key to Encoding::trimTrailingWhitespace() to remove newline characters, etc. */ + $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 = (string) 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 = (string) Core::ourSubstr( + $bytes, + 0, + Core::ourStrlen($bytes) - self::CHECKSUM_BYTE_SIZE + ); + + /* Grab the included checksum. */ + $checksum_a = (string) 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 (string) Core::ourSubstr( + $bytes, + self::SERIALIZE_HEADER_BYTES, + Core::ourStrlen($bytes) - self::SERIALIZE_HEADER_BYTES - self::CHECKSUM_BYTE_SIZE + ); + } +} diff --git a/src/ncc/ThirdParty/defuse/php-encryption/Exception/BadFormatException.php b/src/ncc/ThirdParty/defuse/php-encryption/Exception/BadFormatException.php new file mode 100644 index 0000000..2e00229 --- /dev/null +++ b/src/ncc/ThirdParty/defuse/php-encryption/Exception/BadFormatException.php @@ -0,0 +1,7 @@ +deriveKeys($file_salt); + $ekey = $keys->getEncryptionKey(); + $akey = $keys->getAuthenticationKey(); + + $ivsize = Core::BLOCK_BYTE_SIZE; + $iv = Core::secureRandom($ivsize); + + /* Initialize a streaming HMAC state. */ + /** @var mixed $hmac */ + $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey); + Core::ensureTrue( + \is_resource($hmac) || \is_object($hmac), + 'Cannot initialize a hash context' + ); + + /* Write the header, salt, and IV. */ + self::writeBytes( + $outputHandle, + Core::CURRENT_VERSION . $file_salt . $iv, + Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + $ivsize + ); + + /* Add the header, salt, and IV to the HMAC. */ + \hash_update($hmac, Core::CURRENT_VERSION); + \hash_update($hmac, $file_salt); + \hash_update($hmac, $iv); + + /* $thisIv will be incremented after each call to the encryption. */ + $thisIv = $iv; + + /* How many blocks do we encrypt at a time? We increment by this value. */ + /** + * @psalm-suppress RedundantCast + */ + $inc = (int) (Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE); + + /* Loop until we reach the end of the input file. */ + $at_file_end = false; + while (! (\feof($inputHandle) || $at_file_end)) { + /* Find out if we can read a full buffer, or only a partial one. */ + /** @var int */ + $pos = \ftell($inputHandle); + if (!\is_int($pos)) { + throw new Ex\IOException( + 'Could not get current position in input file during encryption' + ); + } + if ($pos + Core::BUFFER_BYTE_SIZE >= $inputSize) { + /* We're at the end of the file, so we need to break out of the loop. */ + $at_file_end = true; + $read = self::readBytes( + $inputHandle, + $inputSize - $pos + ); + } else { + $read = self::readBytes( + $inputHandle, + Core::BUFFER_BYTE_SIZE + ); + } + + /* Encrypt this buffer. */ + /** @var string */ + $encrypted = \openssl_encrypt( + $read, + Core::CIPHER_METHOD, + $ekey, + OPENSSL_RAW_DATA, + $thisIv + ); + + Core::ensureTrue(\is_string($encrypted), 'OpenSSL encryption error'); + + /* Write this buffer's ciphertext. */ + self::writeBytes($outputHandle, $encrypted, Core::ourStrlen($encrypted)); + /* Add this buffer's ciphertext to the HMAC. */ + \hash_update($hmac, $encrypted); + + /* Increment the counter by the number of blocks in a buffer. */ + $thisIv = Core::incrementCounter($thisIv, $inc); + /* WARNING: Usually, unless the file is a multiple of the buffer + * size, $thisIv will contain an incorrect value here on the last + * iteration of this loop. */ + } + + /* Get the HMAC and append it to the ciphertext. */ + $final_mac = \hash_final($hmac, true); + self::writeBytes($outputHandle, $final_mac, Core::MAC_BYTE_SIZE); + } + + /** + * Decrypts a file-backed resource with either a key or a password. + * + * @param resource $inputHandle + * @param resource $outputHandle + * @param KeyOrPassword $secret + * @return void + * + * @throws Ex\EnvironmentIsBrokenException + * @throws Ex\IOException + * @throws Ex\WrongKeyOrModifiedCiphertextException + * @psalm-suppress PossiblyInvalidArgument + * Fixes erroneous errors caused by PHP 7.2 switching the return value + * of hash_init from a resource to a HashContext. + */ + public static function decryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret) + { + if (! \is_resource($inputHandle)) { + throw new Ex\IOException( + 'Input handle must be a resource!' + ); + } + if (! \is_resource($outputHandle)) { + throw new Ex\IOException( + 'Output handle must be a resource!' + ); + } + + /* Make sure the file is big enough for all the reads we need to do. */ + $stat = \fstat($inputHandle); + if ($stat['size'] < Core::MINIMUM_CIPHERTEXT_SIZE) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Input file is too small to have been created by this library.' + ); + } + + /* Check the version header. */ + $header = self::readBytes($inputHandle, Core::HEADER_VERSION_SIZE); + if ($header !== Core::CURRENT_VERSION) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Bad version header.' + ); + } + + /* Get the salt. */ + $file_salt = self::readBytes($inputHandle, Core::SALT_BYTE_SIZE); + + /* Get the IV. */ + $ivsize = Core::BLOCK_BYTE_SIZE; + $iv = self::readBytes($inputHandle, $ivsize); + + /* Derive the authentication and encryption keys. */ + $keys = $secret->deriveKeys($file_salt); + $ekey = $keys->getEncryptionKey(); + $akey = $keys->getAuthenticationKey(); + + /* We'll store the MAC of each buffer-sized chunk as we verify the + * actual MAC, so that we can check them again when decrypting. */ + $macs = []; + + /* $thisIv will be incremented after each call to the decryption. */ + $thisIv = $iv; + + /* How many blocks do we encrypt at a time? We increment by this value. */ + /** + * @psalm-suppress RedundantCast + */ + $inc = (int) (Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE); + + /* Get the HMAC. */ + if (\fseek($inputHandle, (-1 * Core::MAC_BYTE_SIZE), SEEK_END) === -1) { + throw new Ex\IOException( + 'Cannot seek to beginning of MAC within input file' + ); + } + + /* Get the position of the last byte in the actual ciphertext. */ + /** @var int $cipher_end */ + $cipher_end = \ftell($inputHandle); + if (!\is_int($cipher_end)) { + throw new Ex\IOException( + 'Cannot read input file' + ); + } + /* We have the position of the first byte of the HMAC. Go back by one. */ + --$cipher_end; + + /* Read the HMAC. */ + /** @var string $stored_mac */ + $stored_mac = self::readBytes($inputHandle, Core::MAC_BYTE_SIZE); + + /* Initialize a streaming HMAC state. */ + /** @var mixed $hmac */ + $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey); + Core::ensureTrue(\is_resource($hmac) || \is_object($hmac), 'Cannot initialize a hash context'); + + /* Reset file pointer to the beginning of the file after the header */ + if (\fseek($inputHandle, Core::HEADER_VERSION_SIZE, SEEK_SET) === -1) { + throw new Ex\IOException( + 'Cannot read seek within input file' + ); + } + + /* Seek to the start of the actual ciphertext. */ + if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize, SEEK_CUR) === -1) { + throw new Ex\IOException( + 'Cannot seek input file to beginning of ciphertext' + ); + } + + /* PASS #1: Calculating the HMAC. */ + + \hash_update($hmac, $header); + \hash_update($hmac, $file_salt); + \hash_update($hmac, $iv); + /** @var mixed $hmac2 */ + $hmac2 = \hash_copy($hmac); + + $break = false; + while (! $break) { + /** @var int $pos */ + $pos = \ftell($inputHandle); + if (!\is_int($pos)) { + throw new Ex\IOException( + 'Could not get current position in input file during decryption' + ); + } + + /* Read the next buffer-sized chunk (or less). */ + if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) { + $break = true; + $read = self::readBytes( + $inputHandle, + $cipher_end - $pos + 1 + ); + } else { + $read = self::readBytes( + $inputHandle, + Core::BUFFER_BYTE_SIZE + ); + } + + /* Update the HMAC. */ + \hash_update($hmac, $read); + + /* Remember this buffer-sized chunk's HMAC. */ + /** @var mixed $chunk_mac */ + $chunk_mac = \hash_copy($hmac); + Core::ensureTrue(\is_resource($chunk_mac) || \is_object($chunk_mac), 'Cannot duplicate a hash context'); + $macs []= \hash_final($chunk_mac); + } + + /* Get the final HMAC, which should match the stored one. */ + /** @var string $final_mac */ + $final_mac = \hash_final($hmac, true); + + /* Verify the HMAC. */ + if (! Core::hashEquals($final_mac, $stored_mac)) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Integrity check failed.' + ); + } + + /* PASS #2: Decrypt and write output. */ + + /* Rewind to the start of the actual ciphertext. */ + if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === -1) { + throw new Ex\IOException( + 'Could not move the input file pointer during decryption' + ); + } + + $at_file_end = false; + while (! $at_file_end) { + /** @var int $pos */ + $pos = \ftell($inputHandle); + if (!\is_int($pos)) { + throw new Ex\IOException( + 'Could not get current position in input file during decryption' + ); + } + + /* Read the next buffer-sized chunk (or less). */ + if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) { + $at_file_end = true; + $read = self::readBytes( + $inputHandle, + $cipher_end - $pos + 1 + ); + } else { + $read = self::readBytes( + $inputHandle, + Core::BUFFER_BYTE_SIZE + ); + } + + /* Recalculate the MAC (so far) and compare it with the one we + * remembered from pass #1 to ensure attackers didn't change the + * ciphertext after MAC verification. */ + \hash_update($hmac2, $read); + /** @var mixed $calc_mac */ + $calc_mac = \hash_copy($hmac2); + Core::ensureTrue(\is_resource($calc_mac) || \is_object($calc_mac), 'Cannot duplicate a hash context'); + $calc = \hash_final($calc_mac); + + if (empty($macs)) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'File was modified after MAC verification' + ); + } elseif (! Core::hashEquals(\array_shift($macs), $calc)) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'File was modified after MAC verification' + ); + } + + /* Decrypt this buffer-sized chunk. */ + /** @var string $decrypted */ + $decrypted = \openssl_decrypt( + $read, + Core::CIPHER_METHOD, + $ekey, + OPENSSL_RAW_DATA, + $thisIv + ); + Core::ensureTrue(\is_string($decrypted), 'OpenSSL decryption error'); + + /* Write the plaintext to the output file. */ + self::writeBytes( + $outputHandle, + $decrypted, + Core::ourStrlen($decrypted) + ); + + /* Increment the IV by the amount of blocks in a buffer. */ + /** @var string $thisIv */ + $thisIv = Core::incrementCounter($thisIv, $inc); + /* WARNING: Usually, unless the file is a multiple of the buffer + * size, $thisIv will contain an incorrect value here on the last + * iteration of this loop. */ + } + } + + /** + * Read from a stream; prevent partial reads. + * + * @param resource $stream + * @param int $num_bytes + * @return string + * + * @throws Ex\IOException + * @throws Ex\EnvironmentIsBrokenException + */ + public static function readBytes($stream, $num_bytes) + { + Core::ensureTrue($num_bytes >= 0, 'Tried to read less than 0 bytes'); + + if ($num_bytes === 0) { + return ''; + } + + $buf = ''; + $remaining = $num_bytes; + while ($remaining > 0 && ! \feof($stream)) { + /** @var string $read */ + $read = \fread($stream, $remaining); + if (!\is_string($read)) { + throw new Ex\IOException( + 'Could not read from the file' + ); + } + $buf .= $read; + $remaining -= Core::ourStrlen($read); + } + if (Core::ourStrlen($buf) !== $num_bytes) { + throw new Ex\IOException( + 'Tried to read past the end of the file' + ); + } + return $buf; + } + + /** + * Write to a stream; prevents partial writes. + * + * @param resource $stream + * @param string $buf + * @param int $num_bytes + * @return int + * + * @throws Ex\IOException + */ + public static function writeBytes($stream, $buf, $num_bytes = null) + { + $bufSize = Core::ourStrlen($buf); + if ($num_bytes === null) { + $num_bytes = $bufSize; + } + if ($num_bytes > $bufSize) { + throw new Ex\IOException( + 'Trying to write more bytes than the buffer contains.' + ); + } + if ($num_bytes < 0) { + throw new Ex\IOException( + 'Tried to write less than 0 bytes' + ); + } + $remaining = $num_bytes; + while ($remaining > 0) { + /** @var int $written */ + $written = \fwrite($stream, $buf, $remaining); + if (!\is_int($written)) { + throw new Ex\IOException( + 'Could not write to the file' + ); + } + $buf = (string) Core::ourSubstr($buf, $written, null); + $remaining -= $written; + } + return $num_bytes; + } + + /** + * Returns the last PHP error's or warning's message string. + * + * @return string + */ + private static function getLastErrorMessage() + { + $error = error_get_last(); + if ($error === null) { + return '[no PHP error]'; + } else { + return $error['message']; + } + } +} diff --git a/src/ncc/ThirdParty/defuse/php-encryption/Key.php b/src/ncc/ThirdParty/defuse/php-encryption/Key.php new file mode 100644 index 0000000..c0bbdde --- /dev/null +++ b/src/ncc/ThirdParty/defuse/php-encryption/Key.php @@ -0,0 +1,94 @@ +key_bytes + ); + } + + /** + * Gets the raw bytes of the key. + * + * @return string + */ + public function getRawBytes() + { + return $this->key_bytes; + } + + /** + * Constructs a new Key object from a string of raw bytes. + * + * @param string $bytes + * + * @throws Ex\EnvironmentIsBrokenException + */ + private function __construct($bytes) + { + Core::ensureTrue( + Core::ourStrlen($bytes) === self::KEY_BYTE_SIZE, + 'Bad key length.' + ); + $this->key_bytes = $bytes; + } + +} diff --git a/src/ncc/ThirdParty/defuse/php-encryption/KeyOrPassword.php b/src/ncc/ThirdParty/defuse/php-encryption/KeyOrPassword.php new file mode 100644 index 0000000..c65a6e1 --- /dev/null +++ b/src/ncc/ThirdParty/defuse/php-encryption/KeyOrPassword.php @@ -0,0 +1,149 @@ +secret_type === self::SECRET_TYPE_KEY) { + Core::ensureTrue($this->secret instanceof Key); + /** + * @psalm-suppress PossiblyInvalidMethodCall + */ + $akey = Core::HKDF( + Core::HASH_FUNCTION_NAME, + $this->secret->getRawBytes(), + Core::KEY_BYTE_SIZE, + Core::AUTHENTICATION_INFO_STRING, + $salt + ); + /** + * @psalm-suppress PossiblyInvalidMethodCall + */ + $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) { + Core::ensureTrue(\is_string($this->secret)); + /* 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. */ + + /** + * @psalm-suppress PossiblyInvalidArgument + */ + $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.'); + } + } + + /** + * Constructor for KeyOrPassword. + * + * @param int $secret_type + * @param mixed $secret (either a Key or a password string) + */ + private function __construct($secret_type, $secret) + { + // The constructor is private, so these should never throw. + if ($secret_type === self::SECRET_TYPE_KEY) { + Core::ensureTrue($secret instanceof Key); + } elseif ($secret_type === self::SECRET_TYPE_PASSWORD) { + Core::ensureTrue(\is_string($secret)); + } else { + throw new Ex\EnvironmentIsBrokenException('Bad secret type.'); + } + $this->secret_type = $secret_type; + $this->secret = $secret; + } +} diff --git a/src/ncc/ThirdParty/defuse/php-encryption/KeyProtectedByPassword.php b/src/ncc/ThirdParty/defuse/php-encryption/KeyProtectedByPassword.php new file mode 100644 index 0000000..696934a --- /dev/null +++ b/src/ncc/ThirdParty/defuse/php-encryption/KeyProtectedByPassword.php @@ -0,0 +1,145 @@ +saveToAsciiSafeString(), + \hash(Core::HASH_FUNCTION_NAME, $password, true), + true + ); + + return new KeyProtectedByPassword($encrypted_key); + } + + /** + * Loads a KeyProtectedByPassword from its encoded form. + * + * @param string $saved_key_string + * + * @throws Ex\BadFormatException + * + * @return KeyProtectedByPassword + */ + public static function loadFromAsciiSafeString($saved_key_string) + { + $encrypted_key = Encoding::loadBytesFromChecksummedAsciiSafeString( + self::PASSWORD_KEY_CURRENT_VERSION, + $saved_key_string + ); + return new KeyProtectedByPassword($encrypted_key); + } + + /** + * Encodes the KeyProtectedByPassword into a string of printable ASCII + * characters. + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string + */ + public function saveToAsciiSafeString() + { + return Encoding::saveBytesToChecksummedAsciiSafeString( + self::PASSWORD_KEY_CURRENT_VERSION, + $this->encrypted_key + ); + } + + /** + * Decrypts the protected key, returning an unprotected Key object that can + * be used for encryption and decryption. + * + * @throws Ex\EnvironmentIsBrokenException + * @throws Ex\WrongKeyOrModifiedCiphertextException + * + * @param string $password + * @return Key + */ + public function unlockKey($password) + { + try { + $inner_key_encoded = Crypto::decryptWithPassword( + $this->encrypted_key, + \hash(Core::HASH_FUNCTION_NAME, $password, true), + true + ); + return Key::loadFromAsciiSafeString($inner_key_encoded); + } catch (Ex\BadFormatException $ex) { + /* This should never happen unless an attacker replaced the + * encrypted key ciphertext with some other ciphertext that was + * encrypted with the same password. We transform the exception type + * here in order to make the API simpler, avoiding the need to + * document that this method might throw an Ex\BadFormatException. */ + throw new Ex\WrongKeyOrModifiedCiphertextException( + "The decrypted key was found to be in an invalid format. " . + "This very likely indicates it was modified by an attacker." + ); + } + } + + /** + * Changes the password. + * + * @param string $current_password + * @param string $new_password + * + * @throws Ex\EnvironmentIsBrokenException + * @throws Ex\WrongKeyOrModifiedCiphertextException + * + * @return KeyProtectedByPassword + */ + public function changePassword($current_password, $new_password) + { + $inner_key = $this->unlockKey($current_password); + /* The password is hashed as a form of poor-man's domain separation + * between this use of encryptWithPassword() and other uses of + * encryptWithPassword() that the user may also be using as part of the + * same protocol. */ + $encrypted_key = Crypto::encryptWithPassword( + $inner_key->saveToAsciiSafeString(), + \hash(Core::HASH_FUNCTION_NAME, $new_password, true), + true + ); + + $this->encrypted_key = $encrypted_key; + + return $this; + } + + /** + * Constructor for KeyProtectedByPassword. + * + * @param string $encrypted_key + */ + private function __construct($encrypted_key) + { + $this->encrypted_key = $encrypted_key; + } +} diff --git a/src/ncc/ThirdParty/defuse/php-encryption/RuntimeTests.php b/src/ncc/ThirdParty/defuse/php-encryption/RuntimeTests.php new file mode 100644 index 0000000..c44654c --- /dev/null +++ b/src/ncc/ThirdParty/defuse/php-encryption/RuntimeTests.php @@ -0,0 +1,228 @@ +getRawBytes()) === Core::KEY_BYTE_SIZE); + + Core::ensureTrue(Core::ENCRYPTION_INFO_STRING !== Core::AUTHENTICATION_INFO_STRING); + } 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; + } + + /** + * High-level tests of Crypto operations. + * + * @throws Ex\EnvironmentIsBrokenException + * @return void + */ + private static function testEncryptDecrypt() + { + $key = Key::createNewRandomKey(); + $data = "EnCrYpT EvErYThInG\x00\x00"; + + // Make sure encrypting then decrypting doesn't change the message. + $ciphertext = Crypto::encrypt($data, $key, true); + try { + $decrypted = Crypto::decrypt($ciphertext, $key, true); + } catch (Ex\WrongKeyOrModifiedCiphertextException $ex) { + // It's important to catch this and change it into a + // Ex\EnvironmentIsBrokenException, otherwise a test failure could trick + // the user into thinking it's just an invalid ciphertext! + throw new Ex\EnvironmentIsBrokenException(); + } + Core::ensureTrue($decrypted === $data); + + // Modifying the ciphertext: Appending a string. + try { + Crypto::decrypt($ciphertext . 'a', $key, true); + throw new Ex\EnvironmentIsBrokenException(); + } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ + } + + // Modifying the ciphertext: Changing an HMAC byte. + $indices_to_change = [ + 0, // The header. + Core::HEADER_VERSION_SIZE + 1, // the salt + Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + 1, // the IV + Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + Core::BLOCK_BYTE_SIZE + 1, // the ciphertext + ]; + + foreach ($indices_to_change as $index) { + try { + $ciphertext[$index] = \chr((\ord($ciphertext[$index]) + 1) % 256); + Crypto::decrypt($ciphertext, $key, true); + throw new Ex\EnvironmentIsBrokenException(); + } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ + } + } + + // Decrypting with the wrong key. + $key = Key::createNewRandomKey(); + $data = 'abcdef'; + $ciphertext = Crypto::encrypt($data, $key, true); + $wrong_key = Key::createNewRandomKey(); + try { + Crypto::decrypt($ciphertext, $wrong_key, true); + throw new Ex\EnvironmentIsBrokenException(); + } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ + } + + // Ciphertext too small. + $key = Key::createNewRandomKey(); + $ciphertext = \str_repeat('A', Core::MINIMUM_CIPHERTEXT_SIZE - 1); + try { + Crypto::decrypt($ciphertext, $key, true); + throw new Ex\EnvironmentIsBrokenException(); + } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ + } + } + + /** + * Test HKDF against test vectors. + * + * @throws Ex\EnvironmentIsBrokenException + * @return void + */ + private static function HKDFTestVector() + { + // HKDF test vectors from RFC 5869 + + // Test Case 1 + $ikm = \str_repeat("\x0b", 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); + Core::ensureTrue($computed_okm === $okm); + + // Test Case 7 + $ikm = \str_repeat("\x0c", 22); + $length = 42; + $okm = Encoding::hexToBin( + '2c91117204d745f3500d636a62f64f0a' . + 'b3bae548aa53d423b0d1f27ebba6f5e5' . + '673a081d70cce7acfc48' + ); + $computed_okm = Core::HKDF('sha1', $ikm, $length, '', null); + Core::ensureTrue($computed_okm === $okm); + } + + /** + * Test HMAC against test vectors. + * + * @throws Ex\EnvironmentIsBrokenException + * @return void + */ + private static function HMACTestVector() + { + // HMAC test vector From RFC 4231 (Test Case 1) + $key = \str_repeat("\x0b", 20); + $data = 'Hi There'; + $correct = 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'; + Core::ensureTrue( + \hash_hmac(Core::HASH_FUNCTION_NAME, $data, $key) === $correct + ); + } + + /** + * Test AES against test vectors. + * + * @throws Ex\EnvironmentIsBrokenException + * @return void + */ + private static function AESTestVector() + { + // AES CTR mode test vector from NIST SP 800-38A + $key = Encoding::hexToBin( + '603deb1015ca71be2b73aef0857d7781' . + '1f352c073b6108d72d9810a30914dff4' + ); + $iv = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff'); + $plaintext = Encoding::hexToBin( + '6bc1bee22e409f96e93d7e117393172a' . + 'ae2d8a571e03ac9c9eb76fac45af8e51' . + '30c81c46a35ce411e5fbc1191a0a52ef' . + 'f69f2445df4f9b17ad2b417be66c3710' + ); + $ciphertext = Encoding::hexToBin( + '601ec313775789a5b7a7f504bbf3d228' . + 'f443e3ca4d62b59aca84e990cacaf5c5' . + '2b0930daa23de94ce87017ba2d84988d' . + 'dfc9c58db67aada613c2dd08457941a6' + ); + + $computed_ciphertext = Crypto::plainEncrypt($plaintext, $key, $iv); + Core::ensureTrue($computed_ciphertext === $ciphertext); + + $computed_plaintext = Crypto::plainDecrypt($ciphertext, $key, $iv, Core::CIPHER_METHOD); + Core::ensureTrue($computed_plaintext === $plaintext); + } +} diff --git a/src/ncc/ThirdParty/defuse/php-encryption/VERSION b/src/ncc/ThirdParty/defuse/php-encryption/VERSION new file mode 100644 index 0000000..a625450 --- /dev/null +++ b/src/ncc/ThirdParty/defuse/php-encryption/VERSION @@ -0,0 +1 @@ +2.3.1 \ No newline at end of file diff --git a/src/ncc/autoload.php b/src/ncc/autoload.php index b4858a9..12449b8 100644 --- a/src/ncc/autoload.php +++ b/src/ncc/autoload.php @@ -7,7 +7,7 @@ spl_autoload_register( static $classes = null; if ($classes === null) { $classes = array( - ' ncc\\abstracts\\authenticationsource' => '/Abstracts/AuthenticationSource.php', + 'ncc\\abstracts\\authenticationsource' => '/Abstracts/AuthenticationSource.php', 'ncc\\abstracts\\exceptioncodes' => '/Abstracts/ExceptionCodes.php', 'ncc\\abstracts\\nccbuildflags' => '/Abstracts/NccBuildFlags.php', 'ncc\\abstracts\\regexpatterns' => '/Abstracts/RegexPatterns.php', @@ -15,17 +15,34 @@ spl_autoload_register( 'ncc\\abstracts\\remotesource' => '/Abstracts/RemoteSource.php', 'ncc\\abstracts\\scopes' => '/Abstracts/Scopes.php', 'ncc\\abstracts\\stringpaddingmethod' => '/Abstracts/StringPaddingMethod.php', + 'ncc\\abstracts\\versions' => '/Abstracts/Versions.php', 'ncc\\cli\\credentialmenu' => '/CLI/CredentialMenu.php', 'ncc\\cli\\functions' => '/CLI/Functions.php', 'ncc\\cli\\helpmenu' => '/CLI/HelpMenu.php', 'ncc\\cli\\main' => '/CLI/Main.php', + 'ncc\\defuse\\crypto\\core' => '/ThirdParty/defuse/php-encryption/Core.php', + 'ncc\\defuse\\crypto\\crypto' => '/ThirdParty/defuse/php-encryption/Crypto.php', + 'ncc\\defuse\\crypto\\derivedkeys' => '/ThirdParty/defuse/php-encryption/DerivedKeys.php', + 'ncc\\defuse\\crypto\\encoding' => '/ThirdParty/defuse/php-encryption/Encoding.php', + 'ncc\\defuse\\crypto\\exception\\badformatexception' => '/ThirdParty/defuse/php-encryption/Exception/BadFormatException.php', + 'ncc\\defuse\\crypto\\exception\\cryptoexception' => '/ThirdParty/defuse/php-encryption/Exception/CryptoException.php', + 'ncc\\defuse\\crypto\\exception\\environmentisbrokenexception' => '/ThirdParty/defuse/php-encryption/Exception/EnvironmentIsBrokenException.php', + 'ncc\\defuse\\crypto\\exception\\ioexception' => '/ThirdParty/defuse/php-encryption/Exception/IOException.php', + 'ncc\\defuse\\crypto\\exception\\wrongkeyormodifiedciphertextexception' => '/ThirdParty/defuse/php-encryption/Exception/WrongKeyOrModifiedCiphertextException.php', + 'ncc\\defuse\\crypto\\file' => '/ThirdParty/defuse/php-encryption/File.php', + 'ncc\\defuse\\crypto\\key' => '/ThirdParty/defuse/php-encryption/Key.php', + 'ncc\\defuse\\crypto\\keyorpassword' => '/ThirdParty/defuse/php-encryption/KeyOrPassword.php', + 'ncc\\defuse\\crypto\\keyprotectedbypassword' => '/ThirdParty/defuse/php-encryption/KeyProtectedByPassword.php', + 'ncc\\defuse\\crypto\\runtimetests' => '/ThirdParty/defuse/php-encryption/RuntimeTests.php', 'ncc\\exceptions\\accessdeniedexception' => '/Exceptions/AccessDeniedException.php', 'ncc\\exceptions\\directorynotfoundexception' => '/Exceptions/DirectoryNotFoundException.php', 'ncc\\exceptions\\filenotfoundexception' => '/Exceptions/FileNotFoundException.php', + 'ncc\\exceptions\\invalidcredentialsentryexception' => '/Exceptions/InvalidCredentialsEntryException.php', 'ncc\\exceptions\\invalidprojectconfigurationexception' => '/Exceptions/InvalidProjectConfigurationException.php', 'ncc\\exceptions\\invalidscopeexception' => '/Exceptions/InvalidScopeException.php', 'ncc\\exceptions\\malformedjsonexception' => '/Exceptions/MalformedJsonException.php', 'ncc\\exceptions\\runtimeexception' => '/Exceptions/RuntimeException.php', + 'ncc\\managers\\credentialmanager' => '/Managers/CredentialManager.php', 'ncc\\ncc' => '/ncc.php', 'ncc\\ncc\\ziproto\\typetransformer\\binarytransformer' => '/Extensions/ZiProto/TypeTransformer/BinaryTransformer.php', 'ncc\\ncc\\ziproto\\typetransformer\\extension' => '/Extensions/ZiProto/TypeTransformer/Extension.php',