diff --git a/.gitattributes b/.gitattributes index e57bf9c..88d7e1c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,7 @@ /.gitattributes export-ignore +/.gitignore export-ignore +/.php_cs export-ignore /.travis.yml export-ignore +/psalm.xml export-ignore /test.sh export-ignore /test export-ignore diff --git a/.gitignore b/.gitignore index 987c516..5e47134 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ box.phar phpunit.phar phpunit.phar.asc test/unit/File/tmp +test/.phpunit.result.cache +.idea diff --git a/.travis.yml b/.travis.yml index 1e3713c..e8d1dac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,37 +4,75 @@ sudo: false matrix: fast_finish: true include: - - php: "5.4" - env: USE_PSALM=0 - - php: "5.5" - env: USE_PSALM=0 - php: "5.6" - env: USE_PSALM=0 + env: + - USE_PSALM=0 + - BUILD_PHAR=0 - php: "7.0" - env: USE_PSALM=1 + env: + - USE_PSALM=0 + - BUILD_PHAR=0 - php: "7.1" - env: USE_PSALM=1 + env: + - USE_PSALM=0 + - BUILD_PHAR=0 - php: "7.2" - env: USE_PSALM=1 + env: + - USE_PSALM=0 + - BUILD_PHAR=0 + - php: "7.3" + env: + - USE_PSALM=0 + - BUILD_PHAR=0 + - php: "7.4" + env: + - USE_PSALM=1 + - BUILD_PHAR=0 + - php: "8.0" + # psalm currently doesn't like our \[#SensitiveParameter]s + env: + - USE_PSALM=0 + - BUILD_PHAR=0 + - php: "8.1" + # psalm currently doesn't like our \[#SensitiveParameter]s + env: + - USE_PSALM=0 + - BUILD_PHAR=1 + - php: "8.2" + env: + - USE_PSALM=1 + - BUILD_PHAR=1 + dist: focal - php: "nightly" - env: USE_PSALM=1 + env: + - USE_PSALM=1 + - BUILD_PHAR=1 - php: "hhvm" - env: USE_PSALM=1 + env: + - USE_PSALM=1 + - BUILD_PHAR=1 allow_failures: - php: "nightly" - php: "hhvm" - + # Travis-CI's 8.2 is currently broken, see: + # https://github.com/defuse/php-encryption/pull/506#issuecomment-1594084107 + #- php: "8.2" install: - - if [[ $USE_PSALM -eq 1 ]]; then composer require --dev "vimeo/psalm:dev-master"; fi - composer install - - curl -LSs https://box-project.github.io/box2/installer.php | php - - mkdir ~/box - - mv box.phar ~/box/box - +before_script: + - echo "xdebug.mode = coverage" > extra_php_config.ini + - phpenv config-add extra_php_config.ini script: - ./test.sh - - PATH=$PATH:~/box/ make -C dist/ build-phar - - ./test.sh dist/defuse-crypto.phar + - if [[ $BUILD_PHAR -eq 1 ]]; then mkdir /tmp/box; fi + - if [[ $BUILD_PHAR -eq 1 ]]; then chmod 755 /tmp/box; fi + - if [[ $BUILD_PHAR -eq 1 ]]; then curl -LSs https://github.com/box-project/box/releases/download/4.3.8/box.phar -o /tmp/box/box; fi + - if [[ $BUILD_PHAR -eq 1 ]]; then chmod 755 /tmp/box/box; fi + - if [[ $BUILD_PHAR -eq 1 ]]; then PATH="$PATH:/tmp/box/" which box; fi + - if [[ $BUILD_PHAR -eq 1 ]]; then PATH="$PATH:/tmp/box/" make -C dist/ build-phar; fi + - if [[ $BUILD_PHAR -eq 1 ]]; then ./test.sh dist/phar-testing-autoload.php; fi + - if [[ $USE_PSALM -eq 1 ]]; then composer require --with-all-dependencies --dev "vimeo/psalm:dev-master"; fi + - if [[ $USE_PSALM -eq 1 ]]; then composer install; fi - if [[ $USE_PSALM -eq 1 ]]; then vendor/bin/psalm; fi after_success: diff --git a/LICENSE b/LICENSE index f3e7c8e..aaf2fe9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ The MIT License (MIT) -Copyright (c) 2016 Taylor Hornby and Paragon Initiative -Enterprises . +Copyright (c) 2016 Taylor Hornby +Copyright (c) 2016 Paragon Initiative Enterprises . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index 73100cd..0e76317 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,26 @@ php-encryption =============== -[![Build Status](https://travis-ci.org/defuse/php-encryption.svg?branch=master)](https://travis-ci.org/defuse/php-encryption) +![Build Status](https://app.travis-ci.com/defuse/php-encryption.svg?branch=master) [![codecov](https://codecov.io/gh/defuse/php-encryption/branch/master/graph/badge.svg)](https://codecov.io/gh/defuse/php-encryption) [![Latest Stable Version](https://poser.pugx.org/defuse/php-encryption/v/stable)](https://packagist.org/packages/defuse/php-encryption) -[![Latest Unstable Version](https://poser.pugx.org/defuse/php-encryption/v/unstable)](https://packagist.org/packages/defuse/php-encryption) [![License](https://poser.pugx.org/defuse/php-encryption/license)](https://packagist.org/packages/defuse/php-encryption) [![Downloads](https://img.shields.io/packagist/dt/defuse/php-encryption.svg)](https://packagist.org/packages/defuse/php-encryption) +```terminal +composer require defuse/php-encryption +``` + This is a library for encrypting data with a key or password in PHP. **It -requires PHP 5.6 or newer and OpenSSL 1.0.1 or newer.** The current version is -v2.2.0, which is expected to remain stable and supported by its authors with -security and bugfixes until at least January 1st, 2020. +requires PHP 5.6 or newer and OpenSSL 1.0.1 or newer.** We recommend using a +version of PHP that [still has security +support](https://www.php.net/supported-versions.php), which at the time of +writing means PHP 8.0 or later. Using this library with an unsupported +version of PHP could lead to security vulnerabilities. + +The current version of `php-encryption` is v2.4.0. This library is expected to +remain stable and supported by its authors with security and bugfixes until at +least January 1st, 2024. The library is a joint effort between [Taylor Hornby](https://defuse.ca/) and [Scott Arciszewski](https://paragonie.com/blog/author/scott-arcizewski) as well @@ -90,13 +99,26 @@ a formal audit, please [contact Taylor Hornby](https://defuse.ca/contact.htm). Public Keys ------------ -The GnuPG public key used to sign releases is available in -[dist/signingkey.asc](https://github.com/defuse/php-encryption/raw/master/dist/signingkey.asc). Its fingerprint is: +The GnuPG public key used to sign the current and new releases is available in +[dist/signingkey-new.asc](https://github.com/defuse/php-encryption/raw/master/dist/signingkey-new.asc). Its fingerprint is: ``` -2FA6 1D8D 99B9 2658 6BAC 3D53 385E E055 A129 1538 +6DD6 E677 0281 5846 FC85 25A3 DD2E 507F 7BDB 1669 ``` You can verify it against Taylor Hornby's [contact page](https://defuse.ca/contact.htm) and +[twitter](https://twitter.com/DefuseSec/status/1670840796743081984). + +Older releases were signed with a (now-expired) available in +[dist/signingkey-old.asc](https://github.com/defuse/php-encryption/raw/master/dist/signingkey-old.asc). The old key's fingerprint is: + +``` +2FA6 1D8D 99B9 2658 6BAC 3D53 385E E055 A129 1538 +``` + +The old key's fingerprint can be verified against Taylor Hornby's [contact page](https://defuse.ca/contact.htm) and [twitter](https://twitter.com/DefuseSec/status/723741424253059074). + +A signature of this new key by the old key is available in +[dist/signingkey-new.asc.sig](https://github.com/defuse/php-encryption/raw/master/dist/signingkey-new.asc.sig). diff --git a/composer.json b/composer.json index e90db62..29a1d52 100644 --- a/composer.json +++ b/composer.json @@ -21,13 +21,13 @@ } }, "require": { - "paragonie/random_compat": "~2.0", + "paragonie/random_compat": ">= 2", "ext-openssl": "*", - "php": ">=5.4.0" + "php": ">=5.6.0" }, "require-dev": { - "phpunit/phpunit": "^4|^5", - "nikic/php-parser": "^2.0|^3.0|^4.0" + "yoast/phpunit-polyfills": "^2.0.0", + "phpunit/phpunit": "^5|^6|^7|^8|^9|^10" }, "bin": [ "bin/generate-defuse-key" diff --git a/dist/Makefile b/dist/Makefile index 4b65c9e..bffe616 100644 --- a/dist/Makefile +++ b/dist/Makefile @@ -2,14 +2,15 @@ # must be installed and in your $PATH. Run it from inside the dist/ directory. box := $(shell which box) -composer := "composer" +composer := $(shell which composer) +gitcommit := $(shell git rev-parse HEAD) .PHONY: all all: build-phar .PHONY: sign-phar sign-phar: - gpg -u 7B4B2D98 --armor --output defuse-crypto.phar.sig --detach-sig defuse-crypto.phar + gpg -u DD2E507F7BDB1669 --armor --output defuse-crypto.phar.sig --detach-sig defuse-crypto.phar # ensure we run in clean tree. export git tree and run there. .PHONY: build-phar @@ -30,8 +31,9 @@ clean: defuse-crypto.phar: dist/box.json composer.lock cp dist/box.json . - php -d phar.readonly=0 $(box) build -c box.json -v + php $(box) compile -c box.json -v composer.lock: + $(composer) config autoloader-suffix $(gitcommit) $(composer) install --no-dev diff --git a/dist/box.json b/dist/box.json index f225f78..3c81e95 100644 --- a/dist/box.json +++ b/dist/box.json @@ -15,9 +15,6 @@ "exclude": "other" } ], - "compactors": [ - "Herrera\\Box\\Compactor\\Php" - ], "main": "vendor/autoload.php", "output": "defuse-crypto.phar", "shebang": false, diff --git a/dist/phar-testing-autoload.php b/dist/phar-testing-autoload.php new file mode 100644 index 0000000..433395a --- /dev/null +++ b/dist/phar-testing-autoload.php @@ -0,0 +1,4 @@ + diff --git a/dist/signingkey-new.asc b/dist/signingkey-new.asc new file mode 100644 index 0000000..7e3a4a1 --- /dev/null +++ b/dist/signingkey-new.asc @@ -0,0 +1,53 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=Ysgx +-----END PGP PUBLIC KEY BLOCK----- diff --git a/dist/signingkey-new.asc.sig b/dist/signingkey-new.asc.sig new file mode 100644 index 0000000..56e1f3a Binary files /dev/null and b/dist/signingkey-new.asc.sig differ diff --git a/dist/signingkey.asc b/dist/signingkey-old.asc similarity index 100% rename from dist/signingkey.asc rename to dist/signingkey-old.asc diff --git a/docs/InternalDeveloperDocs.md b/docs/InternalDeveloperDocs.md index 19b0086..75d4a2c 100644 --- a/docs/InternalDeveloperDocs.md +++ b/docs/InternalDeveloperDocs.md @@ -111,7 +111,7 @@ Check out the branch you want to release: git checkout ``` -Check that the version number in composer.json is correct: +Check that the version number in composer.json is correct (or not specified so that it gets picked up from the git tag): ``` cat composer.json @@ -141,7 +141,7 @@ Test the `.phar`: ``` cd ../ -./test.sh dist/defuse-crypto.phar +./test.sh dist/phar-testing-autoload.php ``` Sign the `.phar`: @@ -154,7 +154,7 @@ make sign-phar Tag the release: ``` -git -c user.signingkey=7B4B2D98 tag -s "" -m "" +git -c user.signingkey=DD2E507F7BDB1669 tag -s "" -m "" ``` `` should be in the format `v2.0.0` and `` should look diff --git a/docs/Tutorial.md b/docs/Tutorial.md index f130caf..a40cac3 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -229,10 +229,10 @@ application is using don't compute it either. Then, when the user logs in, Dave's code will load the protected key from the user's account record, unlock it to get a `Key` object, and save the `Key` -object somewhere safe (like temporary memory-backed session storage). Note that -wherever Dave's code saves the key, it must be destroyed once the user logs out, -or else the attacker might be able to find users' keys even if they were never -logged in during the attack. +object somewhere safe (like temporary memory-backed session storage or +a cookie). Note that wherever Dave's code saves the key, it must be destroyed +once the user logs out, or else the attacker might be able to find users' keys +even if they were never logged in during the attack. ```php unlockKey($password); $user_key_encoded = $user_key->saveToAsciiSafeString(); -// ... save $user_key_encoded in the session +// ... save $user_key_encoded in a cookie ``` ```php @@ -255,7 +255,7 @@ $user_key_encoded = $user_key->saveToAsciiSafeString(); ``` When a user adds their credit card number, Dave's code will get the key from the -session and use it to encrypt the credit card number: +memory-backed session or cookie and use it to encrypt the credit card number: ```php + + + + diff --git a/src/Core.php b/src/Core.php index 3506cda..3fc20ab 100644 --- a/src/Core.php +++ b/src/Core.php @@ -98,9 +98,14 @@ public static function incrementCounter($ctr, $inc) */ public static function secureRandom($octets) { + if ($octets <= 0) { + throw new Ex\CryptoException( + 'A zero or negative amount of random bytes was requested.' + ); + } self::ensureFunctionExists('random_bytes'); try { - return \random_bytes($octets); + return \random_bytes(max(1, $octets)); } catch (\Exception $ex) { throw new Ex\EnvironmentIsBrokenException( 'Your system does not have a secure random number generator.' @@ -228,7 +233,10 @@ public static function hashEquals($expected, $given) */ public static function ensureConstantExists($name) { - Core::ensureTrue(\defined($name)); + Core::ensureTrue( + \defined($name), + 'Constant '.$name.' does not exists' + ); } /** @@ -241,7 +249,10 @@ public static function ensureConstantExists($name) */ public static function ensureFunctionExists($name) { - Core::ensureTrue(\function_exists($name)); + Core::ensureTrue( + \function_exists($name), + 'function '.$name.' does not exists' + ); } /** @@ -279,7 +290,7 @@ 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; + $exists = \extension_loaded('mbstring') && \function_exists('mb_strlen'); } if ($exists) { $length = \mb_strlen($str, '8bit'); @@ -305,7 +316,7 @@ 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; + $exists = \extension_loaded('mbstring') && \function_exists('mb_substr'); } // This is required to make mb_substr behavior identical to substr. @@ -375,7 +386,15 @@ public static function ourSubstr($str, $start, $length = null) * * @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) + public static function pbkdf2( + $algorithm, + #[\SensitiveParameter] + $password, + $salt, + $count, + $key_length, + $raw_output = false + ) { // Type checks: if (! \is_string($algorithm)) { @@ -434,6 +453,9 @@ public static function pbkdf2($algorithm, $password, $salt, $count, $key_length, $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; diff --git a/src/Crypto.php b/src/Crypto.php index 86eb204..f148db4 100644 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -55,7 +55,12 @@ public static function encrypt($plaintext, $key, $raw_binary = false) * * @return string */ - public static function encryptWithPassword($plaintext, $password, $raw_binary = false) + public static function encryptWithPassword( + $plaintext, + #[\SensitiveParameter] + $password, + $raw_binary = false + ) { if (!\is_string($plaintext)) { throw new \TypeError( @@ -130,7 +135,12 @@ public static function decrypt($ciphertext, $key, $raw_binary = false) * * @return string */ - public static function decryptWithPassword($ciphertext, $password, $raw_binary = false) + public static function decryptWithPassword( + $ciphertext, + #[\SensitiveParameter] + $password, + $raw_binary = false + ) { if (!\is_string($ciphertext)) { throw new \TypeError( @@ -166,7 +176,11 @@ public static function decryptWithPassword($ciphertext, $password, $raw_binary = * * @return string */ - public static function legacyDecrypt($ciphertext, $key) + public static function legacyDecrypt( + $ciphertext, + #[\SensitiveParameter] + $key + ) { if (!\is_string($ciphertext)) { throw new \TypeError( @@ -378,7 +392,13 @@ private static function decryptInternal($ciphertext, KeyOrPassword $secret, $raw * * @return string */ - protected static function plainEncrypt($plaintext, $key, $iv) + protected static function plainEncrypt( + $plaintext, + #[\SensitiveParameter] + $key, + #[\SensitiveParameter] + $iv + ) { Core::ensureConstantExists('OPENSSL_RAW_DATA'); Core::ensureFunctionExists('openssl_encrypt'); @@ -408,7 +428,14 @@ protected static function plainEncrypt($plaintext, $key, $iv) * * @return string */ - protected static function plainDecrypt($ciphertext, $key, $iv, $cipherMethod) + protected static function plainDecrypt( + $ciphertext, + #[\SensitiveParameter] + $key, + #[\SensitiveParameter] + $iv, + $cipherMethod + ) { Core::ensureConstantExists('OPENSSL_RAW_DATA'); Core::ensureFunctionExists('openssl_decrypt'); @@ -437,7 +464,12 @@ protected static function plainDecrypt($ciphertext, $key, $iv, $cipherMethod) * * @return bool */ - protected static function verifyHMAC($expected_hmac, $message, $key) + protected static function verifyHMAC( + $expected_hmac, + $message, + #[\SensitiveParameter] + $key + ) { $message_hmac = \hash_hmac(Core::HASH_FUNCTION_NAME, $message, $key, true); return Core::hashEquals($message_hmac, $expected_hmac); diff --git a/src/Encoding.php b/src/Encoding.php index 8f933cf..b14044f 100644 --- a/src/Encoding.php +++ b/src/Encoding.php @@ -46,6 +46,7 @@ public static function binToHex($byte_string) * @throws Ex\EnvironmentIsBrokenException * * @return string + * @psalm-suppress TypeDoesNotContainType */ public static function hexToBin($hex_string) { @@ -174,7 +175,11 @@ public static function trimTrailingWhitespace($string = '') * * @return string */ - public static function saveBytesToChecksummedAsciiSafeString($header, $bytes) + public static function saveBytesToChecksummedAsciiSafeString( + $header, + #[\SensitiveParameter] + $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. @@ -206,7 +211,11 @@ public static function saveBytesToChecksummedAsciiSafeString($header, $bytes) * * @return string */ - public static function loadBytesFromChecksummedAsciiSafeString($expected_header, $string) + public static function loadBytesFromChecksummedAsciiSafeString( + $expected_header, + #[\SensitiveParameter] + $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. diff --git a/src/File.php b/src/File.php index 0d4ed74..caf32ee 100644 --- a/src/File.php +++ b/src/File.php @@ -38,7 +38,12 @@ public static function encryptFile($inputFilename, $outputFilename, Key $key) * @throws Ex\EnvironmentIsBrokenException * @throws Ex\IOException */ - public static function encryptFileWithPassword($inputFilename, $outputFilename, $password) + public static function encryptFileWithPassword( + $inputFilename, + $outputFilename, + #[\SensitiveParameter] + $password + ) { self::encryptFileInternal( $inputFilename, @@ -81,7 +86,12 @@ public static function decryptFile($inputFilename, $outputFilename, Key $key) * @throws Ex\IOException * @throws Ex\WrongKeyOrModifiedCiphertextException */ - public static function decryptFileWithPassword($inputFilename, $outputFilename, $password) + public static function decryptFileWithPassword( + $inputFilename, + $outputFilename, + #[\SensitiveParameter] + $password + ) { self::decryptFileInternal( $inputFilename, @@ -125,7 +135,12 @@ public static function encryptResource($inputHandle, $outputHandle, Key $key) * @throws Ex\IOException * @throws Ex\WrongKeyOrModifiedCiphertextException */ - public static function encryptResourceWithPassword($inputHandle, $outputHandle, $password) + public static function encryptResourceWithPassword( + $inputHandle, + $outputHandle, + #[\SensitiveParameter] + $password + ) { self::encryptResourceInternal( $inputHandle, @@ -169,7 +184,12 @@ public static function decryptResource($inputHandle, $outputHandle, Key $key) * @throws Ex\IOException * @throws Ex\WrongKeyOrModifiedCiphertextException */ - public static function decryptResourceWithPassword($inputHandle, $outputHandle, $password) + public static function decryptResourceWithPassword( + $inputHandle, + $outputHandle, + #[\SensitiveParameter] + $password + ) { self::decryptResourceInternal( $inputHandle, @@ -191,8 +211,14 @@ public static function decryptResourceWithPassword($inputHandle, $outputHandle, */ 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. */ + self::removePHPUnitErrorHandler(); $if = @\fopen($inputFilename, 'rb'); + self::restorePHPUnitErrorHandler(); if ($if === false) { throw new Ex\IOException( 'Cannot open input file for encrypting: ' . @@ -205,7 +231,9 @@ private static function encryptFileInternal($inputFilename, $outputFilename, Key } /* Open the output file. */ + self::removePHPUnitErrorHandler(); $of = @\fopen($outputFilename, 'wb'); + self::restorePHPUnitErrorHandler(); if ($of === false) { \fclose($if); throw new Ex\IOException( @@ -256,8 +284,14 @@ private static function encryptFileInternal($inputFilename, $outputFilename, Key */ 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. */ + self::removePHPUnitErrorHandler(); $if = @\fopen($inputFilename, 'rb'); + self::restorePHPUnitErrorHandler(); if ($if === false) { throw new Ex\IOException( 'Cannot open input file for decrypting: ' . @@ -271,7 +305,9 @@ private static function decryptFileInternal($inputFilename, $outputFilename, Key } /* Open the output file. */ + self::removePHPUnitErrorHandler(); $of = @\fopen($outputFilename, 'wb'); + self::restorePHPUnitErrorHandler(); if ($of === false) { \fclose($if); throw new Ex\IOException( @@ -320,6 +356,9 @@ private static function decryptFileInternal($inputFilename, $outputFilename, Key * * @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) { @@ -346,7 +385,7 @@ private static function encryptResourceInternal($inputHandle, $outputHandle, Key $iv = Core::secureRandom($ivsize); /* Initialize a streaming HMAC state. */ - /** @var resource $hmac */ + /** @var mixed $hmac */ $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey); Core::ensureTrue( \is_resource($hmac) || \is_object($hmac), @@ -369,6 +408,9 @@ private static function encryptResourceInternal($inputHandle, $outputHandle, Key $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. */ @@ -436,6 +478,9 @@ private static function encryptResourceInternal($inputHandle, $outputHandle, Key * @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) { @@ -486,10 +531,13 @@ public static function decryptResourceInternal($inputHandle, $outputHandle, KeyO $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) === false) { + if (\fseek($inputHandle, (-1 * Core::MAC_BYTE_SIZE), SEEK_END) === -1) { throw new Ex\IOException( 'Cannot seek to beginning of MAC within input file' ); @@ -511,19 +559,19 @@ public static function decryptResourceInternal($inputHandle, $outputHandle, KeyO $stored_mac = self::readBytes($inputHandle, Core::MAC_BYTE_SIZE); /* Initialize a streaming HMAC state. */ - /** @var resource $hmac */ + /** @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) === false) { + 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) === false) { + if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize, SEEK_CUR) === -1) { throw new Ex\IOException( 'Cannot seek input file to beginning of ciphertext' ); @@ -534,7 +582,7 @@ public static function decryptResourceInternal($inputHandle, $outputHandle, KeyO \hash_update($hmac, $header); \hash_update($hmac, $file_salt); \hash_update($hmac, $iv); - /** @var resource $hmac2 */ + /** @var mixed $hmac2 */ $hmac2 = \hash_copy($hmac); $break = false; @@ -565,7 +613,7 @@ public static function decryptResourceInternal($inputHandle, $outputHandle, KeyO \hash_update($hmac, $read); /* Remember this buffer-sized chunk's HMAC. */ - /** @var resource $chunk_mac */ + /** @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); @@ -585,7 +633,7 @@ public static function decryptResourceInternal($inputHandle, $outputHandle, KeyO /* 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) === false) { + 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' ); @@ -619,7 +667,7 @@ public static function decryptResourceInternal($inputHandle, $outputHandle, KeyO * remembered from pass #1 to ensure attackers didn't change the * ciphertext after MAC verification. */ \hash_update($hmac2, $read); - /** @var resource $calc_mac */ + /** @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); @@ -670,8 +718,6 @@ public static function decryptResourceInternal($inputHandle, $outputHandle, KeyO * * @throws Ex\IOException * @throws Ex\EnvironmentIsBrokenException - * - * @return string */ public static function readBytes($stream, $num_bytes) { @@ -711,8 +757,6 @@ public static function readBytes($stream, $num_bytes) * @return int * * @throws Ex\IOException - * - * @return string */ public static function writeBytes($stream, $buf, $num_bytes = null) { @@ -754,9 +798,38 @@ private static function getLastErrorMessage() { $error = error_get_last(); if ($error === null) { - return '[no PHP error]'; + return '[no PHP error, or you have a custom error handler set]'; } else { return $error['message']; } } + + /** + * PHPUnit sets an error handler, which prevents getLastErrorMessage() from working, + * because error_get_last does not work when custom handlers are set. + * + * This is a workaround, which should be a no-op in production deployments, to make + * getLastErrorMessage() return the error messages that the PHPUnit tests expect. + * + * If, in a production deployment, a custom error handler is set, the exception + * handling will still work as usual, but the error messages will be confusing. + * + * @return void + */ + private static function removePHPUnitErrorHandler() { + if (defined('PHPUNIT_COMPOSER_INSTALL') || defined('__PHPUNIT_PHAR__')) { + set_error_handler(null); + } + } + + /** + * Undoes what removePHPUnitErrorHandler did. + * + * @return void + */ + private static function restorePHPUnitErrorHandler() { + if (defined('PHPUNIT_COMPOSER_INSTALL') || defined('__PHPUNIT_PHAR__')) { + restore_error_handler(); + } + } } diff --git a/src/Key.php b/src/Key.php index 27b919f..1767d1f 100644 --- a/src/Key.php +++ b/src/Key.php @@ -41,7 +41,11 @@ public static function createNewRandomKey() * * @return Key */ - public static function loadFromAsciiSafeString($saved_key_string, $do_not_trim = false) + public static function loadFromAsciiSafeString( + #[\SensitiveParameter] + $saved_key_string, + $do_not_trim = false + ) { if (!$do_not_trim) { $saved_key_string = Encoding::trimTrailingWhitespace($saved_key_string); @@ -82,7 +86,10 @@ public function getRawBytes() * * @throws Ex\EnvironmentIsBrokenException */ - private function __construct($bytes) + private function __construct( + #[\SensitiveParameter] + $bytes + ) { Core::ensureTrue( Core::ourStrlen($bytes) === self::KEY_BYTE_SIZE, diff --git a/src/KeyOrPassword.php b/src/KeyOrPassword.php index 890b2c2..3850655 100644 --- a/src/KeyOrPassword.php +++ b/src/KeyOrPassword.php @@ -39,7 +39,10 @@ public static function createFromKey(Key $key) * * @return KeyOrPassword */ - public static function createFromPassword($password) + public static function createFromPassword( + #[\SensitiveParameter] + $password + ) { return new KeyOrPassword(self::SECRET_TYPE_PASSWORD, $password); } @@ -133,7 +136,11 @@ public function deriveKeys($salt) * @param int $secret_type * @param mixed $secret (either a Key or a password string) */ - private function __construct($secret_type, $secret) + private function __construct( + $secret_type, + #[\SensitiveParameter] + $secret + ) { // The constructor is private, so these should never throw. if ($secret_type === self::SECRET_TYPE_KEY) { diff --git a/src/KeyProtectedByPassword.php b/src/KeyProtectedByPassword.php index 347bdd9..7d90ed1 100644 --- a/src/KeyProtectedByPassword.php +++ b/src/KeyProtectedByPassword.php @@ -22,7 +22,10 @@ final class KeyProtectedByPassword * * @return KeyProtectedByPassword */ - public static function createRandomPasswordProtectedKey($password) + public static function createRandomPasswordProtectedKey( + #[\SensitiveParameter] + $password + ) { $inner_key = Key::createNewRandomKey(); /* The password is hashed as a form of poor-man's domain separation @@ -47,7 +50,10 @@ public static function createRandomPasswordProtectedKey($password) * * @return KeyProtectedByPassword */ - public static function loadFromAsciiSafeString($saved_key_string) + public static function loadFromAsciiSafeString( + #[\SensitiveParameter] + $saved_key_string + ) { $encrypted_key = Encoding::loadBytesFromChecksummedAsciiSafeString( self::PASSWORD_KEY_CURRENT_VERSION, @@ -82,7 +88,10 @@ public function saveToAsciiSafeString() * @param string $password * @return Key */ - public function unlockKey($password) + public function unlockKey( + #[\SensitiveParameter] + $password + ) { try { $inner_key_encoded = Crypto::decryptWithPassword( @@ -115,7 +124,12 @@ public function unlockKey($password) * * @return KeyProtectedByPassword */ - public function changePassword($current_password, $new_password) + public function changePassword( + #[\SensitiveParameter] + $current_password, + #[\SensitiveParameter] + $new_password + ) { $inner_key = $this->unlockKey($current_password); /* The password is hashed as a form of poor-man's domain separation diff --git a/test/phpunit-10.xml b/test/phpunit-10.xml new file mode 100644 index 0000000..c1808f0 --- /dev/null +++ b/test/phpunit-10.xml @@ -0,0 +1,9 @@ + + + + ../src + + + + + diff --git a/test/phpunit-5.xml b/test/phpunit-5.xml new file mode 100644 index 0000000..f76c419 --- /dev/null +++ b/test/phpunit-5.xml @@ -0,0 +1,7 @@ + + + + ../src + + + diff --git a/test/phpunit-8.xml b/test/phpunit-8.xml new file mode 100644 index 0000000..03d8998 --- /dev/null +++ b/test/phpunit-8.xml @@ -0,0 +1,7 @@ + + + + ../src + + + diff --git a/test/phpunit.sh b/test/phpunit.sh index 098e5aa..4de7df6 100755 --- a/test/phpunit.sh +++ b/test/phpunit.sh @@ -33,19 +33,12 @@ if [ "$clean" -eq 1 ]; then fi # Let's grab the latest release and its signature +phpunitversion=$([ $PHP_VERSION -ge 80100 ] && echo "10" || ([ $PHP_VERSION -ge 70200 ] && echo "8" || echo "5")) if [ ! -f phpunit.phar ]; then - if [[ $PHP_VERSION -ge 50600 ]]; then - wget -O phpunit.phar https://phar.phpunit.de/phpunit-5.7.phar - else - wget -O phpunit.phar https://phar.phpunit.de/phpunit-4.8.phar - fi + wget -O phpunit.phar https://phar.phpunit.de/phpunit-$phpunitversion.phar fi if [ ! -f phpunit.phar.asc ]; then - if [[ $PHP_VERSION -ge 50600 ]]; then - wget -O phpunit.phar.asc https://phar.phpunit.de/phpunit-5.7.phar.asc - else - wget -O phpunit.phar.asc https://phar.phpunit.de/phpunit-4.8.phar.asc - fi + wget -O phpunit.phar.asc https://phar.phpunit.de/phpunit-$phpunitversion.phar.asc fi # What are the major/minor versions? @@ -56,17 +49,20 @@ gpg --verify phpunit.phar.asc phpunit.phar if [ $? -eq 0 ]; then echo if [ "$2" -eq "1" ]; then - COVERAGE1_ARGS="--coverage-clover=$parentdir/coverage1.xml -c $parentdir/test/phpunit.xml" - COVERAGE2_ARGS="--coverage-clover=$parentdir/coverage2.xml -c $parentdir/test/phpunit.xml" + COVERAGE1_ARGS="--coverage-clover=$parentdir/coverage1.xml" + COVERAGE2_ARGS="--coverage-clover=$parentdir/coverage2.xml" else COVERAGE1_ARGS="" COVERAGE2_ARGS="" fi echo -e "\033[33mBegin Unit Testing\033[0m" # Run the test suite with normal func_overload. - php -d mbstring.func_overload=0 phpunit.phar $COVERAGE1_ARGS --bootstrap "$parentdir/$1" "$parentdir/test/unit" && \ + php -d mbstring.func_overload=0 phpunit.phar -c "$parentdir/test/phpunit-$phpunitversion.xml" $COVERAGE1_ARGS --bootstrap "$parentdir/$1" "$parentdir/test/unit" && \ # Run the test suite again with funky func_overload. - php -d mbstring.func_overload=7 phpunit.phar $COVERAGE2_ARGS --bootstrap "$parentdir/$1" "$parentdir/test/unit" + # This is deprecated in PHP 7 and PHPUnit is no longer compatible with the options. + if [[ $PHP_VERSION -le 50600 ]]; then + php -d mbstring.func_overload=7 phpunit.phar -c "$parentdir/test/phpunit-$phpunitversion.xml" $COVERAGE2_ARGS --bootstrap "$parentdir/$1" "$parentdir/test/unit" + fi EXITCODE=$? # Cleanup if [ "$clean" -eq 1 ]; then diff --git a/test/phpunit.xml b/test/phpunit.xml deleted file mode 100644 index 2808abe..0000000 --- a/test/phpunit.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - ../src - - - diff --git a/test/unit/BackwardsCompatibilityTest.php b/test/unit/BackwardsCompatibilityTest.php index 5556f97..3abe5f2 100644 --- a/test/unit/BackwardsCompatibilityTest.php +++ b/test/unit/BackwardsCompatibilityTest.php @@ -3,25 +3,22 @@ use \Defuse\Crypto\Crypto; use \Defuse\Crypto\Encoding; use \Defuse\Crypto\Key; +use Yoast\PHPUnitPolyfills\TestCases\TestCase; -class BackwardsCompatibilityTest extends PHPUnit_Framework_TestCase +class BackwardsCompatibilityTest extends TestCase { - /* helper function to create a key with raw bytes */ - public function keyHelper($rawkey) { - $key = Key::createNewRandomKey(); - $func = function ($bytes) { - $this->key_bytes = $bytes; - }; - $helper = $func->bindTo($key,$key); - $helper($rawkey); - return $key; - } + /* helper function to create a key with raw bytes */ + public function keyHelper($rawkey) { + $key = Key::createNewRandomKey(); + $func = function ($bytes) { + $this->key_bytes = $bytes; + }; + $helper = $func->bindTo($key,$key); + $helper($rawkey); + return $key; + } - /** - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException - * @expectedExceptionMessage invalid hex encoding - */ public function testDecryptLegacyWithWrongMethodStraightUpHex() { $cipher = Encoding::hexToBin( @@ -34,6 +31,9 @@ public function testDecryptLegacyWithWrongMethodStraightUpHex() '00000000000000000000000000000000' ); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); + $this->expectExceptionMessage('invalid hex encoding'); + /* Make it try to parse the binary as hex. */ $plain = Crypto::decrypt( $cipher, @@ -45,10 +45,6 @@ public function testDecryptLegacyWithWrongMethodStraightUpHex() ); } - /** - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException - * @expectedExceptionMessage Bad version header - */ public function testDecryptLegacyWithWrongMethodStraightUpBinary() { $cipher = Encoding::hexToBin( @@ -61,6 +57,9 @@ public function testDecryptLegacyWithWrongMethodStraightUpBinary() '00000000000000000000000000000000' ); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); + $this->expectExceptionMessage('Bad version header'); + /* This time, treat the binary as binary. */ $plain = Crypto::decrypt( $cipher, diff --git a/test/unit/CoreTest.php b/test/unit/CoreTest.php index 5873f9b..69cfe08 100644 --- a/test/unit/CoreTest.php +++ b/test/unit/CoreTest.php @@ -1,8 +1,9 @@ assertSame( - false, - substr('abc', 5, 2) - ); + if (PHP_VERSION_ID < 80000) { + // In PHP 8.0, substr starts returning '' instead of false. + // Core::ourSubstr should behave the OLD way. + $this->assertSame( + false, + substr('abc', 5, 2) + ); + } // Confirm that mb_substr does not have that behavior. if (function_exists('mb_substr')) { @@ -107,11 +112,9 @@ public function testOurSubstrOutOfBorders() ); } - /** - * @expectedException \InvalidArgumentException - */ public function testOurSubstrNegativeLength() { + $this->expectException(\InvalidArgumentException::class); Core::ourSubstr('abc', 0, -1); } @@ -124,4 +127,24 @@ public function testOurSubstrLengthIsMax() { $this->assertSame('bc', Core::ourSubstr('abc', 1, 500)); } + + public function testSecureRandomZeroLength() + { + $this->expectException(\Defuse\Crypto\Exception\CryptoException::class); + $this->expectExceptionMessage('zero or negative'); + Core::secureRandom(0); + } + + public function testSecureRandomNegativeLength() + { + $this->expectException(\Defuse\Crypto\Exception\CryptoException::class); + $this->expectExceptionMessage('zero or negative'); + Core::secureRandom(-1); + } + + public function testSecureRandomPositiveLength() + { + $x = Core::secureRandom(10); + $this->assertSame(10, strlen($x)); + } } diff --git a/test/unit/CryptoTest.php b/test/unit/CryptoTest.php index e94153d..ea81fb8 100644 --- a/test/unit/CryptoTest.php +++ b/test/unit/CryptoTest.php @@ -3,9 +3,10 @@ use \Defuse\Crypto\Core; use \Defuse\Crypto\Crypto; use \Defuse\Crypto\Key; +use Yoast\PHPUnitPolyfills\TestCases\TestCase; use Defuse\Crypto\Exception as Ex; -class CryptoTest extends PHPUnit_Framework_TestCase +class CryptoTest extends TestCase { # Test for issue #165 -- encrypting then decrypting empty string fails. public function testEmptyString() @@ -93,137 +94,105 @@ public function testEncryptDecryptWithPassword() } } - /** - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException - */ public function testDecryptRawAsHex() { $ciphertext = Crypto::encryptWithPassword('testdata', 'password', true); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); Crypto::decryptWithPassword($ciphertext, 'password', false); } - /** - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException - */ public function testDecryptHexAsRaw() { $ciphertext = Crypto::encryptWithPassword('testdata', 'password', false); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); Crypto::decryptWithPassword($ciphertext, 'password', true); } - /** - * @expectedException \TypeError - */ public function testEncryptTypeErrorA() { $key = Key::createNewRandomKey(); + $this->expectException(\TypeError::class); Crypto::encrypt(3, $key, false); } - /** - * @expectedException \TypeError - */ public function testEncryptTypeErrorB() { + $this->expectException(\TypeError::class); Crypto::encrypt("plaintext", 3, false); } - /** - * @expectedException \TypeError - */ public function testEncryptTypeErrorC() { $key = Key::createNewRandomKey(); + $this->expectException(\TypeError::class); Crypto::encrypt("plaintext", $key, 3); } - /** - * @expectedException \TypeError - */ public function testEncryptWithPasswordTypeErrorA() { + $this->expectException(\TypeError::class); Crypto::encryptWithPassword(3, "password", false); } - /** - * @expectedException \TypeError - */ public function testEncryptWithPasswordTypeErrorB() { + $this->expectException(\TypeError::class); Crypto::encryptWithPassword("plaintext", 3, false); } - /** - * @expectedException \TypeError - */ public function testEncryptWithPasswordTypeErrorC() { + $this->expectException(\TypeError::class); Crypto::encryptWithPassword("plaintext", "password", 3); } - /** - * @expectedException \TypeError - */ public function testDecryptTypeErrorA() { $key = Key::createNewRandomKey(); + $this->expectException(\TypeError::class); Crypto::decrypt(3, $key, false); } - /** - * @expectedException \TypeError - */ public function testDecryptTypeErrorB() { + $this->expectException(\TypeError::class); Crypto::decrypt("ciphertext", 3, false); } - /** - * @expectedException \TypeError - */ public function testDecryptTypeErrorC() { $key = Key::createNewRandomKey(); + $this->expectException(\TypeError::class); Crypto::decrypt("ciphertext", $key, 3); } - /** - * @expectedException \TypeError - */ public function testDecryptWithPasswordTypeErrorA() { + $this->expectException(\TypeError::class); Crypto::decryptWithPassword(3, "password", false); } - /** - * @expectedException \TypeError - */ public function testDecryptWithPasswordTypeErrorB() { + $this->expectException(\TypeError::class); Crypto::decryptWithPassword("ciphertext", 3, false); } - /** - * @expectedException \TypeError - */ public function testDecryptWithPasswordTypeErrorC() { + $this->expectException(\TypeError::class); Crypto::decryptWithPassword("ciphertext", "password", 3); } - /** - * @expectedException \TypeError - */ public function testLegacyDecryptTypeErrorA() { + $this->expectException(\TypeError::class); Crypto::legacyDecrypt(3, "key"); } - /** - * @expectedException \TypeError - */ public function testLegacyDecryptTypeErrorB() { + $this->expectException(\TypeError::class); Crypto::legacyDecrypt("ciphertext", 3); } diff --git a/test/unit/CtrModeTest.php b/test/unit/CtrModeTest.php index f4ab297..72e3abd 100644 --- a/test/unit/CtrModeTest.php +++ b/test/unit/CtrModeTest.php @@ -1,10 +1,11 @@ expectException(\Defuse\Crypto\Exception\EnvironmentIsBrokenException::class); \Defuse\Crypto\Core::incrementCounter( str_repeat("\x00", 16), -1 ); } - /** - * @expectedException \Defuse\Crypto\Exception\EnvironmentIsBrokenException - */ public function testIncrementByZero() { + $this->expectException(\Defuse\Crypto\Exception\EnvironmentIsBrokenException::class); \Defuse\Crypto\Core::incrementCounter( str_repeat("\x00", 16), 0 ); } - public function allNonZeroByteValuesProvider() + public static function allNonZeroByteValuesProvider() { $all_bytes = []; for ($i = 1; $i <= 0xff; $i++) { @@ -159,43 +156,37 @@ public function allNonZeroByteValuesProvider() /** * @dataProvider allNonZeroByteValuesProvider - * @expectedException \Defuse\Crypto\Exception\EnvironmentIsBrokenException */ public function testIncrementCausingOverflowInFirstByte($lsb) { /* Smallest value that will overflow. */ $increment = (PHP_INT_MAX - $lsb) + 1; $start = str_repeat("\x00", 15) . chr($lsb); + $this->expectException(\Defuse\Crypto\Exception\EnvironmentIsBrokenException::class); \Defuse\Crypto\Core::incrementCounter($start, $increment); } - /** - * @expectedException \Defuse\Crypto\Exception\EnvironmentIsBrokenException - */ public function testIncrementWithShortIvLength() { + $this->expectException(\Defuse\Crypto\Exception\EnvironmentIsBrokenException::class); \Defuse\Crypto\Core::incrementCounter( str_repeat("\x00", 15), 1 ); } - /** - * @expectedException \Defuse\Crypto\Exception\EnvironmentIsBrokenException - */ public function testIncrementWithLongIvLength() { + $this->expectException(\Defuse\Crypto\Exception\EnvironmentIsBrokenException::class); \Defuse\Crypto\Core::incrementCounter( str_repeat("\x00", 17), 1 ); } - /** - * @expectedException \Defuse\Crypto\Exception\EnvironmentIsBrokenException - */ public function testIncrementByNonInteger() { + $this->expectException(\Defuse\Crypto\Exception\EnvironmentIsBrokenException::class); \Defuse\Crypto\Core::incrementCounter( str_repeat("\x00", 16), 1.0 diff --git a/test/unit/EncodingTest.php b/test/unit/EncodingTest.php index b707b82..4e1e274 100644 --- a/test/unit/EncodingTest.php +++ b/test/unit/EncodingTest.php @@ -2,8 +2,9 @@ use \Defuse\Crypto\Encoding; use \Defuse\Crypto\Core; +use Yoast\PHPUnitPolyfills\TestCases\TestCase; -class EncodingTest extends PHPUnit_Framework_TestCase +class EncodingTest extends TestCase { public function testEncodeDecodeEquivalency() { @@ -46,10 +47,6 @@ public function testEncodeDecodeEquivalencyTwoBytes() } } - /** - * @expectedException \Defuse\Crypto\Exception\BadFormatException - * @expectedExceptionMessage checksum doesn't match - */ public function testIncorrectChecksum() { $header = Core::secureRandom(Core::HEADER_VERSION_SIZE); @@ -65,13 +62,11 @@ public function testIncorrectChecksum() $str[2*Encoding::SERIALIZE_HEADER_BYTES + 6] = 'f'; $str[2*Encoding::SERIALIZE_HEADER_BYTES + 7] = 'f'; $str[2*Encoding::SERIALIZE_HEADER_BYTES + 8] = 'f'; + $this->expectException(\Defuse\Crypto\Exception\BadFormatException::class); + $this->expectExceptionMessage("checksum doesn't match"); Encoding::loadBytesFromChecksummedAsciiSafeString($header, $str); } - /** - * @expectedException \Defuse\Crypto\Exception\BadFormatException - * @expectedExceptionMessage not a hex string - */ public function testBadHexEncoding() { $header = Core::secureRandom(Core::HEADER_VERSION_SIZE); @@ -80,6 +75,8 @@ public function testBadHexEncoding() Core::secureRandom(Core::KEY_BYTE_SIZE) ); $str[0] = 'Z'; + $this->expectException(\Defuse\Crypto\Exception\BadFormatException::class); + $this->expectExceptionMessage('not a hex string'); Encoding::loadBytesFromChecksummedAsciiSafeString($header, $str); } diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php index 93f423f..98fb17b 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -1,14 +1,15 @@ key = Key::createNewRandomKey(); } - public function tearDown() + public function tear_down() { array_map('unlink', glob(self::$TEMP_DIR . '/*')); rmdir(self::$TEMP_DIR); @@ -149,29 +150,27 @@ public function testResourceToResourceWithPassword($srcFile) 'Original file mismatches the result of encrypt and decrypt'); } - /** - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException - * @expectedExceptionMessage Input file is too small to have been created by this library. - */ public function testDecryptBadMagicNumber() { $junk = self::$TEMP_DIR . '/junk'; file_put_contents($junk, 'This file does not have the right magic number.'); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); + $this->expectExceptionMessage('Input file is too small to have been created by this library.'); File::decryptFile($junk, self::$TEMP_DIR . '/unjunked', $this->key); } /** * @dataProvider garbageCiphertextProvider - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException */ public function testDecryptGarbage($ciphertext) { $junk = self::$TEMP_DIR . '/junk'; file_put_contents($junk, $ciphertext); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); File::decryptFile($junk, self::$TEMP_DIR . '/unjunked', $this->key); } - public function garbageCiphertextProvider() + public static function garbageCiphertextProvider() { $ciphertexts = [ [str_repeat('this is not anything that can be decrypted.', 100)], @@ -182,19 +181,14 @@ public function garbageCiphertextProvider() return $ciphertexts; } - /** - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException - */ public function testDecryptEmptyFile() { $junk = self::$TEMP_DIR . '/junk'; file_put_contents($junk, ''); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); File::decryptFile($junk, self::$TEMP_DIR . '/unjunked', $this->key); } - /** - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException - */ public function testDecryptTruncatedCiphertext() { // This tests for issue #115 on GitHub. @@ -209,6 +203,7 @@ public function testDecryptTruncatedCiphertext() $truncated = substr($ciphertext, 0, 64); file_put_contents($truncated_path, $truncated); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); File::decryptFile($truncated_path, $plaintext_path, $this->key); } @@ -243,10 +238,6 @@ public function testEncryptWithFileDecryptWithCrypto() $this->assertSame($plaintext, $plaintext_decrypted); } - /** - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException - * @excpectedExceptionMessage Message Authentication failure; tampering detected. - */ public function testExtraData() { $src = self::$FILE_DIR . '/wat-gigantic-duck.jpg'; @@ -256,6 +247,8 @@ public function testExtraData() file_put_contents($dest, str_repeat('A', 2048), FILE_APPEND); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); + $this->expectExceptionMessage('Integrity check failed.'); File::decryptFile($dest, $dest . '.jpg', $this->key); } @@ -265,167 +258,102 @@ public function testFileCreateRandomKey() $this->assertInstanceOf('\Defuse\Crypto\Key', $result); } - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage No such file or directory - */ public function testBadSourcePathEncrypt() { + $this->expectException(\Defuse\Crypto\Exception\IOException::class); + $this->expectExceptionMessage('No such file or directory'); File::encryptFile('./i-do-not-exist', 'output-file', $this->key); } - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage No such file or directory - */ public function testBadSourcePathDecrypt() { + $this->expectException(\Defuse\Crypto\Exception\IOException::class); + $this->expectExceptionMessage('No such file or directory'); File::decryptFile('./i-do-not-exist', 'output-file', $this->key); } - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage No such file or directory - */ public function testBadSourcePathEncryptWithPassword() { + $this->expectException(\Defuse\Crypto\Exception\IOException::class); + $this->expectExceptionMessage('No such file or directory'); File::encryptFileWithPassword('./i-do-not-exist', 'output-file', 'password'); } - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage No such file or directory - */ public function testBadSourcePathDecryptWithPassword() { + $this->expectException(\Defuse\Crypto\Exception\IOException::class); + $this->expectExceptionMessage('No such file or directory'); File::decryptFileWithPassword('./i-do-not-exist', 'output-file', 'password'); } - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage Is a directory - */ public function testBadDestinationPathEncrypt() { $src = self::$FILE_DIR . '/wat-gigantic-duck.jpg'; + $this->expectException(\Defuse\Crypto\Exception\IOException::class); + $this->expectExceptionMessage('Is a directory'); File::encryptFile($src, './', $this->key); } - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage Is a directory - */ public function testBadDestinationPathDecrypt() { $src = self::$FILE_DIR . '/wat-gigantic-duck.jpg'; + $this->expectException(\Defuse\Crypto\Exception\IOException::class); + $this->expectExceptionMessage('Is a directory'); File::decryptFile($src, './', $this->key); } - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage Is a directory - */ public function testBadDestinationPathEncryptWithPassword() { $src = self::$FILE_DIR . '/wat-gigantic-duck.jpg'; + $this->expectException(\Defuse\Crypto\Exception\IOException::class); + $this->expectExceptionMessage('Is a directory'); File::encryptFileWithPassword($src, './', 'password'); } - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage Is a directory - */ public function testBadDestinationPathDecryptWithPassword() { $src = self::$FILE_DIR . '/wat-gigantic-duck.jpg'; + $this->expectException(\Defuse\Crypto\Exception\IOException::class); + $this->expectExceptionMessage('Is a directory'); File::decryptFileWithPassword($src, './', 'password'); } - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage array given - */ - public function testNonStringSourcePathEncrypt() - { - File::encryptFile([], 'output-file', $this->key); - } - - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage array given - */ - public function testNonStringDestinationPathEncrypt() - { - $src = self::$FILE_DIR . '/wat-gigantic-duck.jpg'; - File::encryptFile($src, [], $this->key); - } - - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage array given - */ - public function testNonStringSourcePathDecrypt() - { - File::decryptFile([], 'output-file', $this->key); - } - - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage array given - */ - public function testNonStringDestinationPathDecrypt() - { - $src = self::$FILE_DIR . '/wat-gigantic-duck.jpg'; - File::decryptFile($src, [], $this->key); - } - - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage must be a resource - */ public function testNonResourceInputEncrypt() { $resource = fopen('php://memory', 'wb'); + $this->expectException(\Defuse\Crypto\Exception\IOException::class); + $this->expectExceptionMessage('must be a resource'); File::encryptResource('not a resource', $resource, $this->key); fclose($resource); } - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage must be a resource - */ public function testNonResourceOutputEncrypt() { $resource = fopen('php://memory', 'wb'); + $this->expectException(\Defuse\Crypto\Exception\IOException::class); + $this->expectExceptionMessage('must be a resource'); File::encryptResource($resource, 'not a resource', $this->key); fclose($resource); } - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage must be a resource - */ public function testNonResourceInputDecrypt() { $resource = fopen('php://memory', 'wb'); + $this->expectException(\Defuse\Crypto\Exception\IOException::class); + $this->expectExceptionMessage('must be a resource'); File::decryptResource('not a resource', $resource, $this->key); fclose($resource); } - /** - * @expectedException \Defuse\Crypto\Exception\IOException - * @expectedExceptionMessage must be a resource - */ public function testNonResourceOutputDecrypt() { $resource = fopen('php://memory', 'wb'); + $this->expectException(\Defuse\Crypto\Exception\IOException::class); + $this->expectExceptionMessage('must be a resource'); File::decryptResource($resource, 'not a resource', $this->key); fclose($resource); } - /** - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException - */ public function testNonFileResourceDecrypt() { /* This should behave equivalently to an empty file. Calling fstat() on @@ -434,14 +362,15 @@ public function testNonFileResourceDecrypt() $output = fopen('php://memory', 'wb'); try { File::decryptResource($stdin, $output, $this->key); - } catch (Exception $ex) { + } catch (\Exception $ex) { fclose($output); fclose($stdin); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); throw $ex; } } - public function fileToFileProvider() + public static function fileToFileProvider() { $data = []; diff --git a/test/unit/KeyTest.php b/test/unit/KeyTest.php index 03c9d84..d64df35 100644 --- a/test/unit/KeyTest.php +++ b/test/unit/KeyTest.php @@ -2,8 +2,9 @@ use \Defuse\Crypto\Core; use \Defuse\Crypto\Key; +use Yoast\PHPUnitPolyfills\TestCases\TestCase; -class KeyTest extends PHPUnit_Framework_TestCase +class KeyTest extends TestCase { public function testCreateNewRandomKey() { @@ -19,15 +20,13 @@ public function testSaveAndLoadKey() $this->assertSame($key1->getRawBytes(), $key2->getRawBytes()); } - /** - * @expectedException \Defuse\Crypto\Exception\BadFormatException - * @excpectedExceptionMessage key version header - */ public function testIncorrectHeader() { $key = Key::createNewRandomKey(); $str = $key->saveToAsciiSafeString(); $str[0] = 'f'; + $this->expectException(\Defuse\Crypto\Exception\BadFormatException::class); + $this->expectExceptionMessage('Invalid header.'); Key::loadFromAsciiSafeString($str); } } diff --git a/test/unit/LegacyDecryptTest.php b/test/unit/LegacyDecryptTest.php index 59832b8..e379b34 100644 --- a/test/unit/LegacyDecryptTest.php +++ b/test/unit/LegacyDecryptTest.php @@ -3,8 +3,9 @@ use \Defuse\Crypto\Core; use \Defuse\Crypto\Crypto; use \Defuse\Crypto\Encoding; +use Yoast\PHPUnitPolyfills\TestCases\TestCase; -class LegacyDecryptTest extends PHPUnit_Framework_TestCase +class LegacyDecryptTest extends TestCase { public function testDecryptLegacyCiphertext() { @@ -24,9 +25,6 @@ public function testDecryptLegacyCiphertext() $this->assertSame($plain, 'This is a test message'); } - /** - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException - */ public function testDecryptLegacyCiphertextWrongKey() { $cipher = Encoding::hexToBin( @@ -37,6 +35,7 @@ public function testDecryptLegacyCiphertextWrongKey() '024b5e2009106870f1db25d8b85fd01f' ); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); $plain = Crypto::legacyDecrypt( $cipher, "\x01\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" @@ -44,13 +43,11 @@ public function testDecryptLegacyCiphertextWrongKey() ); } - /** - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException - * @expectedExceptionMessage short - */ public function testLegacyDecryptTooShort() { $too_short = str_repeat("a", Core::LEGACY_MAC_BYTE_SIZE); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); + $this->expectExceptionMessage('short'); Crypto::legacyDecrypt($too_short, "0123456789ABCDEF"); } diff --git a/test/unit/PasswordTest.php b/test/unit/PasswordTest.php index a1c2370..38bffae 100644 --- a/test/unit/PasswordTest.php +++ b/test/unit/PasswordTest.php @@ -1,8 +1,9 @@ assertSame($key1->getRawBytes(), $key2->getRawBytes()); } - /** - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException - */ public function testKeyProtectedByPasswordWrong() { $pkey = KeyProtectedByPassword::createRandomPasswordProtectedKey('rightpassword'); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); $key1 = $pkey->unlockKey('wrongpassword'); } @@ -47,19 +46,16 @@ public function testChangePassword() /** * Check that changing the password actually changes the password. - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException */ function testPasswordActuallyChanges() { $pkey1 = KeyProtectedByPassword::createRandomPasswordProtectedKey('password'); $pkey1->changePassword('password', 'new password'); + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); $pkey1->unlockKey('password'); } - /** - * @expectedException \Defuse\Crypto\Exception\BadFormatException - */ function testMalformedLoad() { $pkey1 = KeyProtectedByPassword::createRandomPasswordProtectedKey('password'); @@ -67,6 +63,7 @@ function testMalformedLoad() $pkey1_enc_ascii[0] = "\xFF"; + $this->expectException(\Defuse\Crypto\Exception\BadFormatException::class); KeyProtectedByPassword::loadFromAsciiSafeString($pkey1_enc_ascii); } } diff --git a/test/unit/RuntimeTestTest.php b/test/unit/RuntimeTestTest.php index 7790323..78325e6 100644 --- a/test/unit/RuntimeTestTest.php +++ b/test/unit/RuntimeTestTest.php @@ -1,8 +1,9 @@