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 28e1f86..e8d1dac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,33 +4,78 @@ 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=1 + 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=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: + - cat coverage1.xml + - cat coverage2.xml + - bash <(curl -s https://codecov.io/bash) -Z 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 a8f2925..0e76317 100644 --- a/README.md +++ b/README.md @@ -1,12 +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) +[![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.4 or newer.** The current version is v2.0.0, which is expected to +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, 2019. +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 @@ -19,14 +33,15 @@ ecosystem. Secondly, this library is "difficult to misuse." Like [libsodium](https://github.com/jedisct1/libsodium), its API is designed to be easy to use in a secure way and hard to use in an insecure way. + Dependencies ------------ -This library requres no special dependencies except for PHP 5.4 or newer with -the OpenSSL extensions enabled (this is the default). It uses -[random\_compat](https://github.com/paragonie/random_compat), which is bundled -in with this library so that your users will not need to follow any special -installation steps. +This library requires no special dependencies except for PHP 5.6 or newer with +the OpenSSL extensions (version 1.0.1 or later) enabled (this is the default). +It uses [random\_compat](https://github.com/paragonie/random_compat), which is +bundled in with this library so that your users will not need to follow any +special installation steps. Getting Started ---------------- @@ -53,6 +68,14 @@ documentation. If you're interested in contributing to this library, see the [Internal Developer Documentation](docs/InternalDeveloperDocs.md). +Other Language Support +---------------------- + +This library is intended for server-side PHP software that needs to encrypt data at rest. +If you are building software that needs to encrypt client-side, or building a system that +requires cross-platform encryption/decryption support, we strongly recommend using +[libsodium](https://download.libsodium.org/doc/bindings_for_other_languages) instead. + Examples --------- @@ -76,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 the Taylor Hornby's [contact +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 866b497..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" + "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----- + +mQINBF5V4TABEAC4G2BkHDaqbip3gj1oOqKh3V6LQa9QAd/f/hyhmR5hXpciPxf3 +NNHAxzoGAuB51f1YPJTNO59mGKHDuCFfr0pI94HDGoW4WgxqnUyqHBj2+/JhQPqO +lgDT0QDcfxmUd0wfZl/Ur+8SsaBYvfFWNmPaXHp9m4MMRtw9uZNIW6LlZ24JqmGy +/YUELUSH7P+uJ4HQEdixaqQ0VgIomRDI+5IwdJMtq4TSNazQm3nNmH9Em37cdi6J +NDfFRy2QxJDmuqlg8mkpS5TvrrNy/UJwIeXO9PuGaBODr8GAKWvhkpfGlxN+hWMY +01bOFnuEnOcuXw8BjPAKHqwOuGvinNmQ7lX1Rj3ssd31sTUimop0oNjOTZztpJBR +m6wO2/YGMjt+eL02NgBBDIsV837PeWuJmymTJDGQuBjZ3JWUfyT3AnkA8OU5vKjs +pM8AjIiuU7C8zQhUSHDnukTKWpBmMdOXeWNb5Ye6n60wJWzWFGlm+cYalPs+q3H8 +bxHxHEdFT0rUpxB05bc9zsZ3gGkc2NTNW/00a6gvTyX1UsBAeNgvVSHBHQGfow6o +ZKG+LnVxd+cl97ay6kP29eLypXffbXQ3hMXe9tUNBjAeiok9tssU70Epr9wTh/Fm +/iEbGc8VhS4TSk3c+3eS16rvlQ51FmAlmG6kAnN/ah+BiM4syPrWcJHIDQARAQAB +tG1kZWZ1c2UvcGhwLWVuY3J5cHRpb24gbWFpbnRhaW5lcnMgKGRlZnVzZS9waHAt +ZW5jcnlwdGlvbiByZWxlYXNlIHNpZ25pbmcga2V5KSA8cGhwLWVuY3J5cHRpb25A +ZGVmdXNlLmludmFsaWQ+iQJUBBMBCAA+FiEEbdbmdwKBWEb8hSWj3S5Qf3vbFmkF +Al5V4TACGwMFCQlmAYAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ3S5Qf3vb +FmnQ4Q//bHAwDI7CcTlDDktdRCP0YCRtb5zMa6vSqnZLi5aTqzmL1yQCAp1/JTwf +nlHn5xt60eKwfjIKj7Kj0n8WDFYnlOu30H5fNtFHis0xeS7GkH60tIE/pQUZILlB +Wcnx/ZPnlxccjtfSbnpelSPdvIoHVRNhh1ZYG/49kuuv8TMbMIi2FBAzGEatPoLN +f4wntoOKGvl8R2rPc2geapXTz++X+HJkddHCISR61obDRl9P0t9x+0M7gGSVMGfC +GC4wh3JB6/ha8l+DI+l88/64IKRG+M33bBwLGQ0RIhotHIy442gLJTm6TeoH8iUz +xCBwPYW+Ta1wZi17PIjHdTkNGBeEj/Hr5tTVV3oxrQVgHCymzasnz9IwcCCMwpKK +ZOMFl0+PT3TSBKLnUByvOB64YOjxU7t+sRf53Biz3yKzto5VdHGW64OG2vGFy/Xz +vI5RqU34wjtEHxWfz8y2GBnhD2TzEFCIIWPAX3TDG64NBSEBjhUraOmoVoaYJlP6 +rqxIQo4yhC+f5rnr2ZA48Hnrg0jEdVvN07FegoOnQQPpYBBkOrkTDWChn8oiXMfg +9bjv19zDOBVXl9EU+P8AhwTHz/pBKmhb97N9nYp/pbmejA+I0Kw1vZo7gaMxL938 +oQkdtWT70ZzcpcZfeKVXoZa/ddAmuxzNknZA8ZnjQ9Qhv7aNX2O5Ag0EXlXhMAEQ +AM8od4/85i7ZPmM6C1M4n4XcXeKsuZKHLvLLcRHFGkjVdXRSaxpbk2yDJiLnB9hX +GSJG2gUCT+yrimjQ71bJ4q9K2+mkVHVjdtCrCtoOYEIpMLzsRtqyAWotcVmdv8Zv +4IIjxfdxpTkj9gZmUfDIe6tbN2iBCAo1HArXq1qSdof3ui8SqdWeinkd7lZMesFm +dGQaAcHbmEakO5mRzljme8IBs3UY9j/zxEG1JbsHx9ua7CVwJ7lxi2SgSW6nF9k5 +CX5zbrDqlqSJNtDs+KbjCbI2eK+qe4qZWHPiw4bNBn6EWf97/4Os8w7Vrrpyd2eO +1JENwjlG6WG9mbJdIWWwakZ0CeH5LnJo6dV47KZbbbB6ncavaL+VpfbTCgdOGsCc +GcYUVl90/v5pPm2owx4Dg9hSfcp8fesQuq4b79NAcjF7meu5wgNdvFlfuXony+UC +W2wNi0mi9lzLD0n0j0GDzWyd3r7yXmPTL4LhrQu/pIcWIljKI3GUAQZqIYbGAO3G +7hEFT8rDWg2vKRtMag4iy5FvZFqR+7TwWJAcWnHJBZ95F9NzeYIFhp9a3hxbKXqD +xEnyGgzAszUycq29BApT4/4rDZQuXuOBd4lJp8tSzctLjvo7D3la+MWD6AlDkYT4 +bGKN9NfRCzYr2Zq3jOByAV3d5hGgyzdJlZSqXAGtbHHdABEBAAGJAjwEGAEIACYW +IQRt1uZ3AoFYRvyFJaPdLlB/e9sWaQUCXlXhMAIbDAUJCWYBgAAKCRDdLlB/e9sW +aSGfD/wPeq6lGu8ocHIkO74VPioJRKRXDVLsY02xKP64p0RHUGFTOqqB3A3UV0ue +tkizoUdfF5xkgJ18gbxXo8lotBq+Ita5hoYAfqJnTnucAPGovREJ+X1HfdK4pJqW +KNJElBz+fC4chqksiUAuH7IMImmy0/lA+LqZagzkQJU10MvmiFZ6kn+X5Mb4izRl +vAHo16eI4psApdT8Bs7mwAjgCHxS9Re46uOElB4Bx3iFPd/PEwHWnfr8x9TJZYKW +fsShG31+vfBRCfGtfKGxiAkp3EEM11lzbbfMcC3lai5iJQ/FmHgoIDeIG2Ebuk4w +/PYakSrpvkEYoMP31pVHDhzopVeURS2lpvQJ4CvTP5CVQtKrbuygow6GF8N/drCE +hdEx22pzW02ADS9fgzrlDztIOlOvC9a+epISIaEjfrc9dWhrw6chZEoWIil2MVQR +Sj0jZ8w/H7P88oHTOcFVel73ZEPg9eRUkqMnIn3DWUuqLI2SX/AtVnhdYHWTiOkq +knsGofWxUSu3RZR2ZElK9hjNKdVbGDzHGAYeJihieTKIOXpCf6Ix5B32tmFpfmBV +Q9YP3JLsRTxIMbXsJImand/r6fSjdmTpk2PovYPtE1HTJKaVHeagQdsrWw5LaJv0 +ZWuwJm0y0WJXcAEjwOHhBs0nvq2CXuZi2ZTPtY+DbsSFWhaN7g== +=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/FAQ.md b/docs/FAQ.md index 135e235..9f77392 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -37,3 +37,15 @@ If you're getting this exception, then the string you're giving to `loadFromAsciiSafeString()` is *not* the same as the string you got from `saveToAsciiSafeString()`. Perhaps your database column isn't wide enough and it's truncating the string as you insert it? + +Does encrypting hide the length of the plaintext? +-------------------------------------------------- + +Encryption does not, and is not intended to, hide the length of the data being +encrypted. For example, it is not safe to encrypt a field in which only a small +number of different-length values are possible (e.g. "male" or "female") since +it would be possible to tell what the plaintext is by looking at the length of +the ciphertext. In order to do this safely, it is your responsibility to, before +encrypting, pad the data out to the length of the longest string that will ever +be encrypted. This way, all plaintexts are the same length, and no information +about the plaintext can be gleaned from the length of the ciphertext. diff --git a/docs/InternalDeveloperDocs.md b/docs/InternalDeveloperDocs.md index 4604cff..75d4a2c 100644 --- a/docs/InternalDeveloperDocs.md +++ b/docs/InternalDeveloperDocs.md @@ -111,12 +111,18 @@ 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 ``` +Check that the version number and support lifetime in README.md are correct: + +``` +cat README.md +``` + Run the tests: ``` @@ -135,7 +141,7 @@ Test the `.phar`: ``` cd ../ -./test.sh dist/defuse-crypto.phar +./test.sh dist/phar-testing-autoload.php ``` Sign the `.phar`: @@ -148,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 40285cc..a40cac3 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -46,6 +46,16 @@ What this library provides is symmetric encryption for "data at rest." This means it is not suitable for use in building protocols where "data is in motion" (i.e. moving over a network) except in limited set of cases. +Please note that **encryption does not, and is not intended to, hide the +*length* of the data being encrypted.** For example, it is not safe to encrypt +a field in which only a small number of different-length values are possible +(e.g. "male" or "female") since it would be possible to tell what the plaintext +is by looking at the length of the ciphertext. In order to do this safely, it is +your responsibility to, before encrypting, pad the data out to the length of the +longest string that will ever be encrypted. This way, all plaintexts are the +same length, and no information about the plaintext can be gleaned from the +length of the ciphertext. + Getting the Code ----------------- @@ -211,12 +221,18 @@ function CreateUserAccount($username, $password) } ``` +**WARNING:** Because of the way `KeyProtectedByPassword` is implemented, knowing +`SHA256($password)` is enough to decrypt a `KeyProtectedByPassword`. To be +secure, your application MUST NOT EVER compute `SHA256($password)` and use or +store it for any reason. You must also make sure that other libraries your +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 @@ -239,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 3f5ed52..3fc20ab 100644 --- a/src/Core.php +++ b/src/Core.php @@ -45,32 +45,31 @@ final class Core * @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) { - if (Core::ourStrlen($ctr) !== Core::BLOCK_BYTE_SIZE) { - throw new Ex\EnvironmentIsBrokenException( - 'Trying to increment a nonce of the wrong size.' - ); - } - - if (! \is_int($inc)) { - throw new Ex\EnvironmentIsBrokenException( - 'Trying to increment nonce by a non-integer.' - ); - } - - if ($inc < 0) { - throw new Ex\EnvironmentIsBrokenException( - 'Trying to increment nonce by a negative amount.' - ); - } - - if ($inc > PHP_INT_MAX - 255) { - throw new Ex\EnvironmentIsBrokenException( - 'Integer overflow may occur.' - ); - } + 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) @@ -80,11 +79,7 @@ public static function incrementCounter($ctr, $inc) $sum = \ord($ctr[$i]) + $inc; /* Detect integer overflow and fail. */ - if (! \is_int($sum)) { - throw new Ex\EnvironmentIsBrokenException( - 'Integer overflow in CTR mode nonce increment.' - ); - } + Core::ensureTrue(\is_int($sum), 'Integer overflow in CTR mode nonce increment'); $ctr[$i] = \pack('C', $sum & 0xFF); $inc = $sum >> 8; @@ -103,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.' @@ -135,18 +135,19 @@ public static function HKDF($hash, $ikm, $length, $info = '', $salt = 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. - if (empty($length) || ! \is_int($length) || - $length < 0 || $length > 255 * $digest_length) { - throw new Ex\EnvironmentIsBrokenException( - 'Bad output length requested of HKDF.' - ); - } + 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)) { @@ -161,9 +162,7 @@ public static function HKDF($hash, $ikm, $length, $info = '', $salt = null) // HKDF-Expand: // This check is useless, but it serves as a reminder to the spec. - if (Core::ourStrlen($prk) < $digest_length) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue(Core::ourStrlen($prk) >= $digest_length); // T(0) = '' $t = ''; @@ -183,9 +182,7 @@ public static function HKDF($hash, $ikm, $length, $info = '', $salt = null) // ORM = first L octets of T /** @var string $orm */ $orm = Core::ourSubstr($t, 0, $length); - if (!\is_string($orm)) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue(\is_string($orm)); return $orm; } @@ -219,9 +216,7 @@ public static function hashEquals($expected, $given) // We're not attempting to make variable-length string comparison // secure, as that's very difficult. Make sure the strings are the same // length. - if (Core::ourStrlen($expected) !== Core::ourStrlen($given)) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue(Core::ourStrlen($expected) === Core::ourStrlen($given)); $blind = Core::secureRandom(32); $message_compare = \hash_hmac(Core::HASH_FUNCTION_NAME, $given, $blind); @@ -238,9 +233,10 @@ public static function hashEquals($expected, $given) */ public static function ensureConstantExists($name) { - if (! \defined($name)) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue( + \defined($name), + 'Constant '.$name.' does not exists' + ); } /** @@ -253,8 +249,25 @@ public static function ensureConstantExists($name) */ public static function ensureFunctionExists($name) { - if (! \function_exists($name)) { - throw new Ex\EnvironmentIsBrokenException(); + 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); } } @@ -277,13 +290,11 @@ public static function ourStrlen($str) { static $exists = null; if ($exists === null) { - $exists = \function_exists('mb_strlen'); + $exists = \extension_loaded('mbstring') && \function_exists('mb_strlen'); } if ($exists) { $length = \mb_strlen($str, '8bit'); - if ($length === false) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue($length !== false); return $length; } else { return \strlen($str); @@ -305,33 +316,46 @@ public static function ourSubstr($str, $start, $length = null) { static $exists = null; if ($exists === null) { - $exists = \function_exists('mb_substr'); + $exists = \extension_loaded('mbstring') && \function_exists('mb_substr'); } - if ($exists) { - // mb_substr($str, 0, NULL, '8bit') returns an empty string on PHP - // 5.3, so we have to find the length ourselves. - if (! isset($length)) { - if ($start >= 0) { - $length = Core::ourStrlen($str) - $start; - } else { - $length = -$start; - } - } + // 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 ''; + } - // 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.) - if ($start === Core::ourStrlen($str) && $length === 0) { - return ''; - } + if ($start > $input_len) { + return false; + } - if ($start > Core::ourStrlen($str)) { - 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'); - if (Core::ourStrlen($substr) !== $length) { + // 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: @@ -341,12 +365,7 @@ public static function ourSubstr($str, $start, $length = null) return $substr; } - // Unlike mb_substr(), substr() doesn't accept NULL for length - if (isset($length)) { - return \substr($str, $start, $length); - } else { - return \substr($str, $start); - } + return \substr($str, $start, $length); } /** @@ -367,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)) { @@ -390,28 +417,22 @@ public static function pbkdf2($algorithm, $password, $salt, $count, $key_length, $key_length += 0; $algorithm = \strtolower($algorithm); - if (! \in_array($algorithm, \hash_algos(), true)) { - throw new Ex\EnvironmentIsBrokenException( - 'Invalid or unsupported hash 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', ]; - if (! \in_array($algorithm, $ok_algorithms, true)) { - throw new Ex\EnvironmentIsBrokenException( - 'Algorithm is not a secure cryptographic hash function.' - ); - } + Core::ensureTrue( + \in_array($algorithm, $ok_algorithms, true), + 'Algorithm is not a secure cryptographic hash function.' + ); - if ($count <= 0 || $key_length <= 0) { - throw new Ex\EnvironmentIsBrokenException( - 'Invalid PBKDF2 parameters.' - ); - } + 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! @@ -432,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 fdb411c..f148db4 100644 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -14,11 +14,27 @@ class Crypto * @param bool $raw_binary * * @throws Ex\EnvironmentIsBrokenException + * @throws \TypeError * * @return string */ - public static function encrypt($plaintext, Key $key, $raw_binary = false) + 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), @@ -35,11 +51,32 @@ public static function encrypt($plaintext, Key $key, $raw_binary = false) * @param bool $raw_binary * * @throws Ex\EnvironmentIsBrokenException + * @throws \TypeError * * @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( + '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), @@ -54,13 +91,29 @@ public static function encryptWithPassword($plaintext, $password, $raw_binary = * @param Key $key * @param bool $raw_binary * + * @throws \TypeError * @throws Ex\EnvironmentIsBrokenException * @throws Ex\WrongKeyOrModifiedCiphertextException * * @return string */ - public static function decrypt($ciphertext, Key $key, $raw_binary = false) + 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), @@ -78,11 +131,32 @@ public static function decrypt($ciphertext, Key $key, $raw_binary = false) * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\WrongKeyOrModifiedCiphertextException + * @throws \TypeError * * @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( + '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), @@ -98,11 +172,27 @@ public static function decryptWithPassword($ciphertext, $password, $raw_binary = * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\WrongKeyOrModifiedCiphertextException + * @throws \TypeError * * @return string */ - public static function legacyDecrypt($ciphertext, $key) + public static function legacyDecrypt( + $ciphertext, + #[\SensitiveParameter] + $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. @@ -115,16 +205,12 @@ public static function legacyDecrypt($ciphertext, $key) * @var string */ $hmac = Core::ourSubstr($ciphertext, 0, Core::LEGACY_MAC_BYTE_SIZE); - if (!\is_string($hmac)) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue(\is_string($hmac)); /** * @var string */ - $ciphertext = Core::ourSubstr($ciphertext, Core::LEGACY_MAC_BYTE_SIZE); - if (!\is_string($ciphertext)) { - throw new Ex\EnvironmentIsBrokenException(); - } + $messageCiphertext = Core::ourSubstr($ciphertext, Core::LEGACY_MAC_BYTE_SIZE); + Core::ensureTrue(\is_string($messageCiphertext)); // Regenerate the same authentication sub-key. $akey = Core::HKDF( @@ -135,7 +221,7 @@ public static function legacyDecrypt($ciphertext, $key) null ); - if (self::verifyHMAC($hmac, $ciphertext, $akey)) { + if (self::verifyHMAC($hmac, $messageCiphertext, $akey)) { // Regenerate the same encryption sub-key. $ekey = Core::HKDF( Core::LEGACY_HASH_FUNCTION_NAME, @@ -146,7 +232,7 @@ public static function legacyDecrypt($ciphertext, $key) ); // Extract the IV from the ciphertext. - if (Core::ourStrlen($ciphertext) <= Core::LEGACY_BLOCK_BYTE_SIZE) { + if (Core::ourStrlen($messageCiphertext) <= Core::LEGACY_BLOCK_BYTE_SIZE) { throw new Ex\WrongKeyOrModifiedCiphertextException( 'Ciphertext is too short.' ); @@ -154,18 +240,14 @@ public static function legacyDecrypt($ciphertext, $key) /** * @var string */ - $iv = Core::ourSubstr($ciphertext, 0, Core::LEGACY_BLOCK_BYTE_SIZE); - if (!\is_string($iv)) { - throw new Ex\EnvironmentIsBrokenException(); - } + $iv = Core::ourSubstr($messageCiphertext, 0, Core::LEGACY_BLOCK_BYTE_SIZE); + Core::ensureTrue(\is_string($iv)); /** * @var string */ - $actualCiphertext = Core::ourSubstr($ciphertext, Core::LEGACY_BLOCK_BYTE_SIZE); - if (!\is_string($actualCiphertext)) { - throw new Ex\EnvironmentIsBrokenException(); - } + $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); @@ -254,9 +336,7 @@ private static function decryptInternal($ciphertext, KeyOrPassword $secret, $raw Core::HEADER_VERSION_SIZE, Core::SALT_BYTE_SIZE ); - if (!\is_string($salt)) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue(\is_string($salt)); // Get the IV. /** @var string $iv */ @@ -265,9 +345,7 @@ private static function decryptInternal($ciphertext, KeyOrPassword $secret, $raw Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE, Core::BLOCK_BYTE_SIZE ); - if (!\is_string($iv)) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue(\is_string($iv)); // Get the HMAC. /** @var string $hmac */ @@ -276,9 +354,7 @@ private static function decryptInternal($ciphertext, KeyOrPassword $secret, $raw Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE, Core::MAC_BYTE_SIZE ); - if (!\is_string($hmac)) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue(\is_string($hmac)); // Get the actual encrypted ciphertext. /** @var string $encrypted */ @@ -289,9 +365,7 @@ private static function decryptInternal($ciphertext, KeyOrPassword $secret, $raw Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE - Core::SALT_BYTE_SIZE - Core::BLOCK_BYTE_SIZE - Core::HEADER_VERSION_SIZE ); - if (!\is_string($encrypted)) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue(\is_string($encrypted)); // Derive the separate encryption and authentication keys from the key // or password, whichever it is. @@ -318,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'); @@ -331,11 +411,7 @@ protected static function plainEncrypt($plaintext, $key, $iv) $iv ); - if (!\is_string($ciphertext)) { - throw new Ex\EnvironmentIsBrokenException( - 'openssl_encrypt() failed.' - ); - } + Core::ensureTrue(\is_string($ciphertext), 'openssl_encrypt() failed'); return $ciphertext; } @@ -352,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'); @@ -365,11 +448,7 @@ protected static function plainDecrypt($ciphertext, $key, $iv, $cipherMethod) OPENSSL_RAW_DATA, $iv ); - if (!\is_string($plaintext)) { - throw new Ex\EnvironmentIsBrokenException( - 'openssl_decrypt() failed.' - ); - } + Core::ensureTrue(\is_string($plaintext), 'openssl_decrypt() failed.'); return $plaintext; } @@ -385,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 001fb6e..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,15 +175,18 @@ 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. - if (Core::ourStrlen($header) !== self::SERIALIZE_HEADER_BYTES) { - throw new Ex\EnvironmentIsBrokenException( - 'Header must be ' . self::SERIALIZE_HEADER_BYTES . ' bytes.' - ); - } + Core::ensureTrue( + Core::ourStrlen($header) === self::SERIALIZE_HEADER_BYTES, + 'Header must be ' . self::SERIALIZE_HEADER_BYTES . ' bytes.' + ); return Encoding::binToHex( $header . @@ -207,15 +211,18 @@ 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. - if (Core::ourStrlen($expected_header) !== self::SERIALIZE_HEADER_BYTES) { - throw new Ex\EnvironmentIsBrokenException( - 'Header must be 4 bytes.' - ); - } + 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. */ diff --git a/src/File.php b/src/File.php index 78e39d6..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,13 +385,12 @@ 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); - if (!\is_resource($hmac)) { - throw new Ex\EnvironmentIsBrokenException( - 'Cannot initialize a hash context' - ); - } + Core::ensureTrue( + \is_resource($hmac) || \is_object($hmac), + 'Cannot initialize a hash context' + ); /* Write the header, salt, and IV. */ self::writeBytes( @@ -370,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. */ @@ -407,11 +448,7 @@ private static function encryptResourceInternal($inputHandle, $outputHandle, Key $thisIv ); - if (!\is_string($encrypted)) { - throw new Ex\EnvironmentIsBrokenException( - 'OpenSSL encryption error' - ); - } + Core::ensureTrue(\is_string($encrypted), 'OpenSSL encryption error'); /* Write this buffer's ciphertext. */ self::writeBytes($outputHandle, $encrypted, Core::ourStrlen($encrypted)); @@ -441,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) { @@ -491,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' ); @@ -516,23 +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); - if (!\is_resource($hmac)) { - throw new Ex\EnvironmentIsBrokenException( - 'Cannot initialize a hash context' - ); - } + 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' ); @@ -543,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; @@ -574,13 +613,9 @@ 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); - if (!\is_resource($chunk_mac)) { - throw new Ex\EnvironmentIsBrokenException( - 'Cannot duplicate a hash context' - ); - } + Core::ensureTrue(\is_resource($chunk_mac) || \is_object($chunk_mac), 'Cannot duplicate a hash context'); $macs []= \hash_final($chunk_mac); } @@ -598,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' ); @@ -632,13 +667,9 @@ 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); - if (!\is_resource($calc_mac)) { - throw new Ex\EnvironmentIsBrokenException( - 'Cannot duplicate a hash context' - ); - } + Core::ensureTrue(\is_resource($calc_mac) || \is_object($calc_mac), 'Cannot duplicate a hash context'); $calc = \hash_final($calc_mac); if (empty($macs)) { @@ -660,11 +691,7 @@ public static function decryptResourceInternal($inputHandle, $outputHandle, KeyO OPENSSL_RAW_DATA, $thisIv ); - if (!\is_string($decrypted)) { - throw new Ex\EnvironmentIsBrokenException( - 'OpenSSL decryption error' - ); - } + Core::ensureTrue(\is_string($decrypted), 'OpenSSL decryption error'); /* Write the plaintext to the output file. */ self::writeBytes( @@ -691,18 +718,15 @@ public static function decryptResourceInternal($inputHandle, $outputHandle, KeyO * * @throws Ex\IOException * @throws Ex\EnvironmentIsBrokenException - * - * @return string */ public static function readBytes($stream, $num_bytes) { - if ($num_bytes < 0) { - throw new Ex\EnvironmentIsBrokenException( - 'Tried to read less than 0 bytes' - ); - } elseif ($num_bytes === 0) { + 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)) { @@ -733,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) { @@ -776,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 fe4bf7d..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,13 +86,15 @@ public function getRawBytes() * * @throws Ex\EnvironmentIsBrokenException */ - private function __construct($bytes) + private function __construct( + #[\SensitiveParameter] + $bytes + ) { - if (Core::ourStrlen($bytes) !== self::KEY_BYTE_SIZE) { - throw new Ex\EnvironmentIsBrokenException( - 'Bad key length.' - ); - } + Core::ensureTrue( + Core::ourStrlen($bytes) === self::KEY_BYTE_SIZE, + 'Bad key length.' + ); $this->key_bytes = $bytes; } diff --git a/src/KeyOrPassword.php b/src/KeyOrPassword.php index 4a810d3..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); } @@ -57,14 +60,16 @@ public static function createFromPassword($password) */ public function deriveKeys($salt) { - if (Core::ourStrlen($salt) !== Core::SALT_BYTE_SIZE) { - throw new Ex\EnvironmentIsBrokenException('Bad salt.'); - } + Core::ensureTrue( + Core::ourStrlen($salt) === Core::SALT_BYTE_SIZE, + 'Bad salt.' + ); if ($this->secret_type === self::SECRET_TYPE_KEY) { - if (!($this->secret instanceof Key)) { - throw new Ex\CryptoException('Expected a Key object'); - } + Core::ensureTrue($this->secret instanceof Key); + /** + * @psalm-suppress PossiblyInvalidMethodCall + */ $akey = Core::HKDF( Core::HASH_FUNCTION_NAME, $this->secret->getRawBytes(), @@ -72,6 +77,9 @@ public function deriveKeys($salt) Core::AUTHENTICATION_INFO_STRING, $salt ); + /** + * @psalm-suppress PossiblyInvalidMethodCall + */ $ekey = Core::HKDF( Core::HASH_FUNCTION_NAME, $this->secret->getRawBytes(), @@ -81,15 +89,18 @@ public function deriveKeys($salt) ); return new DerivedKeys($akey, $ekey); } elseif ($this->secret_type === self::SECRET_TYPE_PASSWORD) { - if (!\is_string($this->secret)) { - throw new Ex\CryptoException('Expected a string'); - } + 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, @@ -125,8 +136,20 @@ 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) { + Core::ensureTrue($secret instanceof Key); + } elseif ($secret_type === self::SECRET_TYPE_PASSWORD) { + Core::ensureTrue(\is_string($secret)); + } else { + throw new Ex\EnvironmentIsBrokenException('Bad secret type.'); + } $this->secret_type = $secret_type; $this->secret = $secret; } diff --git a/src/KeyProtectedByPassword.php b/src/KeyProtectedByPassword.php index 9d32e76..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, @@ -79,9 +85,13 @@ public function saveToAsciiSafeString() * @throws Ex\EnvironmentIsBrokenException * @throws Ex\WrongKeyOrModifiedCiphertextException * + * @param string $password * @return Key */ - public function unlockKey($password) + public function unlockKey( + #[\SensitiveParameter] + $password + ) { try { $inner_key_encoded = Crypto::decryptWithPassword( @@ -103,6 +113,40 @@ public function unlockKey($password) } } + /** + * Changes the password. + * + * @param string $current_password + * @param string $new_password + * + * @throws Ex\EnvironmentIsBrokenException + * @throws Ex\WrongKeyOrModifiedCiphertextException + * + * @return KeyProtectedByPassword + */ + 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 + * 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. * diff --git a/src/RuntimeTests.php b/src/RuntimeTests.php index 9f00a97..65ce55d 100644 --- a/src/RuntimeTests.php +++ b/src/RuntimeTests.php @@ -59,13 +59,9 @@ public static function runtimeTest() RuntimeTests::HKDFTestVector(); RuntimeTests::testEncryptDecrypt(); - if (Core::ourStrlen(Key::createNewRandomKey()->getRawBytes()) != Core::KEY_BYTE_SIZE) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue(Core::ourStrlen(Key::createNewRandomKey()->getRawBytes()) === Core::KEY_BYTE_SIZE); - if (Core::ENCRYPTION_INFO_STRING == Core::AUTHENTICATION_INFO_STRING) { - throw new Ex\EnvironmentIsBrokenException(); - } + 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; @@ -97,9 +93,7 @@ private static function testEncryptDecrypt() // the user into thinking it's just an invalid ciphertext! throw new Ex\EnvironmentIsBrokenException(); } - if ($decrypted !== $data) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue($decrypted === $data); // Modifying the ciphertext: Appending a string. try { @@ -167,9 +161,7 @@ private static function HKDFTestVector() '34007208d5b887185865' ); $computed_okm = Core::HKDF('sha256', $ikm, $length, $info, $salt); - if ($computed_okm !== $okm) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue($computed_okm === $okm); // Test Case 7 $ikm = \str_repeat("\x0c", 22); @@ -180,9 +172,7 @@ private static function HKDFTestVector() '673a081d70cce7acfc48' ); $computed_okm = Core::HKDF('sha1', $ikm, $length, '', null); - if ($computed_okm !== $okm) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue($computed_okm === $okm); } /** @@ -197,9 +187,9 @@ private static function HMACTestVector() $key = \str_repeat("\x0b", 20); $data = 'Hi There'; $correct = 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'; - if (\hash_hmac(Core::HASH_FUNCTION_NAME, $data, $key) !== $correct) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue( + \hash_hmac(Core::HASH_FUNCTION_NAME, $data, $key) === $correct + ); } /** @@ -230,18 +220,9 @@ private static function AESTestVector() ); $computed_ciphertext = Crypto::plainEncrypt($plaintext, $key, $iv); - if ($computed_ciphertext !== $ciphertext) { - echo \str_repeat("\n", 30); - echo \bin2hex($computed_ciphertext); - echo "\n---\n"; - echo \bin2hex($ciphertext); - echo \str_repeat("\n", 30); - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue($computed_ciphertext === $ciphertext); $computed_plaintext = Crypto::plainDecrypt($ciphertext, $key, $iv, Core::CIPHER_METHOD); - if ($computed_plaintext !== $plaintext) { - throw new Ex\EnvironmentIsBrokenException(); - } + Core::ensureTrue($computed_plaintext === $plaintext); } } diff --git a/test.sh b/test.sh index 47ba370..dfdc496 100755 --- a/test.sh +++ b/test.sh @@ -9,14 +9,16 @@ fi if [ -n "$1" ]; then BOOTSTRAP="$1" + MEASURECOVERAGE="0" else # You need to run `composer install` to generate this file. BOOTSTRAP="vendor/autoload.php" + MEASURECOVERAGE="1" fi # loading bootstrap should output nothing load=$(php -r "require '$BOOTSTRAP';") test -z "$load" -./test/phpunit.sh "$BOOTSTRAP" +./test/phpunit.sh "$BOOTSTRAP" "$MEASURECOVERAGE" echo "" 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 403e46c..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? @@ -55,11 +48,21 @@ fi gpg --verify phpunit.phar.asc phpunit.phar if [ $? -eq 0 ]; then echo + if [ "$2" -eq "1" ]; then + 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 --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 --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/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 41d62c0..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')) { @@ -106,4 +111,40 @@ public function testOurSubstrOutOfBorders() Core::ourSubstr('abc', 5, 2) ); } + + public function testOurSubstrNegativeLength() + { + $this->expectException(\InvalidArgumentException::class); + Core::ourSubstr('abc', 0, -1); + } + + public function testOurSubstrNegativeStart() + { + $this->assertSame('c', Core::ourSubstr('abc', -1, 1)); + } + + 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 bffff3f..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() @@ -75,9 +76,17 @@ public function testEncryptDecryptWithPassword() } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ } - // Ciphertext too small. + // TypeError; password needs to be a string, not an object $password = Key::createNewRandomKey(); - $ciphertext = \str_repeat('A', Core::MINIMUM_CIPHERTEXT_SIZE - 1); + try { + $ciphertext = Crypto::encryptWithPassword($data, $password, true); + $this->fail('Crypto::encryptWithPassword() should not accept key objects'); + } catch (\TypeError $e) { /* expected */ + } + + // Ciphertext too small. + $password = \random_bytes(32); + $ciphertext = \str_repeat('A', Core::MINIMUM_CIPHERTEXT_SIZE - 1); try { Crypto::decryptWithPassword($ciphertext, $password, true); throw new Ex\EnvironmentIsBrokenException(); @@ -85,21 +94,106 @@ 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); } + + public function testEncryptTypeErrorA() + { + $key = Key::createNewRandomKey(); + $this->expectException(\TypeError::class); + Crypto::encrypt(3, $key, false); + } + + public function testEncryptTypeErrorB() + { + $this->expectException(\TypeError::class); + Crypto::encrypt("plaintext", 3, false); + } + + public function testEncryptTypeErrorC() + { + $key = Key::createNewRandomKey(); + $this->expectException(\TypeError::class); + Crypto::encrypt("plaintext", $key, 3); + } + + public function testEncryptWithPasswordTypeErrorA() + { + $this->expectException(\TypeError::class); + Crypto::encryptWithPassword(3, "password", false); + } + + public function testEncryptWithPasswordTypeErrorB() + { + $this->expectException(\TypeError::class); + Crypto::encryptWithPassword("plaintext", 3, false); + } + + public function testEncryptWithPasswordTypeErrorC() + { + $this->expectException(\TypeError::class); + Crypto::encryptWithPassword("plaintext", "password", 3); + } + + public function testDecryptTypeErrorA() + { + $key = Key::createNewRandomKey(); + $this->expectException(\TypeError::class); + Crypto::decrypt(3, $key, false); + } + + public function testDecryptTypeErrorB() + { + $this->expectException(\TypeError::class); + Crypto::decrypt("ciphertext", 3, false); + } + + public function testDecryptTypeErrorC() + { + $key = Key::createNewRandomKey(); + $this->expectException(\TypeError::class); + Crypto::decrypt("ciphertext", $key, 3); + } + + public function testDecryptWithPasswordTypeErrorA() + { + $this->expectException(\TypeError::class); + Crypto::decryptWithPassword(3, "password", false); + } + + public function testDecryptWithPasswordTypeErrorB() + { + $this->expectException(\TypeError::class); + Crypto::decryptWithPassword("ciphertext", 3, false); + } + + public function testDecryptWithPasswordTypeErrorC() + { + $this->expectException(\TypeError::class); + Crypto::decryptWithPassword("ciphertext", "password", 3); + } + + public function testLegacyDecryptTypeErrorA() + { + $this->expectException(\TypeError::class); + Crypto::legacyDecrypt(3, "key"); + } + + public function testLegacyDecryptTypeErrorB() + { + $this->expectException(\TypeError::class); + Crypto::legacyDecrypt("ciphertext", 3); + } + } diff --git a/test/unit/CtrModeTest.php b/test/unit/CtrModeTest.php index cd548c0..72e3abd 100644 --- a/test/unit/CtrModeTest.php +++ b/test/unit/CtrModeTest.php @@ -1,18 +1,13 @@ expectException(\Defuse\Crypto\Exception\EnvironmentIsBrokenException::class); \Defuse\Crypto\Core::incrementCounter( str_repeat("\x00", 16), -1 ); } - public function allNonZeroByteValuesProvider() + public function testIncrementByZero() + { + $this->expectException(\Defuse\Crypto\Exception\EnvironmentIsBrokenException::class); + \Defuse\Crypto\Core::incrementCounter( + str_repeat("\x00", 16), + 0 + ); + } + + public static function allNonZeroByteValuesProvider() { $all_bytes = []; for ($i = 1; $i <= 0xff; $i++) { @@ -154,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 e7f93b7..e379b34 100644 --- a/test/unit/LegacyDecryptTest.php +++ b/test/unit/LegacyDecryptTest.php @@ -1,9 +1,11 @@ assertSame($plain, 'This is a test message'); } - /** - * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException - */ public function testDecryptLegacyCiphertextWrongKey() { $cipher = Encoding::hexToBin( @@ -36,10 +35,20 @@ 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" // ^- I changed that byte ); } + + 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 93805fb..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'); } + + /** + * Check that a new password was set. + */ + public function testChangePassword() + { + $pkey1 = KeyProtectedByPassword::createRandomPasswordProtectedKey('password'); + $pkey1_enc_ascii = $pkey1->saveToAsciiSafeString(); + $key1 = $pkey1->unlockKey('password')->saveToAsciiSafeString(); + + $pkey1->changePassword('password', 'new password'); + + $pkey1_enc_ascii_new = $pkey1->saveToAsciiSafeString(); + $key1_new = $pkey1->unlockKey('new password')->saveToAsciiSafeString(); + + // The encrypted_key should not be the same. + $this->assertNotSame($pkey1_enc_ascii, $pkey1_enc_ascii_new); + + // The actual key should be the same. + $this->assertSame($key1, $key1_new); + } + + /** + * Check that changing the password actually changes the password. + */ + function testPasswordActuallyChanges() + { + $pkey1 = KeyProtectedByPassword::createRandomPasswordProtectedKey('password'); + $pkey1->changePassword('password', 'new password'); + + $this->expectException(\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException::class); + $pkey1->unlockKey('password'); + } + + function testMalformedLoad() + { + $pkey1 = KeyProtectedByPassword::createRandomPasswordProtectedKey('password'); + $pkey1_enc_ascii = $pkey1->saveToAsciiSafeString(); + + $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 @@