Added third-party dependency 'defuse\php-encryption'

This commit is contained in:
Netkas 2022-06-26 09:50:42 -04:00
parent cbdbe46b41
commit 32e3999378
17 changed files with 2669 additions and 1 deletions

View file

@ -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'

View file

@ -0,0 +1,457 @@
<?php
namespace ncc\Defuse\Crypto;
use ncc\Defuse\Crypto\Exception as Ex;
final class Core
{
const HEADER_VERSION_SIZE = 4;
const MINIMUM_CIPHERTEXT_SIZE = 84;
const CURRENT_VERSION = "\xDE\xF5\x02\x00";
const CIPHER_METHOD = 'aes-256-ctr';
const BLOCK_BYTE_SIZE = 16;
const KEY_BYTE_SIZE = 32;
const SALT_BYTE_SIZE = 32;
const MAC_BYTE_SIZE = 32;
const HASH_FUNCTION_NAME = 'sha256';
const ENCRYPTION_INFO_STRING = 'DefusePHP|V2|KeyForEncryption';
const AUTHENTICATION_INFO_STRING = 'DefusePHP|V2|KeyForAuthentication';
const BUFFER_BYTE_SIZE = 1048576;
const LEGACY_CIPHER_METHOD = 'aes-128-cbc';
const LEGACY_BLOCK_BYTE_SIZE = 16;
const LEGACY_KEY_BYTE_SIZE = 16;
const LEGACY_HASH_FUNCTION_NAME = 'sha256';
const LEGACY_MAC_BYTE_SIZE = 32;
const LEGACY_ENCRYPTION_INFO_STRING = 'DefusePHP|KeyForEncryption';
const LEGACY_AUTHENTICATION_INFO_STRING = 'DefusePHP|KeyForAuthentication';
/*
* V2.0 Format: VERSION (4 bytes) || SALT (32 bytes) || IV (16 bytes) ||
* CIPHERTEXT (varies) || HMAC (32 bytes)
*
* V1.0 Format: HMAC (32 bytes) || IV (16 bytes) || CIPHERTEXT (varies).
*/
/**
* Adds an integer to a block-sized counter.
*
* @param string $ctr
* @param int $inc
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*
* @psalm-suppress RedundantCondition - It's valid to use is_int to check for overflow.
*/
public static function incrementCounter($ctr, $inc)
{
Core::ensureTrue(
Core::ourStrlen($ctr) === Core::BLOCK_BYTE_SIZE,
'Trying to increment a nonce of the wrong size.'
);
Core::ensureTrue(
\is_int($inc),
'Trying to increment nonce by a non-integer.'
);
// The caller is probably re-using CTR-mode keystream if they increment by 0.
Core::ensureTrue(
$inc > 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));
}
}
}

View file

@ -0,0 +1,439 @@
<?php
namespace ncc\Defuse\Crypto;
use ncc\Defuse\Crypto\Exception as Ex;
class Crypto
{
/**
* Encrypts a string with a Key.
*
* @param string $plaintext
* @param Key $key
* @param bool $raw_binary
*
* @return string
*/
public static function encrypt($plaintext, $key, $raw_binary = false)
{
if (!\is_string($plaintext)) {
throw new \TypeError(
'String expected for argument 1. ' . \ucfirst(\gettype($plaintext)) . ' given instead.'
);
}
if (!($key instanceof Key)) {
throw new \TypeError(
'Key expected for argument 2. ' . \ucfirst(\gettype($key)) . ' given instead.'
);
}
if (!\is_bool($raw_binary)) {
throw new \TypeError(
'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.'
);
}
return self::encryptInternal(
$plaintext,
KeyOrPassword::createFromKey($key),
$raw_binary
);
}
/**
* Encrypts a string with a password, using a slow key derivation function
* to make password cracking more expensive.
*
* @param string $plaintext
* @param string $password
* @param bool $raw_binary
*
* @return string
*/
public static function encryptWithPassword($plaintext, $password, $raw_binary = false)
{
if (!\is_string($plaintext)) {
throw new \TypeError(
'String expected for argument 1. ' . \ucfirst(\gettype($plaintext)) . ' given instead.'
);
}
if (!\is_string($password)) {
throw new \TypeError(
'String expected for argument 2. ' . \ucfirst(\gettype($password)) . ' given instead.'
);
}
if (!\is_bool($raw_binary)) {
throw new \TypeError(
'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.'
);
}
return self::encryptInternal(
$plaintext,
KeyOrPassword::createFromPassword($password),
$raw_binary
);
}
/**
* Decrypts a ciphertext to a string with a Key.
*
* @param string $ciphertext
* @param Key $key
* @param bool $raw_binary
*
* @throws \TypeError
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*
* @return string
*/
public static function decrypt($ciphertext, $key, $raw_binary = false)
{
if (!\is_string($ciphertext)) {
throw new \TypeError(
'String expected for argument 1. ' . \ucfirst(\gettype($ciphertext)) . ' given instead.'
);
}
if (!($key instanceof Key)) {
throw new \TypeError(
'Key expected for argument 2. ' . \ucfirst(\gettype($key)) . ' given instead.'
);
}
if (!\is_bool($raw_binary)) {
throw new \TypeError(
'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.'
);
}
return self::decryptInternal(
$ciphertext,
KeyOrPassword::createFromKey($key),
$raw_binary
);
}
/**
* Decrypts a ciphertext to a string with a password, using a slow key
* derivation function to make password cracking more expensive.
*
* @param string $ciphertext
* @param string $password
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
* @throws \TypeError
*
* @return string
*/
public static function decryptWithPassword($ciphertext, $password, $raw_binary = false)
{
if (!\is_string($ciphertext)) {
throw new \TypeError(
'String expected for argument 1. ' . \ucfirst(\gettype($ciphertext)) . ' given instead.'
);
}
if (!\is_string($password)) {
throw new \TypeError(
'String expected for argument 2. ' . \ucfirst(\gettype($password)) . ' given instead.'
);
}
if (!\is_bool($raw_binary)) {
throw new \TypeError(
'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.'
);
}
return self::decryptInternal(
$ciphertext,
KeyOrPassword::createFromPassword($password),
$raw_binary
);
}
/**
* Decrypts a legacy ciphertext produced by version 1 of this library.
*
* @param string $ciphertext
* @param string $key
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
* @throws \TypeError
*
* @return string
*/
public static function legacyDecrypt($ciphertext, $key)
{
if (!\is_string($ciphertext)) {
throw new \TypeError(
'String expected for argument 1. ' . \ucfirst(\gettype($ciphertext)) . ' given instead.'
);
}
if (!\is_string($key)) {
throw new \TypeError(
'String expected for argument 2. ' . \ucfirst(\gettype($key)) . ' given instead.'
);
}
RuntimeTests::runtimeTest();
// Extract the HMAC from the front of the ciphertext.
if (Core::ourStrlen($ciphertext) <= Core::LEGACY_MAC_BYTE_SIZE) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Ciphertext is too short.'
);
}
/**
* @var string
*/
$hmac = Core::ourSubstr($ciphertext, 0, Core::LEGACY_MAC_BYTE_SIZE);
Core::ensureTrue(\is_string($hmac));
/**
* @var string
*/
$messageCiphertext = Core::ourSubstr($ciphertext, Core::LEGACY_MAC_BYTE_SIZE);
Core::ensureTrue(\is_string($messageCiphertext));
// Regenerate the same authentication sub-key.
$akey = Core::HKDF(
Core::LEGACY_HASH_FUNCTION_NAME,
$key,
Core::LEGACY_KEY_BYTE_SIZE,
Core::LEGACY_AUTHENTICATION_INFO_STRING,
null
);
if (self::verifyHMAC($hmac, $messageCiphertext, $akey)) {
// Regenerate the same encryption sub-key.
$ekey = Core::HKDF(
Core::LEGACY_HASH_FUNCTION_NAME,
$key,
Core::LEGACY_KEY_BYTE_SIZE,
Core::LEGACY_ENCRYPTION_INFO_STRING,
null
);
// Extract the IV from the ciphertext.
if (Core::ourStrlen($messageCiphertext) <= Core::LEGACY_BLOCK_BYTE_SIZE) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Ciphertext is too short.'
);
}
/**
* @var string
*/
$iv = Core::ourSubstr($messageCiphertext, 0, Core::LEGACY_BLOCK_BYTE_SIZE);
Core::ensureTrue(\is_string($iv));
/**
* @var string
*/
$actualCiphertext = Core::ourSubstr($messageCiphertext, Core::LEGACY_BLOCK_BYTE_SIZE);
Core::ensureTrue(\is_string($actualCiphertext));
// Do the decryption.
$plaintext = self::plainDecrypt($actualCiphertext, $ekey, $iv, Core::LEGACY_CIPHER_METHOD);
return $plaintext;
} else {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Integrity check failed.'
);
}
}
/**
* Encrypts a string with either a key or a password.
*
* @param string $plaintext
* @param KeyOrPassword $secret
* @param bool $raw_binary
*
* @return string
*/
private static function encryptInternal($plaintext, KeyOrPassword $secret, $raw_binary)
{
RuntimeTests::runtimeTest();
$salt = Core::secureRandom(Core::SALT_BYTE_SIZE);
$keys = $secret->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);
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace ncc\Defuse\Crypto;
/**
* Class DerivedKeys
* @package ncc\Defuse\Crypto
*/
final class DerivedKeys
{
/**
* @var string
*/
private $akey = '';
/**
* @var string
*/
private $ekey = '';
/**
* Returns the authentication key.
* @return string
*/
public function getAuthenticationKey()
{
return $this->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;
}
}

View file

@ -0,0 +1,269 @@
<?php
namespace ncc\Defuse\Crypto;
use ncc\Defuse\Crypto\Exception as Ex;
final class Encoding
{
const CHECKSUM_BYTE_SIZE = 32;
const CHECKSUM_HASH_ALGO = 'sha256';
const SERIALIZE_HEADER_BYTES = 4;
/**
* Converts a byte string to a hexadecimal string without leaking
* information through side channels.
*
* @param string $byte_string
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function binToHex($byte_string)
{
$hex = '';
$len = Core::ourStrlen($byte_string);
for ($i = 0; $i < $len; ++$i) {
$c = \ord($byte_string[$i]) & 0xf;
$b = \ord($byte_string[$i]) >> 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
);
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace ncc\Defuse\Crypto\Exception;
class BadFormatException extends \ncc\Defuse\Crypto\Exception\CryptoException
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace ncc\Defuse\Crypto\Exception;
class CryptoException extends \Exception
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace ncc\Defuse\Crypto\Exception;
class EnvironmentIsBrokenException extends \ncc\Defuse\Crypto\Exception\CryptoException
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace ncc\Defuse\Crypto\Exception;
class IOException extends \ncc\Defuse\Crypto\Exception\CryptoException
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace ncc\Defuse\Crypto\Exception;
class WrongKeyOrModifiedCiphertextException extends \ncc\Defuse\Crypto\Exception\CryptoException
{
}

View file

@ -0,0 +1,778 @@
<?php
namespace ncc\Defuse\Crypto;
use ncc\Defuse\Crypto\Exception as Ex;
final class File
{
/**
* Encrypts the input file, saving the ciphertext to the output file.
*
* @param string $inputFilename
* @param string $outputFilename
* @param Key $key
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
*/
public static function encryptFile($inputFilename, $outputFilename, Key $key)
{
self::encryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromKey($key)
);
}
/**
* Encrypts a file with a password, using a slow key derivation function to
* make password cracking more expensive.
*
* @param string $inputFilename
* @param string $outputFilename
* @param string $password
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
*/
public static function encryptFileWithPassword($inputFilename, $outputFilename, $password)
{
self::encryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Decrypts the input file, saving the plaintext to the output file.
*
* @param string $inputFilename
* @param string $outputFilename
* @param Key $key
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptFile($inputFilename, $outputFilename, Key $key)
{
self::decryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromKey($key)
);
}
/**
* Decrypts a file with a password, using a slow key derivation function to
* make password cracking more expensive.
*
* @param string $inputFilename
* @param string $outputFilename
* @param string $password
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptFileWithPassword($inputFilename, $outputFilename, $password)
{
self::decryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Takes two resource handles and encrypts the contents of the first,
* writing the ciphertext into the second.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param Key $key
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function encryptResource($inputHandle, $outputHandle, Key $key)
{
self::encryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromKey($key)
);
}
/**
* Encrypts the contents of one resource handle into another with a
* password, using a slow key derivation function to make password cracking
* more expensive.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param string $password
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function encryptResourceWithPassword($inputHandle, $outputHandle, $password)
{
self::encryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Takes two resource handles and decrypts the contents of the first,
* writing the plaintext into the second.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param Key $key
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptResource($inputHandle, $outputHandle, Key $key)
{
self::decryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromKey($key)
);
}
/**
* Decrypts the contents of one resource into another with a password, using
* a slow key derivation function to make password cracking more expensive.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param string $password
* @return void
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptResourceWithPassword($inputHandle, $outputHandle, $password)
{
self::decryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Encrypts a file with either a key or a password.
*
* @param string $inputFilename
* @param string $outputFilename
* @param KeyOrPassword $secret
* @return void
*
* @throws Ex\CryptoException
* @throws Ex\IOException
*/
private static function encryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret)
{
if (file_exists($inputFilename) && file_exists($outputFilename) && realpath($inputFilename) === realpath($outputFilename)) {
throw new Ex\IOException('Input and output filenames must be different.');
}
/* Open the input file. */
$if = @\fopen($inputFilename, 'rb');
if ($if === false) {
throw new Ex\IOException(
'Cannot open input file for encrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_read_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_read_buffer($if, 0);
}
/* Open the output file. */
$of = @\fopen($outputFilename, 'wb');
if ($of === false) {
\fclose($if);
throw new Ex\IOException(
'Cannot open output file for encrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_write_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_write_buffer($of, 0);
}
/* Perform the encryption. */
try {
self::encryptResourceInternal($if, $of, $secret);
} catch (Ex\CryptoException $ex) {
\fclose($if);
\fclose($of);
throw $ex;
}
/* Close the input file. */
if (\fclose($if) === false) {
\fclose($of);
throw new Ex\IOException(
'Cannot close input file after encrypting'
);
}
/* Close the output file. */
if (\fclose($of) === false) {
throw new Ex\IOException(
'Cannot close output file after encrypting'
);
}
}
/**
* Decrypts a file with either a key or a password.
*
* @param string $inputFilename
* @param string $outputFilename
* @param KeyOrPassword $secret
* @return void
*
* @throws Ex\CryptoException
* @throws Ex\IOException
*/
private static function decryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret)
{
if (file_exists($inputFilename) && file_exists($outputFilename) && realpath($inputFilename) === realpath($outputFilename)) {
throw new Ex\IOException('Input and output filenames must be different.');
}
/* Open the input file. */
$if = @\fopen($inputFilename, 'rb');
if ($if === false) {
throw new Ex\IOException(
'Cannot open input file for decrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_read_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_read_buffer($if, 0);
}
/* Open the output file. */
$of = @\fopen($outputFilename, 'wb');
if ($of === false) {
\fclose($if);
throw new Ex\IOException(
'Cannot open output file for decrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_write_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_write_buffer($of, 0);
}
/* Perform the decryption. */
try {
self::decryptResourceInternal($if, $of, $secret);
} catch (Ex\CryptoException $ex) {
\fclose($if);
\fclose($of);
throw $ex;
}
/* Close the input file. */
if (\fclose($if) === false) {
\fclose($of);
throw new Ex\IOException(
'Cannot close input file after decrypting'
);
}
/* Close the output file. */
if (\fclose($of) === false) {
throw new Ex\IOException(
'Cannot close output file after decrypting'
);
}
}
/**
* Encrypts a 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
* @psalm-suppress PossiblyInvalidArgument
* Fixes erroneous errors caused by PHP 7.2 switching the return value
* of hash_init from a resource to a HashContext.
*/
private static function encryptResourceInternal($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!'
);
}
$inputStat = \fstat($inputHandle);
$inputSize = $inputStat['size'];
$file_salt = Core::secureRandom(Core::SALT_BYTE_SIZE);
$keys = $secret->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'];
}
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace ncc\Defuse\Crypto;
use ncc\Defuse\Crypto\Exception as Ex;
final class Key
{
const KEY_CURRENT_VERSION = "\xDE\xF0\x00\x00";
const KEY_BYTE_SIZE = 32;
/**
* @var string
*/
private $key_bytes;
/**
* Creates new random key.
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return Key
*/
public static function createNewRandomKey()
{
return new Key(Core::secureRandom(self::KEY_BYTE_SIZE));
}
/**
* Loads a Key from its encoded form.
*
* By default, this function will call Encoding::trimTrailingWhitespace()
* to remove trailing CR, LF, NUL, TAB, and SPACE characters, which are
* commonly appended to files when working with text editors.
*
* @param string $saved_key_string
* @param bool $do_not_trim (default: false)
*
* @throws Ex\BadFormatException
* @throws Ex\EnvironmentIsBrokenException
*
* @return Key
*/
public static function loadFromAsciiSafeString($saved_key_string, $do_not_trim = false)
{
if (!$do_not_trim) {
$saved_key_string = Encoding::trimTrailingWhitespace($saved_key_string);
}
$key_bytes = Encoding::loadBytesFromChecksummedAsciiSafeString(self::KEY_CURRENT_VERSION, $saved_key_string);
return new Key($key_bytes);
}
/**
* Encodes the Key into a string of printable ASCII characters.
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public function saveToAsciiSafeString()
{
return Encoding::saveBytesToChecksummedAsciiSafeString(
self::KEY_CURRENT_VERSION,
$this->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;
}
}

View file

@ -0,0 +1,149 @@
<?php
namespace ncc\Defuse\Crypto;
use ncc\Defuse\Crypto\Exception as Ex;
final class KeyOrPassword
{
const PBKDF2_ITERATIONS = 100000;
const SECRET_TYPE_KEY = 1;
const SECRET_TYPE_PASSWORD = 2;
/**
* @var int
*/
private $secret_type = 0;
/**
* @var Key|string
*/
private $secret;
/**
* Initializes an instance of KeyOrPassword from a key.
*
* @param Key $key
*
* @return KeyOrPassword
*/
public static function createFromKey(Key $key)
{
return new KeyOrPassword(self::SECRET_TYPE_KEY, $key);
}
/**
* Initializes an instance of KeyOrPassword from a password.
*
* @param string $password
*
* @return KeyOrPassword
*/
public static function createFromPassword($password)
{
return new KeyOrPassword(self::SECRET_TYPE_PASSWORD, $password);
}
/**
* 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\CryptoException
* @throws Ex\EnvironmentIsBrokenException
*
* @return DerivedKeys
*/
public function deriveKeys($salt)
{
Core::ensureTrue(
Core::ourStrlen($salt) === Core::SALT_BYTE_SIZE,
'Bad salt.'
);
if ($this->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;
}
}

View file

@ -0,0 +1,145 @@
<?php
namespace ncc\Defuse\Crypto;
use ncc\Defuse\Crypto\Exception as Ex;
final class KeyProtectedByPassword
{
const PASSWORD_KEY_CURRENT_VERSION = "\xDE\xF1\x00\x00";
/**
* @var string
*/
private $encrypted_key = '';
/**
* Creates a random key protected by the provided password.
*
* @param string $password
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return KeyProtectedByPassword
*/
public static function createRandomPasswordProtectedKey($password)
{
$inner_key = Key::createNewRandomKey();
/* 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, $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;
}
}

View file

@ -0,0 +1,228 @@
<?php
namespace ncc\Defuse\Crypto;
use ncc\Defuse\Crypto\Exception as Ex;
/*
* We're using static class inheritance to get access to protected methods
* inside Crypto. To make it easy to know where the method we're calling can be
* found, within this file, prefix calls with `Crypto::` or `RuntimeTests::`,
* and don't use `self::`.
*/
class RuntimeTests extends Crypto
{
/**
* Runs the runtime tests.
*
* @throws Ex\EnvironmentIsBrokenException
* @return void
*/
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();
Core::ensureTrue(Core::ourStrlen(Key::createNewRandomKey()->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);
}
}

View file

@ -0,0 +1 @@
2.3.1

View file

@ -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',