diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 3b85ca4f..00000000 --- a/.coveralls.yml +++ /dev/null @@ -1,3 +0,0 @@ -service_name: travis-ci -coverage_clover: build/logs/clover.xml -json_path: build/logs/coveralls-upload.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..4f695283 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto + +/.github export-ignore +/doc export-ignore +/test export-ignore +/.coveralls.yml export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpstan* export-ignore +/phpunit* export-ignore +/psalm* export-ignore diff --git a/.github/output/.gitkeep b/.github/output/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.github/output/README.md b/.github/output/README.md new file mode 100644 index 00000000..d67317d4 --- /dev/null +++ b/.github/output/README.md @@ -0,0 +1,3 @@ +# Coverage Output + +If you are looking at the `image-data` branch, please know that this is just a hack to get the coverage badge working. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..cf09ca5e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + phpunit: + name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} + runs-on: ${{ matrix.operating-system }} + if: "!contains(github.event.head_commit.message, '[ci]')" + strategy: + matrix: + operating-system: ['ubuntu-latest'] + php-versions: ['8.1', '8.2', '8.3', '8.4'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, intl, sodium + ini-values: error_reporting=-1, display_errors=On + coverage: none + + - name: Install Composer dependencies + uses: "ramsey/composer-install@v3" + + - name: PHPUnit tests + run: vendor/bin/phpunit + + - name: PHPStan analysis + run: vendor/bin/phpstan diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..8abe0e64 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,56 @@ +name: Coverage + +permissions: + contents: write + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + coverage: + name: PHP ${{ matrix.php-versions }} on ${{ matrix.operating-system }} + runs-on: ${{ matrix.operating-system }} + if: "!contains(github.event.head_commit.message, '[ci]')" + strategy: + matrix: + operating-system: ['ubuntu-latest'] + php-versions: ['8.4'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, intl, sodium, xdebug + ini-values: error_reporting=-1, display_errors=On + coverage: xdebug + + - name: Install Composer dependencies + uses: "ramsey/composer-install@v3" + + - name: Ensure XML file is being loaded + run: cp phpunit.xml.dist phpunit.xml + + - name: PHPUnit tests with coverage + run: vendor/bin/phpunit --coverage-clover clover.xml --coverage-html build/coverage-report + + - name: phpunit-coverage-badge + uses: timkrase/phpunit-coverage-badge@v1.2.1 + with: + coverage_badge_path: .github/output/coverage.svg + push_badge: false + + - name: Git push to image-data branch + uses: peaceiris/actions-gh-pages@v3 + with: + publish_dir: .github/output + publish_branch: image-data + github_token: ${{ secrets.TOKEN }} + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml new file mode 100644 index 00000000..7935458f --- /dev/null +++ b/.github/workflows/psalm.yml @@ -0,0 +1,32 @@ +name: Psalm + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + psalm: + name: Psalm on PHP ${{ matrix.php-versions }} + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: ['ubuntu-latest'] + php-versions: ['8.1', '8.2', '8.3', '8.4'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + coverage: none + + - name: Install Composer dependencies + uses: "ramsey/composer-install@v3" + + - name: Psalm static analysis + run: vendor/bin/psalm diff --git a/.gitignore b/.gitignore index d4fb8272..321fa666 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ /composer.lock /composer.phar /.idea/ +/.phpunit.result.cache +/phpstan.neon diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a5bb1317..00000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -language: php -sudo: required -dist: trusty -php: - - "7.2" - - "7.3" - - "master" - - "nightly" -matrix: - fast_finish: true - allow_failures: - - php: "master" - - php: "nightly" - -install: - - travis_retry composer install --no-interaction - - wget -c -nc --retry-connrefused --tries=0 https://github.com/php-coveralls/php-coveralls/releases/download/v2.0.0/php-coveralls.phar - - chmod +x php-coveralls.phar - - php php-coveralls.phar --version -script: - - ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml - - ./vendor/bin/psalm -after_success: - - travis_retry php php-coveralls.phar -v -before_script: - - mkdir -p build/logs - - ls -al -cache: - directories: - - vendor - - $HOME/.cache/composer diff --git a/CHANGELOG.md b/CHANGELOG.md index ca47cdb9..132cee21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,111 @@ # Changelog +## Version 5.1.4 (2025-09-18) + +* Add PHPStan analysis, level 5 by @spaze in https://github.com/paragonie/halite/pull/195 +* Replace all `http://` links with the `https://` URL they redirect to by @GrahamCampbell in https://github.com/paragonie/halite/pull/196 +* Use Psalm 6 by @spaze in https://github.com/paragonie/halite/pull/198 +* Remove access modifier `final` from private methods by @junaidbinfarooq in https://github.com/paragonie/halite/pull/204 +* Ignore tests, workflows and .MD docs with "export-ignore" on .gitattr… by @erikn69 in https://github.com/paragonie/halite/pull/205 +* Expand test coverage by @paragonie-security in https://github.com/paragonie/halite/pull/206 +* Fixed the broken test coverage badge (https://github.com/paragonie/halite/pull/207 and https://github.com/paragonie/halite/pull/208) + +## Version 5.1.3 (2025-01-23) + +* Merged [#184](https://github.com/paragonie/halite/pull/194), which fixes PHP 8.4 deprecations with nullable types. + +## Version 5.1.2 (2024-05-08) + +* Use `#[SensitiveParameter]` annotation on some inputs + * This is defense in depth; we already wrapped most in `HiddenString` +* Updated dependencies + +## Version 5.1.1 (2024-04-19) + +* Support both sodium_compat v1 and v2. + [Learn more here](https://paragonie.com/blog/2024/04/release-sodium-compat-v2-and-future-our-polyfill-libraries). + +## Version 5.1.0 (2022-05-23) + +* Dropped PHP 8.0 support, increased minimum PHP version to 8.1. + * This is due to the significant performance difference between ext/sodium + and sodium_compat, and the functions we use in 5.x aren't available until + PHP 8.1. See [#178](https://github.com/paragonie/halite/issues/178). +* The 5.0.x branch will continue to *function* on PHP 8.0 but performance is + not guaranteed. + +## Version 5.0.0 (2022-01-19) + +* Increased minimum PHP version to 8.0. +* **Security:** Asymmetric encryption now uses HKDF-BLAKE2b to extract a 256-bit uniformly random bit string for the + encryption key, rather than using the raw X25519 output directly as an encryption key. This is important because + Elliptic Curve Diffie-Hellman results in a random group element, but that isn't necessarily a uniformly random bit + string. + * Because Halite v4 and earlier did not perform this step, it's superficially susceptible to + [Cheon's attack](https://crypto.stackexchange.com/a/67609). This reduces the effective security + from 125 bits (Pollard's rho) to 123 bits, but neither is a practical concern today. +* **Security:** Halite v5 uses the [PAE](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition) + strategy from PASETO to prevent canonicalization attacks. +* **Security:** Halite v5 appends the random salt to HKDF's `info` parameter instead of + the `salt` parameter. This allows us to meet the KDF Security Definition (which is + stronger than a mere Pseudo-Random Function). +* Encryption now uses XChaCha20 instead of XSalsa20. +* The `File` class no longer supports the `resource` type. To migrate code, wrap your + `resource` arguments in a `ReadOnlyFile` or `MutableFile` object. +* Added `File::asymmetricEncrypt()` and `File::asymmetricDecrypt()`. + +## Version 4.8.0 (2021-04-18) + +* Merged [#158](https://github.com/paragonie/halite/pull/158), which removes + the `final` access modifier from private methods and guarantees PHP 8 support. +* Migrated tests off of Travis CI, onto Github Actions instead. + +## Version 4.7.1 (2020-12-06) + +* Allow v2 of `paragonie/hidden-string` to be installed. + +## Version 4.7.0 (2020-12-03) + +* Merged [#154](https://github.com/paragonie/halite/pull/154), which supports + the SameSite cookie arguments on PHP 7.3+. +* Create a wrapper for `sodium_memzero()` to support sodium_compat. +* Added support for PHP 8. +* [#146](https://github.com/paragonie/halite/pull/146), + [#155](https://github.com/paragonie/halite/pull/155), + [#156](https://github.com/paragonie/halite/pull/156) -- + Various documentation improvements. + +## Version 4.6.0 (2019-09-12) + +* Merged [#138](https://github.com/paragonie/halite/pull/138), which adds + remote stream support to `ReadOnlyFile`. +* Merged [#140](https://github.com/paragonie/halite/pull/140), which saves + some overhead on hash recalculation. +* Merged [#136](https://github.com/paragonie/halite/pull/136) and + [#137](https://github.com/paragonie/halite/pull/137), which updated the + sodium stub files. These aren't strictly necessary anymore; with the + adoption of libsodium in PHP 7.2 and sodium_compat, most IDEs autocomplete + correctly. But fixing nits is always appreciated. +* Update minimum sodium_compat to v1.11.0. + +## Version 4.5.4 (2019-06-05) + +* Merged [#132](https://github.com/paragonie/halite/pull/132), which ensures + all Halite exceptions implement `Throwable`. +* Merged [#133](https://github.com/paragonie/halite/pull/133), which updates + the documentation for the `File` API. + Thanks [@elliot-sawyer](https://github.com/elliot-sawyer). +* Merged [#134](https://github.com/paragonie/halite/pull/134), which allows + `MutableFile` to be used on resources opened in `wb` mode. + Thanks [@christiaanbaartse](https://github.com/christiaanbaartse). +* Other minor documentation improvements. + +## Version 4.5.3 (2019-03-11) + +* Fixed some minor nuisances with Psalm and PHPUnit. +* Added reference to Halite-Legacy to the README. +* Updated docblocks. + ## Version 4.5.2 (2019-02-11) * Fixed [#116](https://github.com/paragonie/halite/issues/116). If the output file diff --git a/LICENSE b/LICENSE index a612ad98..4f573b70 100644 --- a/LICENSE +++ b/LICENSE @@ -357,7 +357,7 @@ Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. + file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE diff --git a/README.md b/README.md index a51efe71..bd95e4c1 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # Halite -[![Build Status](https://travis-ci.org/paragonie/halite.svg?branch=master)](https://travis-ci.org/paragonie/halite) +[![Build Status](https://github.com/paragonie/halite/actions/workflows/ci.yml/badge.svg)](https://github.com/paragonie/halite/actions) +[![Static Analysis](https://github.com/paragonie/halite/actions/workflows/psalm.yml/badge.svg)](https://github.com/paragonie/halite/actions) [![Latest Stable Version](https://poser.pugx.org/paragonie/halite/v/stable)](https://packagist.org/packages/paragonie/halite) [![Latest Unstable Version](https://poser.pugx.org/paragonie/halite/v/unstable)](https://packagist.org/packages/paragonie/halite) [![License](https://poser.pugx.org/paragonie/halite/license)](https://packagist.org/packages/paragonie/halite) [![Downloads](https://img.shields.io/packagist/dt/paragonie/halite.svg)](https://packagist.org/packages/paragonie/halite) -[![Coverage Status](https://coveralls.io/repos/github/paragonie/halite/badge.svg?branch=master)](https://coveralls.io/github/paragonie/halite?branch=master) +[![Coverage Status](https://raw.githubusercontent.com/paragonie/halite/refs/heads/image-data/coverage.svg)](https://github.com/paragonie/halite/actions/workflows/coverage.yml) **Halite** is a high-level cryptography interface that relies on [libsodium](https://pecl.php.net/package/libsodium) for all of its underlying cryptography operations. @@ -34,15 +35,19 @@ Before you can use Halite, you must choose a version that fits the requirements of your project. The differences between the requirements for the available versions of Halite are briefly highlighted below. -| | PHP | libsodium | PECL libsodium | Support | -|-------------------------------------------------------------|-------|-----------|----------------|---------------------------| -| Halite 4.1 and newer | 7.2.0 | 1.0.15 | N/A (standard) | :heavy_check_mark: Active | -| [Halite 4.0](https://github.com/paragonie/halite/tree/v4.0) | 7.2.0 | 1.0.13 | N/A (standard) | :heavy_check_mark: Active | -| [Halite 3](https://github.com/paragonie/halite/tree/v3.x) | 7.0.0 | 1.0.9 | 1.0.6 / 2.0.4 | :x: Not Supported | -| [Halite 2](https://github.com/paragonie/halite/tree/v2.2) | 7.0.0 | 1.0.9 | 1.0.6 | :x: Not Supported | -| [Halite 1](https://github.com/paragonie/halite/tree/v1.x) | 5.6.0 | 1.0.6 | 1.0.2 | :x: Not Supported | +| | PHP | libsodium | PECL libsodium | Support | +|--------------------------------------------------------------|-------|-----------|----------------|---------------------------| +| Halite 5.1 and newer | 8.1.0 | 1.0.18 | N/A (standard) | :heavy_check_mark: Active | +| Halite 5.0.x | 8.0.0 | 1.0.18 | N/A (standard) | :heavy_check_mark: Active | +| [Halite 4.1+](https://github.com/paragonie/halite/tree/v4.x) | 7.2.0 | 1.0.15 | N/A (standard) | :x: Not Supported | +| [Halite 4.0](https://github.com/paragonie/halite/tree/v4.0) | 7.2.0 | 1.0.13 | N/A (standard) | :x: Not Supported | +| [Halite 3](https://github.com/paragonie/halite/tree/v3.x) | 7.0.0 | 1.0.9 | 1.0.6 / 2.0.4 | :x: Not Supported | +| [Halite 2](https://github.com/paragonie/halite/tree/v2.2) | 7.0.0 | 1.0.9 | 1.0.6 | :x: Not Supported | +| [Halite 1](https://github.com/paragonie/halite/tree/v1.x) | 5.6.0 | 1.0.6 | 1.0.2 | :x: Not Supported | -If you need a version of Halite before 4.0, see the documentation relevant to that +Note: Halite 5.0.x works on PHP 8.0, but performance is worse than on PHP 8.1. + +If you need a version of Halite before 5.1, see the documentation relevant to that particular branch. **To install Halite, you first need to [install libsodium](https://paragonie.com/book/pecl-libsodium/read/00-intro.md#installing-libsodium).** @@ -56,16 +61,18 @@ If you're stuck, [this step-by-step guide contributed by @aolko](doc/Install-Gui Once you have the prerequisites installed, install Halite through [Composer](https://getcomposer.org/doc/00-intro.md): - composer require paragonie/halite:^4 + composer require paragonie/halite:^5 ### Commercial Support for Older Halite Versions -Free (gratis) support for Halite only extends to the most recent major version (currently 4). +Free (gratis) support for Halite only extends to the most recent major version (currently 5). If your company requires support for an older version of Halite, [contact Paragon Initiative Enterprises](https://paragonie.com/contact) to inquire about commercial support options. +If you need an easy way to migrate from older versions of Halite, check out [halite-legacy](https://github.com/paragonie/halite-legacy). + ## Using Halite in Your Project Check out the [documentation](doc). The basic Halite API is designed for simplicity: @@ -73,14 +80,18 @@ Check out the [documentation](doc). The basic Halite API is designed for simplic * Encryption * Symmetric * `Symmetric\Crypto::encrypt`([`HiddenString`](doc/Classes/HiddenString.md), [`EncryptionKey`](doc/Classes/Symmetric/EncryptionKey.md)): `string` + * `Symmetric\Crypto::encryptWithAD`([`HiddenString`](doc/Classes/HiddenString.md), [`EncryptionKey`](doc/Classes/Symmetric/EncryptionKey.md), `string`): `string` * `Symmetric\Crypto::decrypt`(`string`, [`EncryptionKey`](doc/Classes/Symmetric/EncryptionKey.md)): [`HiddenString`](doc/Classes/HiddenString.md) + * `Symmetric\Crypto::decryptWithAD`(`string`, [`EncryptionKey`](doc/Classes/Symmetric/EncryptionKey.md), `string`): [`HiddenString`](doc/Classes/HiddenString.md) * Asymmetric * Anonymous * `Asymmetric\Crypto::seal`([`HiddenString`](doc/Classes/HiddenString.md), [`EncryptionPublicKey`](doc/Classes/Asymmetric/EncryptionPublicKey.md)): `string` * `Asymmetric\Crypto::unseal`(`string`, [`EncryptionSecretKey`](doc/Classes/Asymmetric/EncryptionSecretKey.md)): [`HiddenString`](doc/Classes/HiddenString.md) * Authenticated * `Asymmetric\Crypto::encrypt`([`HiddenString`](doc/Classes/HiddenString.md), [`EncryptionSecretKey`](doc/Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](doc/Classes/Asymmetric/EncryptionPublicKey.md)): `string` + * `Asymmetric\Crypto::encryptWithAD`([`HiddenString`](doc/Classes/HiddenString.md), [`EncryptionSecretKey`](doc/Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](doc/Classes/Asymmetric/EncryptionPublicKey.md), `string`): `string` * `Asymmetric\Crypto::decrypt`(`string`, [`EncryptionSecretKey`](doc/Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](doc/Classes/Asymmetric/EncryptionPublicKey.md)): [`HiddenString`](doc/Classes/HiddenString.md) + * `Asymmetric\Crypto::decryptWithAD`(`string`, [`EncryptionSecretKey`](doc/Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](doc/Classes/Asymmetric/EncryptionPublicKey.md), `string`): [`HiddenString`](doc/Classes/HiddenString.md) * Authentication * Symmetric * `Symmetric\Crypto::authenticate`(`string`, [`AuthenticationKey`](doc/Classes/Symmetric/AuthenticationKey.md)): `string` diff --git a/composer.json b/composer.json index 877d3e86..642d846d 100644 --- a/composer.json +++ b/composer.json @@ -32,10 +32,11 @@ } ], "require": { - "php": "^7.2", - "paragonie/constant_time_encoding": "^2", - "paragonie/hidden-string": "^1", - "paragonie/sodium_compat": "^1.6" + "php": "^8.1", + "ext-json": "*", + "paragonie/constant_time_encoding": "^2|^3", + "paragonie/hidden-string": "^1|^2", + "paragonie/sodium_compat": "^1|^2" }, "autoload": { "psr-4": { @@ -43,8 +44,12 @@ } }, "require-dev": { - "phpunit/phpunit": "^7", - "vimeo/psalm": "^1|^2" + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9", + "vimeo/psalm": "^6.8" + }, + "scripts": { + "test": "phpunit && phpstan && psalm" }, "support": { "docs": "https://github.com/paragonie/halite/tree/master/doc" diff --git a/doc/Basic.md b/doc/Basic.md index 057b6d96..95273105 100644 --- a/doc/Basic.md +++ b/doc/Basic.md @@ -2,24 +2,28 @@ This is the Basic Halite API: - * Encryption +* Encryption * Symmetric - * `Symmetric\Crypto::encrypt`(`HiddenString`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md), `bool?`): `string` - * `Symmetric\Crypto::decrypt`(`string`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md), `bool?`): `HiddenString` + * `Symmetric\Crypto::encrypt`([`HiddenString`](Classes/HiddenString.md), [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md)): `string` + * `Symmetric\Crypto::encryptWithAD`([`HiddenString`](Classes/HiddenString.md), [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md), `string`): `string` + * `Symmetric\Crypto::decrypt`(`string`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md)): [`HiddenString`](Classes/HiddenString.md) + * `Symmetric\Crypto::decryptWithAD`(`string`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md), `string`): [`HiddenString`](Classes/HiddenString.md) * Asymmetric - * Anonymous - * `Asymmetric\Crypto::seal`(`HiddenString`, [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md), `bool?`): `string` - * `Asymmetric\Crypto::unseal`(`string`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), `bool?`): `HiddenString` - * Authenticated - * `Asymmetric\Crypto::encrypt`(`HiddenString`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md), `bool?`): `string` - * `Asymmetric\Crypto::decrypt`(`string`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md), `bool?`): `HiddenString` - * Authentication + * Anonymous + * `Asymmetric\Crypto::seal`([`HiddenString`](Classes/HiddenString.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)): `string` + * `Asymmetric\Crypto::unseal`(`string`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md)): [`HiddenString`](Classes/HiddenString.md) + * Authenticated + * `Asymmetric\Crypto::encrypt`([`HiddenString`](Classes/HiddenString.md), [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)): `string` + * `Asymmetric\Crypto::encryptWithAD`([`HiddenString`](Classes/HiddenString.md), [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md), `string`): `string` + * `Asymmetric\Crypto::decrypt`(`string`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)): [`HiddenString`](Classes/HiddenString.md) + * `Asymmetric\Crypto::decryptWithAD`(`string`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md), `string`): [`HiddenString`](Classes/HiddenString.md) +* Authentication * Symmetric - * `Symmetric\Crypto::authenticate`(`string`, [`AuthenticationKey`](Classes/Symmetric/AuthenticationKey.md), `bool?`): `string` - * `Symmetric\Crypto::verify`(`string`, [`AuthenticationKey`](Classes/Symmetric/AuthenticationKey.md), `string`, `bool?`): `bool` + * `Symmetric\Crypto::authenticate`(`string`, [`AuthenticationKey`](Classes/Symmetric/AuthenticationKey.md)): `string` + * `Symmetric\Crypto::verify`(`string`, [`AuthenticationKey`](Classes/Symmetric/AuthenticationKey.md), `string`): `bool` * Asymmetric - * `Asymmetric\Crypto::sign`(`string`, [`SignatureSecretKey`](Classes/Asymmetric/SignatureSecretKey.md), `bool?`): `string` - * `Asymmetric\Crypto::verify`(`string`, [`SignaturePublicKey`](Classes/Asymmetric/SignaturePublicKey.md), `string`, `bool?`): `bool` + * `Asymmetric\Crypto::sign`(`string`, [`SignatureSecretKey`](Classes/Asymmetric/SignatureSecretKey.md)): `string` + * `Asymmetric\Crypto::verify`(`string`, [`SignaturePublicKey`](Classes/Asymmetric/SignaturePublicKey.md), `string`): `bool` Most of the other [Halite features](Features.md) build on top of these simple APIs. @@ -41,7 +45,7 @@ For authentication functions, Halite will typically just return `false`. ## Encryption Encryption functions expect your message to be encapsulated in an instance -of the [`HiddenString`](Classes/HiddenString.md) class. Decryption functions +of the [`HiddenString`](https://github.com/paragonie/hidden-string) class. Decryption functions will return the decrypted plaintext in a `HiddenString` object. ### Symmetric-Key Encryption @@ -50,23 +54,24 @@ First, you'll need is an encryption key. The easiest way to obtain one is to generate it: ```php -$enc_key = \ParagonIE\Halite\KeyFactory::generateEncryptionKey(); +use ParagonIE\Halite\KeyFactory; +$enc_key = KeyFactory::generateEncryptionKey(); ``` This generates a strong random key. If you'd like to reuse it, you can simply store it in a file. ```php -\ParagonIE\Halite\KeyFactory::save($enc_key, '/path/to/encryption.key'); +KeyFactory::save($enc_key, '/path/to/encryption.key'); ``` Later, you can load it like so: ```php -$enc_key = \ParagonIE\Halite\KeyFactory::loadEncryptionKey('/path/to/encryption.key'); +$enc_key = KeyFactory::loadEncryptionKey('/path/to/encryption.key'); ``` -Or if you want to store it in a string +Or if you want to store it in a string, rather than on the filesystem: ```php $key_hex = KeyFactory::export($enc_key)->getString(); @@ -83,6 +88,8 @@ $enc_key = KeyFactory::importEncryptionKey(new HiddenString($key_hex)); **Encryption** should be rather straightforward: ```php +use ParagonIE\HiddenString\HiddenString; + $ciphertext = \ParagonIE\Halite\Symmetric\Crypto::encrypt( new HiddenString( "Your message here. Any string content will do just fine." @@ -91,7 +98,7 @@ $ciphertext = \ParagonIE\Halite\Symmetric\Crypto::encrypt( ); ``` -By default, `Crypto::encrypt()` will return a hexadecimal encoded string. If you +By default, `Crypto::encrypt()` will return a base64url-encoded string. If you want raw binary, simply pass `true` as the third argument (similar to the API used by PHP's `hash()` function). @@ -110,6 +117,57 @@ instance of `\ParagonIE\Halite\Symmetric\EncryptionKey`. If you're attempting to decrypt a raw binary string rather than a hex-encoded string, pass `true` to the third argument of `Crypto::decrypt`. +#### Additional Associated Data + +Sometimes encrypting a message isn't sufficient protection, and you also want to +bind an encrypted message to some context. Usually, this happens when you're concerned +with Confused Deputy Attacks. + +The simplest way to accomplish this is to use Halite's `EncryptWithAD()` and `DecryptWithAD()` +methods. + +**Note:** The Additional Associated Data is **NOT** stored in the encrypted message. +You must manage these strings yourself to ensure successful decryption. + +```php +use ParagonIE\HiddenString\HiddenString; + +$ad = 'Additional data that must be passed to both encrypt and decrypt calls'; + +$ciphertext = \ParagonIE\Halite\Symmetric\Crypto::encryptWithAD( + new HiddenString( + "Your message here. Any string content will do just fine." + ), + $enc_key, + $ad +); +``` + +This string must also be provided in the other direction: + +```php +$plaintext = \ParagonIE\Halite\Symmetric\Crypto::decryptWithAD( + $ciphertext, + $enc_key, + $ad +); +``` + +This will not succeed: + +```php +try { + \ParagonIE\Halite\Symmetric\Crypto::decryptWithAD( + $ciphertext, + $enc_key, + 'Incorrect String' + ); +} catch (\ParagonIE\Halite\Alerts\HaliteAlert $ex) { + var_dump($ex->getMessage()); + exit; +} +``` + ### Authenticated Asymmetric-Key Encryption (Encrypting) This API facilitates message encryption between to participants in a @@ -163,6 +221,34 @@ $message = \ParagonIE\Halite\Asymmetric\Crypto::decrypt( ); ``` +#### Additional Associated Data with Asymmetric Encryption + +If you've read the section on Symmetric Encryption, this should be unsurprising. + +```php +$ad = 'Additional Data that must be asserted on decrypt'; + +$send_to_bob = \ParagonIE\Halite\Asymmetric\Crypto::encryptWithAD( + new HiddenString( + "Your message here. Any string content will do just fine." + ), + $alice_secret, + $bob_public, + $ad +); +``` + +And decryption is similarly straightforward: + +```php +$message = \ParagonIE\Halite\Asymmetric\Crypto::decryptWithAD( + $received_ciphertext, + $alice_secret, + $bob_public, + $ad +); +``` + ### Anonymous Asymmetric-Key Encryption (Sealing) A sealing interface is one where you encrypt a message with a public key, such @@ -183,6 +269,8 @@ You want to only keep `$seal_public` stored outside of the trusted environment. **Encrypting** an anonymous message: ```php +use ParagonIE\HiddenString\HiddenString; + $sealed = \ParagonIE\Halite\Asymmetric\Crypto::seal( new HiddenString( "Your message here. Any string content will do just fine." diff --git a/doc/Classes/Alerts/FileError.md b/doc/Classes/Alerts/FileError.md new file mode 100644 index 00000000..1068243e --- /dev/null +++ b/doc/Classes/Alerts/FileError.md @@ -0,0 +1,5 @@ +# FileError extends [HaliteAlert](HaliteAlert.md) + +**Namespace**: `\ParagonIE\Halite\Alerts` + +This indicates a filesystem error occurred. diff --git a/doc/Classes/Alerts/FileModified.md b/doc/Classes/Alerts/FileModified.md index 6f0263a8..feccf3be 100644 --- a/doc/Classes/Alerts/FileModified.md +++ b/doc/Classes/Alerts/FileModified.md @@ -1,4 +1,4 @@ -# FileModified extends [HaliteAlert](HaliteAlert.md) +# FileModified extends [FileError](FileError.md) **Namespace**: `\ParagonIE\Halite\Alerts` diff --git a/doc/Classes/Alerts/HaliteAlertInterface.md b/doc/Classes/Alerts/HaliteAlertInterface.md new file mode 100644 index 00000000..f2a5cc52 --- /dev/null +++ b/doc/Classes/Alerts/HaliteAlertInterface.md @@ -0,0 +1,5 @@ +# HaliteAlertInterface extends Throwable + +**Namespace**: `\ParagonIE\Halite\Alerts` + +This is just a common interface for all Halite Alerts. diff --git a/doc/Classes/Asymmetric/Crypto.md b/doc/Classes/Asymmetric/Crypto.md index 5398e846..63a74bb8 100644 --- a/doc/Classes/Asymmetric/Crypto.md +++ b/doc/Classes/Asymmetric/Crypto.md @@ -6,11 +6,14 @@ ### `getSharedSecret()` -> `public` getSharedSecret([`EncryptionSecretKey`](EncryptionSecretKey.md) `$privateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$publicKey`, `$get_as_object = false`) : [`EncryptionKey`](../Symmetric/EncryptionKey.md) +> `public` getSharedSecret([`EncryptionSecretKey`](EncryptionSecretKey.md) `$privateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$publicKey`, `$get_as_object = false`, [`?Config`](Config.md) `$config = null`) : [`EncryptionKey`](../Symmetric/EncryptionKey.md) This method calculates a shared [`EncryptionKey`](../Symmetric/EncryptionKey.md) using X25519 (Elliptic Curve Diffie Hellman key agreement over Curve25519). +In Halite v5+, this X25519 output is processed with HKDF-BLAKE2b to ensure a uniformly +random bit string is returned, rather than merely a random group element. + ### `encrypt()` > `public` encrypt(`HiddenString $source`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `$encoding = Halite::ENCODE_BASE64URLSAFE`) : `string` @@ -44,17 +47,23 @@ This method will: key (step 4). 7. Return what should be the original plaintext. -### `encryptWithAd()` +### `encryptWithAD()` + +> `public` encryptWithAD(`HiddenString $plaintext`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `string` + +This is similar to `encrypt()`, except the `$additionalData` string is covered by the Message Authentication Code (MAC). -> `public` encryptWithAd(`HiddenString $plaintext`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `string` +Since Halite v5, this uses the [PAE](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition) +concept from PASETO. -This is similar to `encrypt()`, except the `$additionalData` string is prepended to the ciphertext (after the nonce) when calculating the Message Authentication Code (MAC). +### `decryptWithAD()` -### `decryptWithAd()` +> `public` decryptWithAD(`string $ciphertext`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `HiddenString` -> `public` decryptWithAd(`string $ciphertext`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `HiddenString` +This is similar to `decrypt()`, except the `$additionalData` string is covered by the Message Authentication Code (MAC). -This is similar to `decrypt()`, except the `$additionalData` string is prepended to the ciphertext (after the nonce) when calculating the Message Authentication Code (MAC). +Since Halite v5, this uses the [PAE](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition) +concept from PASETO. ### `seal()` diff --git a/doc/Classes/File.md b/doc/Classes/File.md index 88800e08..2a94da45 100644 --- a/doc/Classes/File.md +++ b/doc/Classes/File.md @@ -6,7 +6,7 @@ ### `checksum()` -> `public static` checksum(`$filepath`, [`Key`](Key.md) `$key = null`, `$raw = false`) : `string` +> `public static` checksum(`$filepath`, [`?Key`](Key.md) `$key = null`, `$raw = false`) : `string` Calculates a BLAKE2b-512 hash of the given file. @@ -36,6 +36,34 @@ Both `$input` and `$output` can be a string, a resource, or an object whose clas In the object case, `$input` must be an instance of [`ReadOnlyFile`](Stream/ReadOnlyFile.md) and `$output` must be an instance of [`MutableFile`](Stream/MutableFile.md). +### `asymmetricDecrypt()` + +> `public static` asymmetricDecrypt(`$input`, `$output`, [`EncryptionSecretKey`](Asymmetric/EncryptionSecretKey.md) `$recipientSK`, [`EncryptionPublicKey`](Asymmetric/EncryptionPublicKey.md) `$senderPK`, `string $aad = null`): `int` + +Decrypt the contents of `$input` (either a string containing the path to a file, or an open file +handle), and store it in the file (handle?) at `$output`. + +Both `$input` and `$output` can be a string, a resource, or an object whose class implements `StreamInterface`. +In the object case, `$input` must be an instance of [`ReadOnlyFile`](Stream/ReadOnlyFile.md) and `$output` must +be an instance of [`MutableFile`](Stream/MutableFile.md). + +The difference between `asymmetricDecrypt()` and `deseal()` is that `asymmetricDecrypt()` authenticates the sender, +while `unseal()` does not. (You can think of `unseal()` as anonymous public-key decryption.) + +### `asymmetricEncrypt()` + +> `public static` asymmetricEncrypt(`$input`, `$output`, [`EncryptionPublicKey`](Asymmetric/EncryptionPublicKey.md) `$recipientPK`, [`EncryptionSecretKey`](Asymmetric/EncryptionSecretKey.md) `$senderSK`, `string $aad = null`): `int` + +Encrypt the contents of `$input` (either a string containing the path to a file, or an open file +handle), and store it in the file (handle?) at `$output`. + +Both `$input` and `$output` can be a string, a resource, or an object whose class implements `StreamInterface`. +In the object case, `$input` must be an instance of [`ReadOnlyFile`](Stream/ReadOnlyFile.md) and `$output` must +be an instance of [`MutableFile`](Stream/MutableFile.md). + +The difference between `asymmetricEncrypt()` and `seal()` is that `asymmetricEncrypt()` authenticates the sender, while +`seal()` does not. (You can think of `seal()` as anonymous public-key encryption.) + ### `seal()` > `public static` seal(`$input`, `$output`, [`EncryptionPublicKey`](Asymmetric/EncryptionPublicKey.md) `$key`): `string` @@ -68,7 +96,7 @@ Calculate a digital signature of a file. ### `verify()` -> `public static` sign(`$input`, [`SignaturePublicKey`](Asymmetric/SignaturePublicKey.md) `$key`, `string $signature`, `boolean $raw_binary`): `bool` +> `public static` verify(`$input`, [`SignaturePublicKey`](Asymmetric/SignaturePublicKey.md) `$key`, `string $signature`, `boolean $raw_binary`): `bool` Verifies a digital signature of a file. diff --git a/doc/Classes/README.md b/doc/Classes/README.md index 653dc05f..c968f7bb 100644 --- a/doc/Classes/README.md +++ b/doc/Classes/README.md @@ -6,8 +6,10 @@ * [`\ParagonIE\Halite\Alerts\CannotSerializeKey`](Alerts/CannotSerializeKey.md) * [`\ParagonIE\Halite\Alerts\ConfigDirectiveNotFound`](Alerts/ConfigDirectiveNotFound.md) * [`\ParagonIE\Halite\Alerts\FileAccessDenied`](Alerts/FileAccessDenied.md) + * [`\ParagonIE\Halite\Alerts\FileError`](Alerts/FileError.md) * [`\ParagonIE\Halite\Alerts\FileModified`](Alerts/FileModified.md) * [`\ParagonIE\Halite\Alerts\HaliteAlert`](Alerts/HaliteAlert.md) (Base Exception for all Alerts) + * [`\ParagonIE\Halite\Alerts\HaliteAlertInterface`](Alerts/HaliteAlertInterface.md) (Common Interface) * [`\ParagonIE\Halite\Alerts\InvalidDigestLength`](Alerts/InvalidDigestLength.md) * [`\ParagonIE\Halite\Alerts\InvalidFlags`](Alerts/InvalidFlags.md) * [`\ParagonIE\Halite\Alerts\InvalidKey`](Alerts/InvalidKey.md) diff --git a/doc/Classes/Symmetric/Crypto.md b/doc/Classes/Symmetric/Crypto.md index 9b7d560c..b9b5ec96 100644 --- a/doc/Classes/Symmetric/Crypto.md +++ b/doc/Classes/Symmetric/Crypto.md @@ -45,13 +45,19 @@ Verify-then-decrypt a message. This method will: > `public` encryptWithAd(`HiddenString $plaintext`, [`EncryptionKey`](EncryptionKey.md) `$secretKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `string` -This is similar to `encrypt()`, except the `$additionalData` string is prepended to the ciphertext (after the nonce) when calculating the Message Authentication Code (MAC). +This is similar to `encrypt()`, except the `$additionalData` string is covered by the Message Authentication Code (MAC). -### `decryptWithAd()` +Since Halite v5, this uses the [PAE](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition) +concept from PASETO. -> `public` decryptWithAd(`string $ciphertext`, [`EncryptionKey`](EncryptionKey.md) `$secretKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `HiddenString` +### `decryptWithAD()` -This is similar to `decrypt()`, except the `$additionalData` string is prepended to the ciphertext (after the nonce) when calculating the Message Authentication Code (MAC). +> `public` decryptWithAD(`string $ciphertext`, [`EncryptionKey`](EncryptionKey.md) `$secretKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `HiddenString` + +This is similar to `decrypt()`, except the `$additionalData` string is covered by the Message Authentication Code (MAC). + +Since Halite v5, this uses the [PAE](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition) +concept from PASETO. ### `verify()` diff --git a/doc/Classes/Util.md b/doc/Classes/Util.md index 0aa7647f..aabac892 100644 --- a/doc/Classes/Util.md +++ b/doc/Classes/Util.md @@ -51,6 +51,16 @@ Returns a copy of a string without triggering PHP's optimizations. The string returned by this method can safely be used with `sodium_memzero()` without corrupting other copies of the same string. +### `splitKeys()` + +Splits a single key into two distinct keys (one for encryption, one for authentication). + +Since Halite v5, the HKDF salt parameter is not used. Instead, this randomness is appended +to the HKDF info parameter, in order to meet the [standard security definition for HKDF](https://eprint.iacr.org/2010/264). + +Additionally, this allows us to reuse the PRK (the value affected by the HKDF salt) value +for both derived keys, which results in a nice performance gain. + ### `xorStrings()` > `public static` xorStrings(`string $left`, `string $right`): `string` diff --git a/doc/Features.md b/doc/Features.md index c5aa8c65..815f3108 100644 --- a/doc/Features.md +++ b/doc/Features.md @@ -43,15 +43,15 @@ authenticated encryption and digital signatures. `File` allows developers to perform secure cryptographic operations on large files with a low memory footprint. -The `File` API looks like this: +The [`File`](Classes/File.md) API looks like this: * `File::checksum`(`file`, [`AuthenticationKey?`](Classes/Symmetric/AuthenticationKey.md), `bool?`): `string` * `File::encrypt`(`file`, `file`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md)) * `File::decrypt`(`file`, `file`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md)) * `File::seal`(`file`, `file`, [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)) * `File::unseal`(`file`, `file`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md)) -* `File::sign`(`file`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md)): `string` -* `File::verify`(`file`, [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)): `bool` +* `File::sign`(`file`, [`SignatureSecretKey`](Classes/Asymmetric/SignatureSecretKey.md)): `string` +* `File::verify`(`file`, [`SignaturePublicKey`](Classes/Asymmetric/SignaturePublicKey.md)): `bool` The `file` type indicates that the argument can be either a `string` containing the file's path, or a `resource` (open file handle). @@ -85,7 +85,7 @@ $keyed_checksum = \ParagonIE\Halite\File::checksum('/source/file/path', null, tr If you need to encrypt a file larger than the amount of memory available to PHP, you'll run into problems with just the basic `\ParagonIE\Halite\Symmetric\Crypto` -API. To work around these limitations, use `File::encryptFile()` instead. +API. To work around these limitations, use `File::encrypt()` instead. For example: @@ -103,7 +103,7 @@ its contents to `$outputFilename`. Decryption is straightforward as well: ```php -\ParagonIE\Halite\File::decryptFile( +\ParagonIE\Halite\File::decrypt( $inputFilename, $outputFilename, $enc_key diff --git a/doc/Primitives.md b/doc/Primitives.md index cc61b8fe..0fabd1db 100644 --- a/doc/Primitives.md +++ b/doc/Primitives.md @@ -1,11 +1,17 @@ # Cryptography Primitives used in Halite -* Symmetric-key encryption: [**XSalsa20**](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-stream) (note: only [authenticated encryption](https://tonyarcieri.com/all-the-crypto-code-youve-ever-written-is-probably-broken) is available through Halite) +* Symmetric-key encryption: (note: only [authenticated encryption](https://tonyarcieri.com/all-the-crypto-code-youve-ever-written-is-probably-broken) is available through Halite) + * [**XChaCha20**](https://libsodium.gitbook.io/doc/advanced/stream_ciphers/xchacha20) then BLAKE2b-MAC + * Previously, [**XSalsa20**](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-stream) then BLAKE2b-MAC * Symmetric-key authentication: **[BLAKE2b](https://download.libsodium.org/doc/hashing/generic_hashing.html#singlepart-example-with-a-key)** (keyed) -* Asymmetric-key encryption: [**X25519**](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-scalarmult) followed by symmetric-key authenticated encryption +* Asymmetric-key encryption: [**X25519**](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-scalarmult) + then [**HKDF-BLAKE2b**](Classes/Util.md#raw_keyed_hash), followed by symmetric-key authenticated encryption * Asymmetric-key digital signatures: [**Ed25519**](https://paragonie.com/book/pecl-libsodium/read/05-publickey-crypto.md#crypto-sign) * Checksums: [**BLAKE2b**](https://paragonie.com/book/pecl-libsodium/read/06-hashing.md#crypto-generichash) -* Key splitting: [**HKDF-BLAKE2b**](Classes/Util.md) +* Key splitting: [**HKDF-BLAKE2b**](Classes/Util.md#splitkeys) * Password-Based Key Derivation: [**Argon2**](https://paragonie.com/book/pecl-libsodium/read/07-password-hashing.md#crypto-pwhash-str) -In all cases, we follow an Encrypt then MAC construction, thus avoiding the [cryptographic doom principle](http://www.thoughtcrime.org/blog/the-cryptographic-doom-principle). +In all cases, we follow an Encrypt-then-MAC construction, thus avoiding the [cryptographic doom principle](https://moxie.org/2011/12/13/the-cryptographic-doom-principle.html). + +As a consequence of our use of a keyed BLAKE2b hash as a MAC, instead of GCM/Poly1305, +Halite ciphertexts are [**message committing**](https://eprint.iacr.org/2020/1456) which makes ciphertexts random key robust. diff --git a/doc/README.md b/doc/README.md index 0d6ef7f5..d97456f5 100644 --- a/doc/README.md +++ b/doc/README.md @@ -13,8 +13,10 @@ * [`\ParagonIE\Halite\Alerts\CannotSerializeKey`](Classes/Alerts/CannotSerializeKey.md) * [`\ParagonIE\Halite\Alerts\ConfigDirectiveNotFound`](Classes/Alerts/ConfigDirectiveNotFound.md) * [`\ParagonIE\Halite\Alerts\FileAccessDenied`](Classes/Alerts/FileAccessDenied.md) + * [`\ParagonIE\Halite\Alerts\FileError`](Classes/Alerts/FileError.md) * [`\ParagonIE\Halite\Alerts\FileModified`](Classes/Alerts/FileModified.md) * [`\ParagonIE\Halite\Alerts\HaliteAlert`](Classes/Alerts/HaliteAlert.md) (Base Exception for all Alerts) + * [`\ParagonIE\Halite\Alerts\HaliteAlertInterface`](Classes/Alerts/HaliteAlertInterface.md) (Common Interface) * [`\ParagonIE\Halite\Alerts\InvalidDigestLength`](Classes/Alerts/InvalidDigestLength.md) * [`\ParagonIE\Halite\Alerts\InvalidFlags`](Classes/Alerts/InvalidFlags.md) * [`\ParagonIE\Halite\Alerts\InvalidKey`](Classes/Alerts/InvalidKey.md) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..93e35770 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,115 @@ +parameters: + ignoreErrors: + - + message: '#^Call to function is_string\(\) with non\-empty\-string will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 4 + path: src/File.php + + - + message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 2 + path: src/File.php + + - + message: '#^Comparison operation "\<" between 10 and 10 is always false\.$#' + identifier: smaller.alwaysFalse + count: 1 + path: src/Halite.php + + - + message: '#^Property ParagonIE\\Halite\\Key\:\:\$keyMaterial \(string\) does not accept null\.$#' + identifier: assign.propertyType + count: 1 + path: src/Key.php + + - + message: '#^Comparison operation "\<\=" between int\<1, max\> and 0 is always false\.$#' + identifier: smallerOrEqual.alwaysFalse + count: 1 + path: src/Stream/MutableFile.php + + - + message: '#^Comparison operation "\<\=" between int\<1, max\> and 0 is always false\.$#' + identifier: smallerOrEqual.alwaysFalse + count: 1 + path: src/Stream/ReadOnlyFile.php + + - + message: '#^Offset 1\|int\<3, max\> on array\ on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: src/Structure/MerkleTree.php + + - + message: '#^Parameter &\$var @param\-out type of method ParagonIE\\Halite\\Util\:\:memzero\(\) expects null, int given\.$#' + identifier: paramOut.type + count: 1 + path: src/Util.php + + - + message: '#^Comparison operation "\<" between 10 and 7 is always false\.$#' + identifier: smaller.alwaysFalse + count: 2 + path: test/unit/AsymmetricTest.php + + - + message: '#^Comparison operation "\<" between 3 and 5 is always true\.$#' + identifier: smaller.alwaysTrue + count: 2 + path: test/unit/AsymmetricTest.php + + - + message: '#^Loose comparison using \=\= between 10 and 7 will always evaluate to false\.$#' + identifier: equal.alwaysFalse + count: 2 + path: test/unit/AsymmetricTest.php + + - + message: '#^Result of && is always false\.$#' + identifier: booleanAnd.alwaysFalse + count: 2 + path: test/unit/AsymmetricTest.php + + - + message: '#^Result of \|\| is always false\.$#' + identifier: booleanOr.alwaysFalse + count: 2 + path: test/unit/AsymmetricTest.php + + - + message: '#^Access to an undefined property object\{abc\: int\}&ParagonIE\\Halite\\Config\:\:\$missing\.$#' + identifier: property.notFound + count: 1 + path: test/unit/ConfigTest.php + + - + message: '#^Dead catch \- ParagonIE\\Halite\\Alerts\\ConfigDirectiveNotFound is never thrown in the try block\.$#' + identifier: catch.neverThrown + count: 1 + path: test/unit/ConfigTest.php + + - + message: '#^Parameter \#1 \$key of static method ParagonIE\\Halite\\KeyFactory\:\:export\(\) expects ParagonIE\\Halite\\Key\|ParagonIE\\Halite\\KeyPair, stdClass given\.$#' + identifier: argument.type + count: 1 + path: test/unit/KeyTest.php + + - + message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsString\(\) with string will always evaluate to true\.$#' + identifier: method.alreadyNarrowedType + count: 3 + path: test/unit/PasswordTest.php + + - + message: '#^Parameter \#1 \$file of class ParagonIE\\Halite\\Stream\\MutableFile constructor expects resource\|string, int given\.$#' + identifier: argument.type + count: 1 + path: test/unit/StreamTest.php + + - + message: '#^Parameter \#1 \$file of class ParagonIE\\Halite\\Stream\\ReadOnlyFile constructor expects resource\|string, int given\.$#' + identifier: argument.type + count: 1 + path: test/unit/StreamTest.php diff --git a/phpstan.dist.neon b/phpstan.dist.neon new file mode 100644 index 00000000..0e73c775 --- /dev/null +++ b/phpstan.dist.neon @@ -0,0 +1,9 @@ +parameters: + paths: + - src + - test + level: 5 + bootstrapFiles: + - src/HiddenString.php +includes: + - phpstan-baseline.neon diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5627e4a4..18f41084 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,46 +1,25 @@ - - - - ./src - - ./libsodium - ./vendor - ./build - ./stub - ./test - ./doc - - - - - - - - - ./test/unit - - + + + + ./src + + + ./libsodium + ./vendor + ./build + ./stub + ./test + ./doc + + + + + + + + + ./test/unit + + diff --git a/psalm.xml b/psalm.xml index a3d39b55..7fed6175 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,7 +1,7 @@ +> @@ -9,5 +9,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Alerts/CannotCloneKey.php b/src/Alerts/CannotCloneKey.php index 78e73b69..3ecadf68 100644 --- a/src/Alerts/CannotCloneKey.php +++ b/src/Alerts/CannotCloneKey.php @@ -1,5 +1,7 @@ Halite::ENCODE_BASE64URLSAFE, + 'HASH_DOMAIN_SEPARATION' => 'HaliteVersion5X25519SharedSecret', + 'HASH_SCALARMULT' => true, + ]; + } + } + if ($major === 4 || $major === 3) { + switch ($minor) { + case 0: + return [ + 'ENCODING' => Halite::ENCODE_BASE64URLSAFE, + 'HASH_DOMAIN_SEPARATION' => '', + 'HASH_SCALARMULT' => false, + ]; + } + } + throw new InvalidMessage( + 'Invalid version tag' + ); + } +} diff --git a/src/Asymmetric/Crypto.php b/src/Asymmetric/Crypto.php index 8191f6a3..2740ff0e 100644 --- a/src/Asymmetric/Crypto.php +++ b/src/Asymmetric/Crypto.php @@ -15,9 +15,26 @@ Halite, Key, Symmetric\Crypto as SymmetricCrypto, - Symmetric\EncryptionKey + Symmetric\EncryptionKey, + Util }; use ParagonIE\HiddenString\HiddenString; +use Error; +use RangeException; +use SodiumException; +use TypeError; +use const + SODIUM_CRYPTO_STREAM_KEYBYTES, + SODIUM_CRYPTO_SIGN_BYTES; +use function + is_string, + sodium_crypto_box_keypair_from_secretkey_and_publickey, + sodium_crypto_box_publickey_from_secretkey, + sodium_crypto_box_seal, + sodium_crypto_box_seal_open, + sodium_crypto_scalarmult, + sodium_crypto_sign_detached, + sodium_crypto_sign_verify_detached; /** * Class Crypto @@ -27,25 +44,25 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Asymmetric * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class Crypto { /** * Don't allow this to be instantiated. * - * @throws \Error + * @throws Error * @codeCoverageIgnore */ - final private function __construct() + private function __construct() { - throw new \Error('Do not instantiate'); + throw new Error('Do not instantiate'); } /** @@ -63,15 +80,18 @@ final private function __construct() * @throws InvalidMessage * @throws InvalidDigestLength * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function encrypt( + #[\SensitiveParameter] HiddenString $plaintext, + #[\SensitiveParameter] EncryptionSecretKey $ourPrivateKey, EncryptionPublicKey $theirPublicKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { - return static::encryptWithAd( + return self::encryptWithAD( $plaintext, $ourPrivateKey, $theirPublicKey, @@ -88,6 +108,7 @@ public static function encrypt( * @param EncryptionPublicKey $theirPublicKey * @param string $additionalData * @param string|bool $encoding + * * @return string * * @throws CannotPerformOperation @@ -95,22 +116,28 @@ public static function encrypt( * @throws InvalidMessage * @throws InvalidDigestLength * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ - public static function encryptWithAd( + public static function encryptWithAD( + #[\SensitiveParameter] HiddenString $plaintext, + #[\SensitiveParameter] EncryptionSecretKey $ourPrivateKey, EncryptionPublicKey $theirPublicKey, + #[\SensitiveParameter] string $additionalData = '', - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { /** @var HiddenString $ss */ $ss = self::getSharedSecret( $ourPrivateKey, - $theirPublicKey + $theirPublicKey, + false, + self::getAsymmetricConfig(Halite::HALITE_VERSION, true) ); $sharedSecretKey = new EncryptionKey($ss); - $ciphertext = SymmetricCrypto::encryptWithAd( + $ciphertext = SymmetricCrypto::encryptWithAD( $plaintext, $sharedSecretKey, $additionalData, @@ -136,15 +163,17 @@ public static function encryptWithAd( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function decrypt( string $ciphertext, + #[\SensitiveParameter] EncryptionSecretKey $ourPrivateKey, EncryptionPublicKey $theirPublicKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { - return static::decryptWithAd( + return self::decryptWithAD( $ciphertext, $ourPrivateKey, $theirPublicKey, @@ -161,6 +190,7 @@ public static function decrypt( * @param EncryptionPublicKey $theirPublicKey * @param string $additionalData * @param string|bool $encoding + * * @return HiddenString * * @throws CannotPerformOperation @@ -169,22 +199,27 @@ public static function decrypt( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ - public static function decryptWithAd( + public static function decryptWithAD( string $ciphertext, + #[\SensitiveParameter] EncryptionSecretKey $ourPrivateKey, EncryptionPublicKey $theirPublicKey, + #[\SensitiveParameter] string $additionalData = '', - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { /** @var HiddenString $ss */ $ss = self::getSharedSecret( $ourPrivateKey, - $theirPublicKey + $theirPublicKey, + false, + self::getAsymmetricConfig($ciphertext, $encoding) ); $sharedSecretKey = new EncryptionKey($ss); - $plaintext = SymmetricCrypto::decryptWithAd( + $plaintext = SymmetricCrypto::decryptWithAD( $ciphertext, $sharedSecretKey, $additionalData, @@ -203,32 +238,52 @@ public static function decryptWithAd( * @param EncryptionSecretKey $privateKey Private key (yours) * @param EncryptionPublicKey $publicKey Public key (theirs) * @param bool $get_as_object Get as a Key object? + * @param ?Config $config Asymmetric Config + * * @return HiddenString|Key * + * @throws CannotPerformOperation + * @throws InvalidDigestLength * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function getSharedSecret( + #[\SensitiveParameter] EncryptionSecretKey $privateKey, EncryptionPublicKey $publicKey, - bool $get_as_object = false - ): object { - if ($get_as_object) { - return new EncryptionKey( - new HiddenString( - \sodium_crypto_scalarmult( - $privateKey->getRawKeyMaterial(), - $publicKey->getRawKeyMaterial() + bool $get_as_object = false, + ?Config $config = null + ): HiddenString|Key { + if (!is_null($config)) { + if ($config->HASH_SCALARMULT) { + $hiddenString = new HiddenString( + Util::hkdfBlake2b( + sodium_crypto_scalarmult( + $privateKey->getRawKeyMaterial(), + $publicKey->getRawKeyMaterial() + ), + SODIUM_CRYPTO_STREAM_KEYBYTES, + (string) $config->HASH_DOMAIN_SEPARATION ) - ) - ); + ); + if ($get_as_object) { + return new EncryptionKey($hiddenString); + } + return $hiddenString; + } } - return new HiddenString( - \sodium_crypto_scalarmult( + + $hiddenString = new HiddenString( + sodium_crypto_scalarmult( $privateKey->getRawKeyMaterial(), $publicKey->getRawKeyMaterial() ) ); + if ($get_as_object) { + return new EncryptionKey($hiddenString); + } + return $hiddenString; } /** @@ -236,18 +291,21 @@ public static function getSharedSecret( * * @param HiddenString $plaintext Message to encrypt * @param EncryptionPublicKey $publicKey Public encryption key - * @param mixed $encoding Which encoding scheme to use? + * @param string|bool $encoding Which encoding scheme to use? + * * @return string Ciphertext * * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function seal( + #[\SensitiveParameter] HiddenString $plaintext, EncryptionPublicKey $publicKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { - $sealed = \sodium_crypto_box_seal( + $sealed = sodium_crypto_box_seal( $plaintext->getString(), $publicKey->getRawKeyMaterial() ); @@ -263,18 +321,21 @@ public static function seal( * * @param string $message Message to sign * @param SignatureSecretKey $privateKey Private signing key - * @param mixed $encoding Which encoding scheme to use? + * @param string|bool $encoding Which encoding scheme to use? + * * @return string Signature (detached) * * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function sign( string $message, + #[\SensitiveParameter] SignatureSecretKey $privateKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { - $signed = \sodium_crypto_sign_detached( + $signed = sodium_crypto_sign_detached( $message, $privateKey->getRawKeyMaterial() ); @@ -292,6 +353,7 @@ public static function sign( * @param SignatureSecretKey $secretKey Private signing key * @param PublicKey $recipientPublicKey Public encryption key * @param string|bool $encoding Which encoding scheme to use? + * * @return string * * @throws CannotPerformOperation @@ -299,13 +361,15 @@ public static function sign( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function signAndEncrypt( HiddenString $message, + #[\SensitiveParameter] SignatureSecretKey $secretKey, PublicKey $recipientPublicKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { if ($recipientPublicKey instanceof SignaturePublicKey) { $publicKey = $recipientPublicKey->getEncryptionPublicKey(); @@ -318,7 +382,7 @@ public static function signAndEncrypt( } $signature = self::sign($message->getString(), $secretKey, true); $plaintext = new HiddenString($signature . $message->getString()); - \sodium_memzero($signature); + Util::memzero($signature); $myEncKey = $secretKey->getEncryptionSecretKey(); return self::encrypt($plaintext, $myEncKey, $publicKey, $encoding); @@ -329,18 +393,21 @@ public static function signAndEncrypt( * * @param string $ciphertext Encrypted message * @param EncryptionSecretKey $privateKey Private decryption key - * @param mixed $encoding Which encoding scheme to use? + * @param string|bool $encoding Which encoding scheme to use? + * * @return HiddenString * * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function unseal( string $ciphertext, + #[\SensitiveParameter] EncryptionSecretKey $privateKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { $decoder = Halite::chooseEncoder($encoding, true); if ($decoder) { @@ -348,7 +415,7 @@ public static function unseal( try { /** @var string $ciphertext */ $ciphertext = $decoder($ciphertext); - } catch (\RangeException $ex) { + } catch (RangeException $ex) { throw new InvalidMessage( 'Invalid character encoding' ); @@ -357,25 +424,25 @@ public static function unseal( // Get a box keypair (needed by crypto_box_seal_open) $secret_key = $privateKey->getRawKeyMaterial(); - $public_key = \sodium_crypto_box_publickey_from_secretkey($secret_key); - $key_pair = \sodium_crypto_box_keypair_from_secretkey_and_publickey( + $public_key = sodium_crypto_box_publickey_from_secretkey($secret_key); + $key_pair = sodium_crypto_box_keypair_from_secretkey_and_publickey( $secret_key, $public_key ); // Wipe these immediately: - \sodium_memzero($secret_key); - \sodium_memzero($public_key); + Util::memzero($secret_key); + Util::memzero($public_key); // Now let's open that sealed box - $message = \sodium_crypto_box_seal_open( + $message = sodium_crypto_box_seal_open( $ciphertext, $key_pair ); // Always memzero after retrieving a value - \sodium_memzero($key_pair); - if (!\is_string($message)) { + Util::memzero($key_pair); + if (!is_string($message)) { // @codeCoverageIgnoreStart throw new InvalidKey( 'Incorrect secret key for this sealed message' @@ -393,18 +460,20 @@ public static function unseal( * @param string $message Message to verify * @param SignaturePublicKey $publicKey Public key * @param string $signature Signature - * @param mixed $encoding Which encoding scheme to use? + * @param string|bool $encoding Which encoding scheme to use? + * * @return bool * * @throws InvalidSignature * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function verify( string $message, SignaturePublicKey $publicKey, string $signature, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): bool { $decoder = Halite::chooseEncoder($encoding, true); if ($decoder) { @@ -420,7 +489,7 @@ public static function verify( // @codeCoverageIgnoreEnd } - return (bool) \sodium_crypto_sign_verify_detached( + return sodium_crypto_sign_verify_detached( $signature, $message, $publicKey->getRawKeyMaterial() @@ -434,6 +503,7 @@ public static function verify( * @param SignaturePublicKey $senderPublicKey Private signing key * @param SecretKey $givenSecretKey Public encryption key * @param string|bool $encoding Which encoding scheme to use? + * * @return HiddenString * * @throws CannotPerformOperation @@ -442,13 +512,15 @@ public static function verify( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function verifyAndDecrypt( string $ciphertext, SignaturePublicKey $senderPublicKey, + #[\SensitiveParameter] SecretKey $givenSecretKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { if ($givenSecretKey instanceof SignatureSecretKey) { $secretKey = $givenSecretKey->getEncryptionSecretKey(); @@ -466,4 +538,41 @@ public static function verifyAndDecrypt( } return new HiddenString($message); } + + /** + * Get the Asymmetric configuration expected for this Halite version + * + * @param string $ciphertext + * @param string|bool $encoding + * + * @return Config + * + * @throws InvalidMessage + * @throws InvalidType + */ + public static function getAsymmetricConfig( + string $ciphertext, + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE + ): Config { + $decoder = Halite::chooseEncoder($encoding, true); + if (is_callable($decoder)) { + // We were given encoded data: + // @codeCoverageIgnoreStart + try { + /** @var string $ciphertext */ + $ciphertext = $decoder($ciphertext); + } catch (RangeException $ex) { + throw new InvalidMessage( + 'Invalid character encoding' + ); + } + // @codeCoverageIgnoreEnd + } + $version = Binary::safeSubstr( + $ciphertext, + 0, + Halite::VERSION_TAG_LEN + ); + return Config::getConfig($version); + } } diff --git a/src/Asymmetric/EncryptionPublicKey.php b/src/Asymmetric/EncryptionPublicKey.php index ab1a6b52..1fde7c9c 100644 --- a/src/Asymmetric/EncryptionPublicKey.php +++ b/src/Asymmetric/EncryptionPublicKey.php @@ -5,6 +5,8 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; +use const SODIUM_CRYPTO_BOX_PUBLICKEYBYTES; +use function sprintf; /** * Class EncryptionPublicKey @@ -12,7 +14,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class EncryptionPublicKey extends PublicKey { @@ -26,9 +28,12 @@ final class EncryptionPublicKey extends PublicKey */ public function __construct(HiddenString $keyMaterial) { - if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) { + if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) { throw new InvalidKey( - 'Encryption public key must be CRYPTO_BOX_PUBLICKEYBYTES bytes long' + sprintf( + 'Encryption public key must be CRYPTO_BOX_PUBLICKEYBYTES (%d) bytes long', + SODIUM_CRYPTO_BOX_PUBLICKEYBYTES + ) ); } parent::__construct($keyMaterial); diff --git a/src/Asymmetric/EncryptionSecretKey.php b/src/Asymmetric/EncryptionSecretKey.php index 07105a9c..233dfee1 100644 --- a/src/Asymmetric/EncryptionSecretKey.php +++ b/src/Asymmetric/EncryptionSecretKey.php @@ -5,6 +5,12 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; +use SodiumException; +use TypeError; +use const SODIUM_CRYPTO_BOX_SECRETKEYBYTES; +use function + sodium_crypto_box_publickey_from_secretkey, + sprintf; /** * Class EncryptionSecretKey @@ -12,7 +18,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class EncryptionSecretKey extends SecretKey { @@ -20,16 +26,22 @@ final class EncryptionSecretKey extends SecretKey * EncryptionSecretKey constructor. * @param HiddenString $keyMaterial - The actual key data * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ - public function __construct(HiddenString $keyMaterial) - { - if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_BOX_SECRETKEYBYTES) { + public function __construct( + #[\SensitiveParameter] + HiddenString $keyMaterial, + ?HiddenString $pk = null + ) { + if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_BOX_SECRETKEYBYTES) { throw new InvalidKey( - 'Encryption secret key must be CRYPTO_BOX_SECRETKEYBYTES bytes long' + sprintf( + 'Encryption secret key must be CRYPTO_BOX_SECRETKEYBYTES (%d) bytes long', + SODIUM_CRYPTO_BOX_SECRETKEYBYTES + ) ); } - parent::__construct($keyMaterial); + parent::__construct($keyMaterial, $pk); } /** @@ -38,15 +50,18 @@ public function __construct(HiddenString $keyMaterial) * @return EncryptionPublicKey * * @throws InvalidKey - * @throws \TypeError + * @throws TypeError + * @throws SodiumException */ - public function derivePublicKey() + public function derivePublicKey(): EncryptionPublicKey { - $publicKey = \sodium_crypto_box_publickey_from_secretkey( - $this->getRawKeyMaterial() - ); + if (is_null($this->cachedPublicKey)) { + $this->cachedPublicKey = sodium_crypto_box_publickey_from_secretkey( + $this->getRawKeyMaterial() + ); + } return new EncryptionPublicKey( - new HiddenString($publicKey) + new HiddenString($this->cachedPublicKey) ); } } diff --git a/src/Asymmetric/PublicKey.php b/src/Asymmetric/PublicKey.php index 61bba20d..3987c4f4 100644 --- a/src/Asymmetric/PublicKey.php +++ b/src/Asymmetric/PublicKey.php @@ -4,6 +4,7 @@ use ParagonIE\Halite\Key; use ParagonIE\HiddenString\HiddenString; +use TypeError; /** * Class PublicKey @@ -11,7 +12,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class PublicKey extends Key { @@ -19,7 +20,7 @@ class PublicKey extends Key * PublicKey constructor. * @param HiddenString $keyMaterial - The actual key data * - * @throws \TypeError + * @throws TypeError */ public function __construct(HiddenString $keyMaterial) { diff --git a/src/Asymmetric/SecretKey.php b/src/Asymmetric/SecretKey.php index a99703e0..914941b9 100644 --- a/src/Asymmetric/SecretKey.php +++ b/src/Asymmetric/SecretKey.php @@ -4,6 +4,7 @@ use ParagonIE\Halite\Alerts\CannotPerformOperation; use ParagonIE\Halite\Key; use ParagonIE\HiddenString\HiddenString; +use TypeError; /** * Class SecretKey @@ -11,29 +12,37 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class SecretKey extends Key { + protected ?string $cachedPublicKey = null; + /** * SecretKey constructor. * @param HiddenString $keyMaterial - The actual key data * - * @throws \TypeError + * @throws TypeError */ - public function __construct(HiddenString $keyMaterial) - { + public function __construct( + #[\SensitiveParameter] + HiddenString $keyMaterial, + ?HiddenString $pk = null + ) { parent::__construct($keyMaterial); + if (!is_null($pk)) { + $this->cachedPublicKey = $pk->getString(); + } $this->isAsymmetricKey = true; } /** * See the appropriate derived class. * @throws CannotPerformOperation - * @return mixed + * @return PublicKey * @codeCoverageIgnore */ - public function derivePublicKey() + public function derivePublicKey(): PublicKey { throw new CannotPerformOperation( 'This is not implemented in the base class' diff --git a/src/Asymmetric/SignaturePublicKey.php b/src/Asymmetric/SignaturePublicKey.php index d5366781..6d9e4b06 100644 --- a/src/Asymmetric/SignaturePublicKey.php +++ b/src/Asymmetric/SignaturePublicKey.php @@ -5,6 +5,12 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; +use SodiumException; +use TypeError; +use const SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES; +use function + sodium_crypto_sign_ed25519_pk_to_curve25519, + sprintf; /** * Class SignaturePublicKey @@ -12,7 +18,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class SignaturePublicKey extends PublicKey { @@ -22,13 +28,16 @@ final class SignaturePublicKey extends PublicKey * @param HiddenString $keyMaterial - The actual key data * * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ public function __construct(HiddenString $keyMaterial) { - if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) { + if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) { throw new InvalidKey( - 'Signature public key must be CRYPTO_SIGN_PUBLICKEYBYTES bytes long' + sprintf( + 'Signature public key must be CRYPTO_SIGN_PUBLICKEYBYTES (%d) bytes long', + SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES + ) ); } parent::__construct($keyMaterial); @@ -39,13 +48,15 @@ public function __construct(HiddenString $keyMaterial) * Get an encryption public key from a signing public key. * * @return EncryptionPublicKey - * @throws \TypeError + * + * @throws SodiumException + * @throws TypeError * @throws InvalidKey */ public function getEncryptionPublicKey(): EncryptionPublicKey { $ed25519_pk = $this->getRawKeyMaterial(); - $x25519_pk = \sodium_crypto_sign_ed25519_pk_to_curve25519( + $x25519_pk = sodium_crypto_sign_ed25519_pk_to_curve25519( $ed25519_pk ); return new EncryptionPublicKey( diff --git a/src/Asymmetric/SignatureSecretKey.php b/src/Asymmetric/SignatureSecretKey.php index 4ebd1c19..f92d4e9b 100644 --- a/src/Asymmetric/SignatureSecretKey.php +++ b/src/Asymmetric/SignatureSecretKey.php @@ -5,6 +5,15 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; +use SodiumException; +use TypeError; +use const + SODIUM_CRYPTO_SIGN_SECRETKEYBYTES; +use function + sodium_crypto_sign_ed25519_sk_to_curve25519, + sodium_crypto_sign_ed25519_pk_to_curve25519, + sodium_crypto_sign_publickey_from_secretkey, + sprintf; /** * Class SignatureSecretKey @@ -12,7 +21,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class SignatureSecretKey extends SecretKey { @@ -22,16 +31,22 @@ final class SignatureSecretKey extends SecretKey * @param HiddenString $keyMaterial - The actual key data * * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ - public function __construct(HiddenString $keyMaterial) - { - if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) { + public function __construct( + #[\SensitiveParameter] + HiddenString $keyMaterial, + ?HiddenString $pk = null + ) { + if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) { throw new InvalidKey( - 'Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES bytes long' + sprintf( + 'Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES (%d) bytes long', + SODIUM_CRYPTO_SIGN_SECRETKEYBYTES + ) ); } - parent::__construct($keyMaterial); + parent::__construct($keyMaterial, $pk); $this->isSigningKey = true; } @@ -40,14 +55,17 @@ public function __construct(HiddenString $keyMaterial) * * @return SignaturePublicKey * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ - public function derivePublicKey() + public function derivePublicKey(): SignaturePublicKey { - $publicKey = \sodium_crypto_sign_publickey_from_secretkey( - $this->getRawKeyMaterial() - ); - return new SignaturePublicKey(new HiddenString($publicKey)); + if (is_null($this->cachedPublicKey)) { + $this->cachedPublicKey = sodium_crypto_sign_publickey_from_secretkey( + $this->getRawKeyMaterial() + ); + } + return new SignaturePublicKey(new HiddenString($this->cachedPublicKey)); } /** @@ -55,14 +73,24 @@ public function derivePublicKey() * * @return EncryptionSecretKey * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public function getEncryptionSecretKey(): EncryptionSecretKey { $ed25519_sk = $this->getRawKeyMaterial(); - $x25519_sk = \sodium_crypto_sign_ed25519_sk_to_curve25519( + $x25519_sk = sodium_crypto_sign_ed25519_sk_to_curve25519( $ed25519_sk ); + if (!is_null($this->cachedPublicKey)) { + $x25519_pk = sodium_crypto_sign_ed25519_pk_to_curve25519( + $this->cachedPublicKey + ); + return new EncryptionSecretKey( + new HiddenString($x25519_sk), + new HiddenString($x25519_pk) + ); + } return new EncryptionSecretKey( new HiddenString($x25519_sk) ); diff --git a/src/Config.php b/src/Config.php index 0f23dd92..b57f02a8 100644 --- a/src/Config.php +++ b/src/Config.php @@ -3,6 +3,7 @@ namespace ParagonIE\Halite; use ParagonIE\Halite\Alerts\ConfigDirectiveNotFound; +use function array_key_exists; /** * Class Config @@ -12,20 +13,42 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. + * + * @property string|bool $ENCODING + * + * AsymmetricCrypto: + * @property string $HASH_DOMAIN_SEPARATION + * @property bool $HASH_SCALARMULT + * + * SymmetricCrypto: + * @property bool $CHECKSUM_PUBKEY + * @property int $BUFFER + * @property int $HASH_LEN + * @property int $SHORTEST_CIPHERTEXT_LENGTH + * @property int $NONCE_BYTES + * @property int $HKDF_SALT_LEN + * @property string $ENC_ALGO + * @property string $MAC_ALGO + * @property int $MAC_SIZE + * @property int $PUBLICKEY_BYTES + * @property bool $HKDF_USE_INFO + * @property string $HKDF_SBOX + * @property string $HKDF_AUTH + * @property bool $USE_PAE */ class Config { /** * @var array */ - private $config; + private array $config; /** * Config constructor. @@ -45,7 +68,7 @@ public function __construct(array $set = []) */ public function __get(string $key) { - if (\array_key_exists($key, $this->config)) { + if (array_key_exists($key, $this->config)) { return $this->config[$key]; } throw new ConfigDirectiveNotFound($key); @@ -56,11 +79,10 @@ public function __get(string $key) * * @param string $key * @param mixed $value - * @return bool + * @return void * @codeCoverageIgnore */ - public function __set(string $key, $value = null) + public function __set(string $key, mixed $value = null): void { - return false; } } diff --git a/src/Contract/StreamInterface.php b/src/Contract/StreamInterface.php index 082b5011..be60a351 100644 --- a/src/Contract/StreamInterface.php +++ b/src/Contract/StreamInterface.php @@ -15,13 +15,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Contract * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ interface StreamInterface { @@ -68,9 +68,9 @@ public function remainingBytes(): int; * Write to a stream; prevent partial writes * * @param string $buf - * @param int $num (number of bytes) + * @param ?int $num (number of bytes) * @return int * @throws FileAccessDenied */ - public function writeBytes(string $buf, int $num = null): int; + public function writeBytes(string $buf, ?int $num = null): int; } diff --git a/src/Cookie.php b/src/Cookie.php index 3436bd37..01a776ee 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -20,6 +20,14 @@ EncryptionKey }; use ParagonIE\HiddenString\HiddenString; +use SodiumException; +use TypeError; +use function + hash_equals, + is_string, + json_decode, + json_encode, + setcookie; /** * Class Cookie @@ -29,22 +37,19 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. * * @codeCoverageIgnore */ final class Cookie { - /** - * @var EncryptionKey - */ - protected $key; + protected EncryptionKey $key; /** * Cookie constructor. @@ -54,12 +59,13 @@ public function __construct(EncryptionKey $key) { $this->key = $key; } + /** * Hide this from var_dump(), etc. * * @return array */ - public function __debugInfo() + public function __debugInfo(): array { return [ 'key' => 'private' @@ -67,34 +73,40 @@ public function __debugInfo() } /** - * Store a value in an encrypted cookie + * Fetch a value from an encrypted cookie * * @param string $name + * * @return mixed|null (typically an array) + * * @throws InvalidDigestLength * @throws InvalidSignature * @throws CannotPerformOperation * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ - public function fetch(string $name) - { + public function fetch( + #[\SensitiveParameter] + string $name + ) { if (!isset($_COOKIE[$name])) { return null; } try { /** @var string|array|int|float|bool $stored */ $stored = $_COOKIE[$name]; - if (!\is_string($stored)) { + if (!is_string($stored)) { throw new InvalidType('Cookie value is not a string'); } $config = self::getConfig($stored); + $encoding = $config->ENCODING; $decrypted = Crypto::decrypt( $stored, $this->key, - $config->ENCODING + $encoding ); - return \json_decode($decrypted->getString(), true); + return json_decode($decrypted->getString(), true); } catch (InvalidMessage $e) { return null; } @@ -107,7 +119,7 @@ public function fetch(string $name) * @return SymmetricConfig * * @throws InvalidMessage - * @throws \TypeError + * @throws TypeError */ protected static function getConfig(string $stored): SymmetricConfig { @@ -118,8 +130,7 @@ protected static function getConfig(string $stored): SymmetricConfig 'Encrypted password hash is way too short.' ); } - if (\hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX)) { - /** @var string $decoded */ + if (hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX)) { $decoded = Base64UrlSafe::decode($stored); return SymmetricConfig::getConfig( $decoded, @@ -140,37 +151,52 @@ protected static function getConfig(string $stored): SymmetricConfig * @param string $domain (defaults to NULL) * @param bool $secure (defaults to TRUE) * @param bool $httpOnly (defaults to TRUE) + * @param string $sameSite (defaults to ''; PHP >= 7.3.0) + * * @return bool * * @throws InvalidDigestLength * @throws CannotPerformOperation * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError + * + * @psalm-suppress InvalidArgument PHP version incompatibilities * @psalm-suppress MixedArgument */ public function store( + #[\SensitiveParameter] string $name, + #[\SensitiveParameter] $value, int $expire = 0, string $path = '/', string $domain = '', bool $secure = true, - bool $httpOnly = true + bool $httpOnly = true, + string $sameSite = '' ): bool { - return \setcookie( - $name, - Crypto::encrypt( - new HiddenString( - (string) \json_encode($value) - ), - $this->key + $val = Crypto::encrypt( + new HiddenString( + (string) json_encode($value) ), - (int) $expire, - (string) $path, - (string) $domain, - (bool) $secure, - (bool) $httpOnly + $this->key + ); + $options = [ + 'expires' => (int) $expire, + 'path' => (string) $path, + 'domain' => (string) $domain, + 'secure' => (bool) $secure, + 'httponly' => (bool) $httpOnly, + ]; + if ($sameSite !== '') { + $options['samesite'] = (string) $sameSite; + } + return setcookie( + $name, + $val, + $options ); } } diff --git a/src/EncryptionKeyPair.php b/src/EncryptionKeyPair.php index 6004f5d5..2db3f95b 100644 --- a/src/EncryptionKeyPair.php +++ b/src/EncryptionKeyPair.php @@ -17,30 +17,30 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class EncryptionKeyPair extends KeyPair { /** * @var EncryptionSecretKey */ - protected $secretKey; + protected Asymmetric\SecretKey $secretKey; /** * @var EncryptionPublicKey */ - protected $publicKey; + protected Asymmetric\PublicKey $publicKey; /** * Pass it a secret key, it will automatically generate a public key * - * @param array $keys + * @param Key ...$keys * * @throws InvalidKey * @throws \InvalidArgumentException @@ -131,8 +131,10 @@ public function __construct(Key ...$keys) * @throws InvalidKey * @throws \TypeError */ - protected function setupKeyPair(EncryptionSecretKey $secret): void - { + protected function setupKeyPair( + #[\SensitiveParameter] + EncryptionSecretKey $secret + ): void { $this->secretKey = $secret; $this->publicKey = $this->secretKey->derivePublicKey(); } diff --git a/src/File.php b/src/File.php index 719cdfec..f0dad8c7 100644 --- a/src/File.php +++ b/src/File.php @@ -23,9 +23,34 @@ Stream\MutableFile, Stream\ReadOnlyFile, Symmetric\AuthenticationKey, + Symmetric\Config as SymmetricConfig, Symmetric\EncryptionKey }; +use ParagonIE\ConstantTime\Binary; use ParagonIE\HiddenString\HiddenString; +use Exception; +use Error; +use Throwable; +use TypeError; +use SodiumException; +use const + SODIUM_CRYPTO_AUTH_KEYBYTES, + SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + SODIUM_CRYPTO_SECRETBOX_KEYBYTES, + SODIUM_CRYPTO_STREAM_NONCEBYTES; +use function + array_shift, + hash_equals, + is_string, + pack, + random_bytes, + sodium_crypto_generichash, + sodium_crypto_generichash_init, + sodium_crypto_generichash_update, + sodium_crypto_generichash_final, + sodium_crypto_scalarmult, + sodium_increment; /** * Class File @@ -35,25 +60,25 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class File { /** * Don't allow this to be instantiated. * - * @throws \Error + * @throws Error * @codeCoverageIgnore */ - final private function __construct() + private function __construct() { - throw new \Error('Do not instantiate'); + throw new Error('Do not instantiate'); } /** @@ -61,10 +86,10 @@ final private function __construct() * the entire file into memory. You may optionally supply a key to use in * the BLAKE2b hash. * - * @param string|resource|ReadOnlyFile $filePath - * @param Key $key (optional; expects SignaturePublicKey or + * @param string|ReadOnlyFile $filePath + * @param ?Key $key (optional; expects SignaturePublicKey or * AuthenticationKey) - * @param mixed $encoding Which encoding scheme to use for the checksum? + * @param bool|string $encoding Which encoding scheme to use for the checksum? * @return string The checksum * * @throws CannotPerformOperation @@ -73,12 +98,13 @@ final private function __construct() * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function checksum( - $filePath, - Key $key = null, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|ReadonlyFile $filePath, + ?Key $key = null, + bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): string { if ($filePath instanceof ReadOnlyFile) { $pos = $filePath->getPos(); @@ -92,33 +118,154 @@ public static function checksum( return $checksum; } - if (\is_resource($filePath) || \is_string($filePath)) { - $readOnly = new ReadOnlyFile($filePath); - try { - $checksum = self::checksumData( - $readOnly, - $key, - $encoding - ); - return $checksum; - } finally { - if (isset($readOnly)) { - $readOnly->close(); - } + $readOnly = new ReadOnlyFile($filePath); + try { + return self::checksumData( + $readOnly, + $key, + $encoding + ); + } finally { + $readOnly->close(); + } + } + + /** + * @param string|ReadOnlyFile $input + * @param string|MutableFile $output + * @param EncryptionPublicKey $recipientPK + * @param EncryptionSecretKey $senderSK + * @param string|null $aad + * @return int + * + * @throws CannotPerformOperation + * @throws FileAccessDenied + * @throws FileError + * @throws FileModified + * @throws InvalidDigestLength + * @throws InvalidKey + * @throws InvalidMessage + * @throws InvalidType + * @throws SodiumException + */ + public static function asymmetricEncrypt( + string|ReadOnlyFile $input, + string|MutableFile $output, + EncryptionPublicKey $recipientPK, + EncryptionSecretKey $senderSK, + ?string $aad = null + ): int { + try { + $key = new EncryptionKey( + new HiddenString( + sodium_crypto_generichash( + sodium_crypto_scalarmult( + $senderSK->getRawKeyMaterial(), + $recipientPK->getRawKeyMaterial() + ) . + $senderSK->derivePublicKey()->getRawKeyMaterial() . + $recipientPK->getRawKeyMaterial() + ) + ) + ); + if ($input instanceof ReadOnlyFile) { + $readOnly = $input; + } else { + $readOnly = new ReadOnlyFile($input); + } + if ($output instanceof MutableFile) { + $mutable = $output; + } else { + $mutable = new MutableFile($output); + } + return self::encryptData( + $readOnly, + $mutable, + $key, + $aad + ); + } finally { + if (isset($readOnly)) { + $readOnly->close(); + } + if (isset($mutable)) { + $mutable->close(); + } + } + } + + /** + * @param string|ReadOnlyFile $input + * @param string|MutableFile $output + * @param EncryptionSecretKey $recipientSK + * @param EncryptionPublicKey $senderPK + * @param string|null $aad + * @return bool + * + * @throws CannotPerformOperation + * @throws FileAccessDenied + * @throws FileError + * @throws FileModified + * @throws InvalidDigestLength + * @throws InvalidKey + * @throws InvalidMessage + * @throws InvalidType + * @throws SodiumException + */ + public static function asymmetricDecrypt( + string|ReadOnlyFile $input, + string|MutableFile $output, + EncryptionSecretKey $recipientSK, + EncryptionPublicKey $senderPK, + ?string $aad = null + ): bool { + try { + $key = new EncryptionKey( + new HiddenString( + sodium_crypto_generichash( + sodium_crypto_scalarmult( + $recipientSK->getRawKeyMaterial(), + $senderPK->getRawKeyMaterial() + ) . + $senderPK->getRawKeyMaterial() . + $recipientSK->derivePublicKey()->getRawKeyMaterial() + ) + ) + ); + if ($input instanceof ReadOnlyFile) { + $readOnly = $input; + } else { + $readOnly = new ReadOnlyFile($input); + } + if ($output instanceof MutableFile) { + $mutable = $output; + } else { + $mutable = new MutableFile($output); + } + return self::decryptData( + $readOnly, + $mutable, + $key, + $aad + ); + } finally { + if (isset($readOnly)) { + $readOnly->close(); + } + if (isset($mutable)) { + $mutable->close(); } } - throw new InvalidType( - 'Argument 1: Expected a filename or resource' - ); } /** * Encrypt a file using symmetric authenticated encryption. * - * @param string|resource|ReadOnlyFile $input Input file - * @param string|resource|MutableFile $output Output file - * @param EncryptionKey $key Symmetric encryption key - * @return int Number of bytes written + * @param string|ReadOnlyFile $input Input file + * @param string|MutableFile $output Output file + * @param EncryptionKey $key Symmetric encryption key + * @param string|null $aad Additional authenticated data + * @return int Number of bytes written * * @throws CannotPerformOperation * @throws FileAccessDenied @@ -128,56 +275,49 @@ public static function checksum( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException */ public static function encrypt( - $input, - $output, - EncryptionKey $key + string|ReadOnlyFile $input, + string|MutableFile $output, + EncryptionKey $key, + ?string $aad = null ): int { - if ( - (\is_resource($input) || \is_string($input) || ($input instanceof ReadOnlyFile)) - && - (\is_resource($output) || \is_string($output) || ($output instanceof MutableFile)) - ) { - try { - if ($input instanceof ReadOnlyFile) { - $readOnly = $input; - } else { - $readOnly = new ReadOnlyFile($input); - } - if ($output instanceof MutableFile) { - $mutable = $output; - } else { - $mutable = new MutableFile($output); - } - $data = self::encryptData( - $readOnly, - $mutable, - $key - ); - return $data; - } finally { - if (isset($readOnly)) { - $readOnly->close(); - } - if (isset($mutable)) { - $mutable->close(); - } + try { + if ($input instanceof ReadOnlyFile) { + $readOnly = $input; + } else { + $readOnly = new ReadOnlyFile($input); + } + if ($output instanceof MutableFile) { + $mutable = $output; + } else { + $mutable = new MutableFile($output); + } + return self::encryptData( + $readOnly, + $mutable, + $key, + $aad + ); + } finally { + if (isset($readOnly)) { + $readOnly->close(); + } + if (isset($mutable)) { + $mutable->close(); } } - throw new InvalidType( - 'Argument 1: Expected a filename or resource' - ); } /** * Decrypt a file using symmetric-key authenticated encryption. * - * @param string|resource|ReadOnlyFile $input Input file - * @param string|resource|MutableFile $output Output file - * @param EncryptionKey $key Symmetric encryption key - * @return bool TRUE if successful + * @param string|ReadOnlyFile $input Input file + * @param string|MutableFile $output Output file + * @param EncryptionKey $key Symmetric encryption key + * @param string|null $aad Additional authenticated data + * @return bool TRUE if successful * * @throws CannotPerformOperation * @throws FileAccessDenied @@ -187,56 +327,49 @@ public static function encrypt( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException */ public static function decrypt( - $input, - $output, - EncryptionKey $key + string|ReadOnlyFile $input, + string|MutableFile $output, + EncryptionKey $key, + ?string $aad = null ): bool { - if ( - (\is_resource($input) || \is_string($input) || ($input instanceof ReadOnlyFile)) - && - (\is_resource($output) || \is_string($output) || ($output instanceof MutableFile)) - ) { - try { - if ($input instanceof ReadOnlyFile) { - $readOnly = $input; - } else { - $readOnly = new ReadOnlyFile($input); - } - if ($output instanceof MutableFile) { - $mutable = $output; - } else { - $mutable = new MutableFile($output); - } - $data = self::decryptData( - $readOnly, - $mutable, - $key - ); - return $data; - } finally { - if (isset($readOnly)) { - $readOnly->close(); - } - if (isset($mutable)) { - $mutable->close(); - } + try { + if ($input instanceof ReadOnlyFile) { + $readOnly = $input; + } else { + $readOnly = new ReadOnlyFile($input); + } + if ($output instanceof MutableFile) { + $mutable = $output; + } else { + $mutable = new MutableFile($output); + } + return self::decryptData( + $readOnly, + $mutable, + $key, + $aad + ); + } finally { + if (isset($readOnly)) { + $readOnly->close(); + } + if (isset($mutable)) { + $mutable->close(); } } - throw new InvalidType( - 'Strings or file handles expected' - ); } /** * Encrypt a file using anonymous public-key encryption (with ciphertext * authentication). * - * @param string|resource|ReadOnlyFile $input Input file - * @param string|resource|MutableFile $output Output file - * @param EncryptionPublicKey $publicKey Recipient's encryption public key + * @param string|ReadOnlyFile $input Input file + * @param string|MutableFile $output Output file + * @param EncryptionPublicKey $publicKey Recipient's encryption public key + * @param string|null $aad Additional authenticated data * @return int * * @throws CannotPerformOperation @@ -245,58 +378,51 @@ public static function decrypt( * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType - * @throws \Exception - * @throws \TypeError + * @throws Exception + * @throws TypeError */ public static function seal( - $input, - $output, - EncryptionPublicKey $publicKey + string|ReadOnlyFile $input, + string|MutableFile $output, + EncryptionPublicKey $publicKey, + ?string $aad = null ): int { - if ( - (\is_resource($input) || \is_string($input) || ($input instanceof ReadOnlyFile)) - && - (\is_resource($output) || \is_string($output) || ($output instanceof MutableFile)) - ) { - try { - if ($input instanceof ReadOnlyFile) { - $readOnly = $input; - } else { - $readOnly = new ReadOnlyFile($input); - } - if ($output instanceof MutableFile) { - $mutable = $output; - } else { - $mutable = new MutableFile($output); - } - $data = self::sealData( - $readOnly, - $mutable, - $publicKey - ); - return $data; - } finally { - if (isset($readOnly)) { - $readOnly->close(); - } - if (isset($mutable)) { - $mutable->close(); - } + try { + if ($input instanceof ReadOnlyFile) { + $readOnly = $input; + } else { + $readOnly = new ReadOnlyFile($input); + } + if ($output instanceof MutableFile) { + $mutable = $output; + } else { + $mutable = new MutableFile($output); + } + return self::sealData( + $readOnly, + $mutable, + $publicKey, + $aad + ); + } finally { + if (isset($readOnly)) { + $readOnly->close(); + } + if (isset($mutable)) { + $mutable->close(); } } - throw new InvalidType( - 'Argument 1: Expected a filename or resource' - ); } /** * Decrypt a file using anonymous public-key encryption. Ciphertext * integrity is still assured thanks to the Encrypt-then-MAC construction. * - * @param string|resource|ReadOnlyFile $input Input file - * @param string|resource|MutableFile $output Output file - * @param EncryptionSecretKey $secretKey Recipient's encryption secret key - * @return bool TRUE on success + * @param string|ReadOnlyFile $input Input file + * @param string|MutableFile $output Output file + * @param EncryptionSecretKey $secretKey Recipient's encryption secret key + * @param string|null $aad Additional authenticated data + * @return bool TRUE on success * * @throws CannotPerformOperation * @throws FileAccessDenied @@ -306,47 +432,40 @@ public static function seal( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function unseal( - $input, - $output, - EncryptionSecretKey $secretKey + string|ReadOnlyFile $input, + string|MutableFile $output, + EncryptionSecretKey $secretKey, + ?string $aad = null ): bool { - if ( - (\is_resource($input) || \is_string($input) || ($input instanceof ReadOnlyFile)) - && - (\is_resource($output) || \is_string($output) || ($output instanceof MutableFile)) - ) { - try { - if ($input instanceof ReadOnlyFile) { - $readOnly = $input; - } else { - $readOnly = new ReadOnlyFile($input); - } - if ($output instanceof MutableFile) { - $mutable = $output; - } else { - $mutable = new MutableFile($output); - } - $data = self::unsealData( - $readOnly, - $mutable, - $secretKey - ); - return $data; - } finally { - if (isset($readOnly)) { - $readOnly->close(); - } - if (isset($mutable)) { - $mutable->close(); - } + try { + if ($input instanceof ReadOnlyFile) { + $readOnly = $input; + } else { + $readOnly = new ReadOnlyFile($input); + } + if ($output instanceof MutableFile) { + $mutable = $output; + } else { + $mutable = new MutableFile($output); + } + return self::unsealData( + $readOnly, + $mutable, + $secretKey, + $aad + ); + } finally { + if (isset($readOnly)) { + $readOnly->close(); + } + if (isset($mutable)) { + $mutable->close(); } } - throw new InvalidType( - 'Argument 1: Expected a filename or resource' - ); } /** @@ -357,9 +476,9 @@ public static function unseal( * Ed25519 public key used as a BLAKE2b key. * 2. Sign the checksum with Ed25519, using the corresponding public key. * - * @param string|resource|ReadOnlyFile $filename File name or file handle + * @param string|ReadOnlyFile $filename File name or ReadOnlyFile object * @param SignatureSecretKey $secretKey Secret key for digital signatures - * @param mixed $encoding Which encoding scheme to use for the signature? + * @param string|bool $encoding Which encoding scheme to use for the signature? * @return string Detached signature for the file * * @throws CannotPerformOperation @@ -368,12 +487,12 @@ public static function unseal( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws TypeError */ public static function sign( - $filename, + string|ReadOnlyFile $filename, SignatureSecretKey $secretKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { if ($filename instanceof ReadOnlyFile) { $pos = $filename->getPos(); @@ -385,36 +504,30 @@ public static function sign( ); $filename->reset($pos); return $signature; - } - if (\is_resource($filename) || \is_string($filename)) { + } else { $readOnly = new ReadOnlyFile($filename); try { - $signature = self::signData( + return self::signData( $readOnly, $secretKey, $encoding ); - return $signature; } finally { - if (isset($readOnly)) { - $readOnly->close(); - } + $readOnly->close(); } } - throw new InvalidType( - 'Argument 1: Expected a filename or resource' - ); } /** * Verify a digital signature for a file. * - * @param string|resource|ReadOnlyFile $filename File name or file handle + * @param string|ReadOnlyFile $filename File name or ReadOnlyFile object * @param SignaturePublicKey $publicKey Other party's signature public key * @param string $signature The signature we received - * @param mixed $encoding Which encoding scheme to use for the signature? + * @param string|bool $encoding Which encoding scheme to use for the signature? * * @return bool + * * @throws CannotPerformOperation * @throws FileAccessDenied * @throws FileError @@ -422,13 +535,14 @@ public static function sign( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function verify( - $filename, + string|ReadOnlyFile $filename, SignaturePublicKey $publicKey, string $signature, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): bool { if ($filename instanceof ReadOnlyFile) { $pos = $filename->getPos(); @@ -441,34 +555,28 @@ public static function verify( ); $filename->reset($pos); return $verified; - } - if (\is_resource($filename) || \is_string($filename)) { + } else { $readOnly = new ReadOnlyFile($filename); try { - $verified = self::verifyData( + return self::verifyData( $readOnly, $publicKey, $signature, $encoding ); - return $verified; } finally { - if (isset($readOnly)) { - $readOnly->close(); - } + $readOnly->close(); } } - throw new InvalidType( - 'Argument 1: Expected a filename or resource' - ); } /** * Calculate the BLAKE2b checksum of the contents of a file * * @param StreamInterface $fileStream - * @param Key $key - * @param mixed $encoding Which encoding scheme to use for the checksum? + * @param ?Key $key + * @param string|bool $encoding Which encoding scheme to use for the checksum? + * * @return string * * @throws CannotPerformOperation @@ -477,13 +585,13 @@ public static function verify( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ protected static function checksumData( StreamInterface $fileStream, - Key $key = null, - $encoding = Halite::ENCODE_BASE64URLSAFE + ?Key $key = null, + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { $config = self::getConfig( Halite::HALITE_VERSION_FILE, @@ -493,13 +601,13 @@ protected static function checksumData( // 1. Initialize the hash context if ($key instanceof AuthenticationKey) { // AuthenticationKey is for HMAC, but we can use it for keyed hashes too - $state = \sodium_crypto_generichash_init( + $state = sodium_crypto_generichash_init( $key->getRawKeyMaterial(), (int) $config->HASH_LEN ); } elseif($config->CHECKSUM_PUBKEY && ($key instanceof SignaturePublicKey)) { // In version 2, we use the public key as a hash key - $state = \sodium_crypto_generichash_init( + $state = sodium_crypto_generichash_init( $key->getRawKeyMaterial(), (int) $config->HASH_LEN ); @@ -510,7 +618,7 @@ protected static function checksumData( 'Argument 2: Expected an instance of AuthenticationKey or SignaturePublicKey' ); } else { - $state = \sodium_crypto_generichash_init( + $state = sodium_crypto_generichash_init( '', (int) $config->HASH_LEN ); @@ -521,23 +629,21 @@ protected static function checksumData( while ($fileStream->remainingBytes() > 0) { // Don't go past the file size even if $config->BUFFER is not an even multiple of it: if (($fileStream->getPos() + (int) $config->BUFFER) > $size) { - /** @var int $amount_to_read */ $amount_to_read = ($size - $fileStream->getPos()); } else { // @codeCoverageIgnoreStart - /** @var int $amount_to_read */ $amount_to_read = (int) $config->BUFFER; // @codeCoverageIgnoreEnd } $read = $fileStream->readBytes($amount_to_read); - \sodium_crypto_generichash_update($state, $read); + sodium_crypto_generichash_update($state, $read); } // 3. Do we want a raw checksum? $encoder = Halite::chooseEncoder($encoding); if ($encoder) { return (string) $encoder( - \sodium_crypto_generichash_final( + sodium_crypto_generichash_final( // @codeCoverageIgnoreStart $state, // @codeCoverageIgnoreEnd @@ -545,7 +651,7 @@ protected static function checksumData( ) ); } - return (string) \sodium_crypto_generichash_final( + return (string) sodium_crypto_generichash_final( // @codeCoverageIgnoreStart $state, // @codeCoverageIgnoreEnd @@ -557,6 +663,8 @@ protected static function checksumData( * @param ReadOnlyFile $input * @param MutableFile $output * @param EncryptionKey $key + * @param string|null $aad Additional authenticated data + * * @return int * * @throws CannotPerformOperation @@ -567,28 +675,30 @@ protected static function checksumData( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ protected static function encryptData( ReadOnlyFile $input, MutableFile $output, - EncryptionKey $key + EncryptionKey $key, + ?string $aad = null ): int { + /** @var SymmetricConfig $config */ $config = self::getConfig(Halite::HALITE_VERSION_FILE, 'encrypt'); // Generate a nonce and HKDF salt // @codeCoverageIgnoreStart try { - $firstNonce = \random_bytes((int) $config->NONCE_BYTES); - $hkdfSalt = \random_bytes((int) $config->HKDF_SALT_LEN); - } catch (\Throwable $ex) { + $firstNonce = random_bytes((int) $config->NONCE_BYTES); + $hkdfSalt = random_bytes((int) $config->HKDF_SALT_LEN); + } catch (Throwable $ex) { throw new CannotPerformOperation($ex->getMessage()); } // @codeCoverageIgnoreEnd // Let's split our key - list ($encKey, $authKey) = self::splitKeys($key, $hkdfSalt, $config); + list ($encKey, $authKey) = Util::splitKeys($key, $hkdfSalt, $config); // Write the header $output->writeBytes( @@ -597,7 +707,7 @@ protected static function encryptData( ); $output->writeBytes( $firstNonce, - \SODIUM_CRYPTO_STREAM_NONCEBYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES ); $output->writeBytes( $hkdfSalt, @@ -605,14 +715,36 @@ protected static function encryptData( ); // VERSION 2+ uses BMAC - $mac = \sodium_crypto_generichash_init($authKey); - \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); - \sodium_crypto_generichash_update($mac, $firstNonce); - \sodium_crypto_generichash_update($mac, $hkdfSalt); - /** @var string $mac */ + $mac = sodium_crypto_generichash_init($authKey); + // Number of pieces that go into MAC (header, first nonce, salt, ciphertext) -> 4 + if ($config->USE_PAE) { + // Number of pieces: + sodium_crypto_generichash_update($mac, pack('P', is_null($aad) ? 4 : 5)); + + // Length followed by piece: + sodium_crypto_generichash_update($mac, pack('P', Halite::VERSION_TAG_LEN)); + sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); + sodium_crypto_generichash_update($mac, pack('P', SODIUM_CRYPTO_STREAM_NONCEBYTES)); + sodium_crypto_generichash_update($mac, $firstNonce); + sodium_crypto_generichash_update($mac, pack('P', $config->HKDF_SALT_LEN)); + sodium_crypto_generichash_update($mac, $hkdfSalt); + if (!is_null($aad)) { + sodium_crypto_generichash_update($mac, pack('P', Binary::safeStrlen($aad))); + sodium_crypto_generichash_update($mac, pack('P', $aad)); + } + sodium_crypto_generichash_update($mac, pack('P', $input->remainingBytes())); + } else { + // Legacy version: No PAE + sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); + sodium_crypto_generichash_update($mac, $firstNonce); + sodium_crypto_generichash_update($mac, $hkdfSalt); + } + if (!is_string($mac)) { + throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); + } - \sodium_memzero($authKey); - \sodium_memzero($hkdfSalt); + Util::memzero($authKey); + Util::memzero($hkdfSalt); return self::streamEncrypt( $input, @@ -620,7 +752,7 @@ protected static function encryptData( new EncryptionKey( new HiddenString($encKey) ), - (string) $firstNonce, + $firstNonce, (string) $mac, $config ); @@ -632,6 +764,7 @@ protected static function encryptData( * @param ReadOnlyFile $input * @param MutableFile $output * @param EncryptionKey $key + * @param string|null $aad Additional authenticated data * @return bool * * @throws CannotPerformOperation @@ -642,12 +775,13 @@ protected static function encryptData( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException + * @throws SodiumException */ protected static function decryptData( ReadOnlyFile $input, MutableFile $output, - EncryptionKey $key + EncryptionKey $key, + ?string $aad = null ): bool { // Rewind $input->reset(0); @@ -659,10 +793,10 @@ protected static function decryptData( ); } // Parse the header, ensuring we get 4 bytes - /** @var string $header */ $header = $input->readBytes(Halite::VERSION_TAG_LEN); // Load the config + /** @var SymmetricConfig $config */ $config = self::getConfig($header, 'encrypt'); // Is this shorter than an encrypted empty string? @@ -673,25 +807,47 @@ protected static function decryptData( } // Let's grab the first nonce and salt - /** @var string $firstNonce */ $firstNonce = $input->readBytes((int) $config->NONCE_BYTES); - /** @var string $hkdfSalt */ $hkdfSalt = $input->readBytes((int) $config->HKDF_SALT_LEN); // Split our keys, begin the HMAC instance - list ($encKey, $authKey) = self::splitKeys($key, $hkdfSalt, $config); + list ($encKey, $authKey) = Util::splitKeys($key, $hkdfSalt, $config); // VERSION 2+ uses BMAC - $mac = \sodium_crypto_generichash_init($authKey); - \sodium_crypto_generichash_update($mac, $header); - \sodium_crypto_generichash_update($mac, $firstNonce); - \sodium_crypto_generichash_update($mac, $hkdfSalt); - /** @var string $mac */ - + $mac = sodium_crypto_generichash_init($authKey); + if ($config->USE_PAE) { + // Number of pieces: + sodium_crypto_generichash_update($mac, pack('P', is_null($aad) ? 4 : 5)); + + // Length followed by piece: + sodium_crypto_generichash_update($mac, pack('P', Halite::VERSION_TAG_LEN)); + sodium_crypto_generichash_update($mac, $header); + sodium_crypto_generichash_update($mac, pack('P', SODIUM_CRYPTO_STREAM_NONCEBYTES)); + sodium_crypto_generichash_update($mac, $firstNonce); + sodium_crypto_generichash_update($mac, pack('P', $config->HKDF_SALT_LEN)); + sodium_crypto_generichash_update($mac, $hkdfSalt); + + if (!is_null($aad)) { + sodium_crypto_generichash_update($mac, pack('P', Binary::safeStrlen($aad))); + sodium_crypto_generichash_update($mac, pack('P', $aad)); + } + sodium_crypto_generichash_update( + $mac, + pack('P', $input->remainingBytes() - ((int) $config->MAC_SIZE)) + ); + } else { + // Legacy version: No PAE + sodium_crypto_generichash_update($mac, $header); + sodium_crypto_generichash_update($mac, $firstNonce); + sodium_crypto_generichash_update($mac, $hkdfSalt); + } + if (!is_string($mac)) { + throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); + } $old_macs = self::streamVerify($input, Util::safeStrcpy($mac), $config); - \sodium_memzero($authKey); - \sodium_memzero($hkdfSalt); + Util::memzero($authKey); + Util::memzero($hkdfSalt); $ret = self::streamDecrypt( $input, @@ -705,7 +861,7 @@ protected static function decryptData( $old_macs ); - \sodium_memzero($encKey); + Util::memzero($encKey); unset($encKey); unset($authKey); unset($firstNonce); @@ -722,6 +878,7 @@ protected static function decryptData( * @param ReadOnlyFile $input * @param MutableFile $output * @param EncryptionPublicKey $publicKey + * @param ?string $aad * @return int * * @throws CannotPerformOperation @@ -730,13 +887,14 @@ protected static function decryptData( * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType - * @throws \Exception - * @throws \TypeError + * @throws Exception + * @throws TypeError */ protected static function sealData( ReadOnlyFile $input, MutableFile $output, - EncryptionPublicKey $publicKey + EncryptionPublicKey $publicKey, + ?string $aad = null ): int { // Generate a new keypair for this encryption $ephemeralKeyPair = KeyFactory::generateEncryptionKeyPair(); @@ -745,10 +903,15 @@ protected static function sealData( unset($ephemeralKeyPair); // Calculate the shared secret key - $sharedSecretKey = AsymmetricCrypto::getSharedSecret($ephSecret, $publicKey, true); + $sharedSecretKey = AsymmetricCrypto::getSharedSecret( + $ephSecret, + $publicKey, + true, + AsymmetricCrypto::getAsymmetricConfig(Halite::HALITE_VERSION_FILE, true) + ); // @codeCoverageIgnoreStart if (!($sharedSecretKey instanceof EncryptionKey)) { - throw new \TypeError('Shared secret is the wrong key type.'); + throw new TypeError('Shared secret is the wrong key type.'); } // @codeCoverageIgnoreEnd @@ -759,21 +922,21 @@ protected static function sealData( $config = self::getConfig(Halite::HALITE_VERSION_FILE, 'seal'); // Generate a nonce as per crypto_box_seal - $nonce = \sodium_crypto_generichash( + $nonce = sodium_crypto_generichash( $ephPublic->getRawKeyMaterial() . $publicKey->getRawKeyMaterial(), '', - \SODIUM_CRYPTO_STREAM_NONCEBYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES ); // Generate a random HKDF salt - $hkdfSalt = \random_bytes((int) $config->HKDF_SALT_LEN); + $hkdfSalt = random_bytes((int) $config->HKDF_SALT_LEN); // Split the keys /** * @var string $encKey * @var string $authKey */ - list ($encKey, $authKey) = self::splitKeys($sharedSecretKey, $hkdfSalt, $config); + list ($encKey, $authKey) = Util::splitKeys($sharedSecretKey, $hkdfSalt, $config); // Write the header: $output->writeBytes( @@ -782,7 +945,7 @@ protected static function sealData( ); $output->writeBytes( $ephPublic->getRawKeyMaterial(), - \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES + SODIUM_CRYPTO_BOX_PUBLICKEYBYTES ); $output->writeBytes( $hkdfSalt, @@ -790,17 +953,36 @@ protected static function sealData( ); // VERSION 2+ - $mac = \sodium_crypto_generichash_init($authKey); - - // We no longer need $authKey after we set up the hash context - \sodium_memzero($authKey); - - \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); - \sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + $mac = sodium_crypto_generichash_init($authKey); + Util::memzero($authKey); + if ($config->USE_PAE) { + // Number of pieces: + sodium_crypto_generichash_update($mac, pack('P', is_null($aad) ? 4 : 5)); + + // Length followed by piece: + sodium_crypto_generichash_update($mac, pack('P', Halite::VERSION_TAG_LEN)); + sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); + sodium_crypto_generichash_update($mac, pack('P', SODIUM_CRYPTO_BOX_PUBLICKEYBYTES)); + sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); + sodium_crypto_generichash_update($mac, pack('P', $config->HKDF_SALT_LEN)); + sodium_crypto_generichash_update($mac, $hkdfSalt); + if (!is_null($aad)) { + sodium_crypto_generichash_update($mac, pack('P', Binary::safeStrlen($aad))); + sodium_crypto_generichash_update($mac, pack('P', $aad)); + } + sodium_crypto_generichash_update($mac, pack('P', $input->remainingBytes())); + } else { + // Legacy version: No PAE + sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); + sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); + sodium_crypto_generichash_update($mac, $hkdfSalt); + } + if (!is_string($mac)) { + throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); + } unset($ephPublic); - \sodium_memzero($hkdfSalt); + Util::memzero($hkdfSalt); $ret = self::streamEncrypt( $input, @@ -812,7 +994,7 @@ protected static function sealData( (string) $mac, $config ); - \sodium_memzero($encKey); + Util::memzero($encKey); unset($encKey); unset($nonce); return $ret; @@ -824,6 +1006,7 @@ protected static function sealData( * @param ReadOnlyFile $input * @param MutableFile $output * @param EncryptionSecretKey $secretKey + * @param ?string $aad * @return bool * * @throws CannotPerformOperation @@ -834,12 +1017,14 @@ protected static function sealData( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws TypeError + * @throws SodiumException */ protected static function unsealData( ReadOnlyFile $input, MutableFile $output, - EncryptionSecretKey $secretKey + EncryptionSecretKey $secretKey, + ?string $aad = null ): bool { $publicKey = $secretKey ->derivePublicKey(); @@ -867,10 +1052,10 @@ protected static function unsealData( $hkdfSalt = $input->readBytes((int) $config->HKDF_SALT_LEN); // Generate the same nonce, as per sealData() - $nonce = \sodium_crypto_generichash( + $nonce = sodium_crypto_generichash( $ephPublic . $publicKey->getRawKeyMaterial(), '', - \SODIUM_CRYPTO_STREAM_NONCEBYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES ); // Create a key object out of the public key: @@ -881,11 +1066,12 @@ protected static function unsealData( $key = AsymmetricCrypto::getSharedSecret( $secretKey, $ephemeral, - true + true, + AsymmetricCrypto::getAsymmetricConfig($header, true) ); // @codeCoverageIgnoreStart if (!($key instanceof EncryptionKey)) { - throw new \TypeError(); + throw new TypeError(); } // @codeCoverageIgnoreEnd unset($ephemeral); @@ -894,22 +1080,46 @@ protected static function unsealData( * @var string $encKey * @var string $authKey */ - list ($encKey, $authKey) = self::splitKeys($key, $hkdfSalt, $config); + list ($encKey, $authKey) = Util::splitKeys($key, $hkdfSalt, $config); // We no longer need the original key after we split it unset($key); - $mac = \sodium_crypto_generichash_init($authKey); - - \sodium_crypto_generichash_update($mac, $header); - \sodium_crypto_generichash_update($mac, $ephPublic); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + $mac = sodium_crypto_generichash_init($authKey); + + if ($config->USE_PAE) { + // Number of pieces: + sodium_crypto_generichash_update($mac, pack('P', is_null($aad) ? 4 : 5)); + + // Length followed by piece: + sodium_crypto_generichash_update($mac, pack('P', Halite::VERSION_TAG_LEN)); + sodium_crypto_generichash_update($mac, $header); + sodium_crypto_generichash_update($mac, pack('P', SODIUM_CRYPTO_BOX_PUBLICKEYBYTES)); + sodium_crypto_generichash_update($mac, $ephPublic); + sodium_crypto_generichash_update($mac, pack('P', $config->HKDF_SALT_LEN)); + sodium_crypto_generichash_update($mac, $hkdfSalt); + if (!is_null($aad)) { + sodium_crypto_generichash_update($mac, pack('P', Binary::safeStrlen($aad))); + sodium_crypto_generichash_update($mac, pack('P', $aad)); + } + sodium_crypto_generichash_update( + $mac, + pack('P', $input->remainingBytes() - ((int) $config->MAC_SIZE)) + ); + } else { + // Legacy version: No PAE + sodium_crypto_generichash_update($mac, $header); + sodium_crypto_generichash_update($mac, $ephPublic); + sodium_crypto_generichash_update($mac, $hkdfSalt); + } + if (!is_string($mac)) { + throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); + } - /** @var string $mac */ $oldMACs = self::streamVerify($input, Util::safeStrcpy($mac), $config); // We no longer need these: - \sodium_memzero($authKey); - \sodium_memzero($hkdfSalt); + Util::memzero($authKey); + Util::memzero($hkdfSalt); $ret = self::streamDecrypt( $input, @@ -923,7 +1133,7 @@ protected static function unsealData( $oldMACs ); - \sodium_memzero($encKey); + Util::memzero($encKey); unset($encKey); unset($nonce); unset($mac); @@ -937,7 +1147,7 @@ protected static function unsealData( * * @param ReadOnlyFile $input * @param SignatureSecretKey $secretKey - * @param mixed $encoding Which encoding scheme to use for the signature? + * @param string|bool $encoding Which encoding scheme to use for the signature? * @return string * * @throws CannotPerformOperation @@ -946,12 +1156,12 @@ protected static function unsealData( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws TypeError */ protected static function signData( ReadOnlyFile $input, SignatureSecretKey $secretKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { $checksum = self::checksumData( $input, @@ -971,7 +1181,7 @@ protected static function signData( * @param $input (file handle) * @param SignaturePublicKey $publicKey * @param string $signature - * @param mixed $encoding Which encoding scheme to use for the signature? + * @param string|bool $encoding Which encoding scheme to use for the signature? * * @return bool * @@ -982,13 +1192,14 @@ protected static function signData( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ protected static function verifyData( ReadOnlyFile $input, SignaturePublicKey $publicKey, string $signature, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): bool { $checksum = self::checksumData($input, $publicKey, true); return AsymmetricCrypto::verify( @@ -1012,25 +1223,25 @@ protected static function getConfig( string $header, string $mode = 'encrypt' ): Config { - if (\ord($header[0]) !== 49 || \ord($header[1]) !== 65) { + if (Util::chrToInt($header[0]) !== 49 || Util::chrToInt($header[1]) !== 65) { // @codeCoverageIgnoreStart throw new InvalidMessage( 'Invalid version tag' ); // @codeCoverageIgnoreEnd } - $major = \ord($header[2]); - $minor = \ord($header[3]); + $major = Util::chrToInt($header[2]); + $minor = Util::chrToInt($header[3]); if ($mode === 'encrypt') { - return new Config( + return new SymmetricConfig( self::getConfigEncrypt($major, $minor) ); } elseif ($mode === 'seal') { - return new Config( + return new SymmetricConfig( self::getConfigSeal($major, $minor) ); } elseif ($mode === 'checksum') { - return new Config( + return new SymmetricConfig( self::getConfigChecksum($major, $minor) ); } @@ -1051,30 +1262,32 @@ protected static function getConfig( */ protected static function getConfigEncrypt(int $major, int $minor): array { - - if ($major === 4) { + if ($major === 5) { return [ 'SHORTEST_CIPHERTEXT_LENGTH' => 92, 'BUFFER' => 1048576, - 'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES, + 'NONCE_BYTES' => SODIUM_CRYPTO_STREAM_NONCEBYTES, 'HKDF_SALT_LEN' => 32, 'MAC_SIZE' => 32, + 'ENC_ALGO' => 'XChaCha20', + 'USE_PAE' => true, + 'HKDF_USE_INFO' => true, + 'HKDF_SBOX' => 'Halite|EncryptionKey', + 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' + ]; + } elseif ($major === 4) { + return [ + 'SHORTEST_CIPHERTEXT_LENGTH' => 92, + 'BUFFER' => 1048576, + 'NONCE_BYTES' => SODIUM_CRYPTO_STREAM_NONCEBYTES, + 'HKDF_SALT_LEN' => 32, + 'MAC_SIZE' => 32, + 'ENC_ALGO' => 'XSalsa20', + 'USE_PAE' => false, + 'HKDF_USE_INFO' => false, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; - } elseif ($major === 3) { - switch ($minor) { - case 0: - return [ - 'SHORTEST_CIPHERTEXT_LENGTH' => 92, - 'BUFFER' => 1048576, - 'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES, - 'HKDF_SALT_LEN' => 32, - 'MAC_SIZE' => 32, - 'HKDF_SBOX' => 'Halite|EncryptionKey', - 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' - ]; - } } // If we reach here, we've got an invalid version tag: // @codeCoverageIgnoreStart @@ -1094,7 +1307,7 @@ protected static function getConfigEncrypt(int $major, int $minor): array */ protected static function getConfigSeal(int $major, int $minor): array { - if ($major === 4) { + if ($major === 5) { switch ($minor) { case 0: return [ @@ -1102,25 +1315,31 @@ protected static function getConfigSeal(int $major, int $minor): array 'BUFFER' => 1048576, 'HKDF_SALT_LEN' => 32, 'MAC_SIZE' => 32, - 'PUBLICKEY_BYTES' => \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + 'PUBLICKEY_BYTES' => SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + 'ENC_ALGO' => 'XChaCha20', + 'USE_PAE' => true, + 'HKDF_USE_INFO' => true, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; } - } elseif ($major === 3) { - switch ($minor) { - case 0: - return [ - 'SHORTEST_CIPHERTEXT_LENGTH' => 100, - 'BUFFER' => 1048576, - 'HKDF_SALT_LEN' => 32, - 'MAC_SIZE' => 32, - 'PUBLICKEY_BYTES' => \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, - 'HKDF_SBOX' => 'Halite|EncryptionKey', - 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' - ]; + } elseif ($major === 4) { + switch ($minor) { + case 0: + return [ + 'SHORTEST_CIPHERTEXT_LENGTH' => 100, + 'BUFFER' => 1048576, + 'HKDF_SALT_LEN' => 32, + 'MAC_SIZE' => 32, + 'PUBLICKEY_BYTES' => SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + 'ENC_ALGO' => 'XSalsa20', + 'USE_PAE' => false, + 'HKDF_USE_INFO' => false, + 'HKDF_SBOX' => 'Halite|EncryptionKey', + 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' + ]; + } } - } // @codeCoverageIgnoreStart throw new InvalidMessage( 'Invalid version tag' @@ -1138,13 +1357,13 @@ protected static function getConfigSeal(int $major, int $minor): array */ protected static function getConfigChecksum(int $major, int $minor): array { - if ($major === 3 || $major === 4) { + if ($major === 3 || $major === 4 || $major === 5) { switch ($minor) { case 0: return [ 'CHECKSUM_PUBKEY' => true, 'BUFFER' => 1048576, - 'HASH_LEN' => \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + 'HASH_LEN' => SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ]; } } @@ -1155,40 +1374,6 @@ protected static function getConfigChecksum(int $major, int $minor): array // @codeCoverageIgnoreEnd } - /** - * Split a key using HKDF-BLAKE2b - * - * @param Key $master - * @param string $salt - * @param Config $config - * @return array - * - * @throws InvalidDigestLength - * @throws CannotPerformOperation - * @throws \TypeError - */ - protected static function splitKeys( - Key $master, - string $salt, - Config $config - ): array { - $binary = $master->getRawKeyMaterial(); - return [ - Util::hkdfBlake2b( - $binary, - \SODIUM_CRYPTO_SECRETBOX_KEYBYTES, - (string) $config->HKDF_SBOX, - $salt - ), - Util::hkdfBlake2b( - $binary, - \SODIUM_CRYPTO_AUTH_KEYBYTES, - (string) $config->HKDF_AUTH, - $salt - ) - ]; - } - /** * Stream encryption - Do not call directly * @@ -1205,9 +1390,10 @@ protected static function splitKeys( * @throws CannotPerformOperation * @throws FileAccessDenied * @throws FileModified - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ - final private static function streamEncrypt( + private static function streamEncrypt( ReadOnlyFile $input, MutableFile $output, EncryptionKey $encKey, @@ -1226,19 +1412,29 @@ final private static function streamEncrypt( : (int) $config->BUFFER ); - $encrypted = \sodium_crypto_stream_xor( - $read, - (string) $nonce, - $encKey->getRawKeyMaterial() - ); - \sodium_crypto_generichash_update($mac, $encrypted); + if ($config->ENC_ALGO === 'XChaCha20') { + $encrypted = sodium_crypto_stream_xchacha20_xor( + $read, + (string)$nonce, + $encKey->getRawKeyMaterial() + ); + } else { + $encrypted = sodium_crypto_stream_xor( + $read, + (string)$nonce, + $encKey->getRawKeyMaterial() + ); + } + sodium_crypto_generichash_update($mac, $encrypted); $written += $output->writeBytes($encrypted); - \sodium_increment($nonce); + sodium_increment($nonce); + } + if (is_string($nonce)) { + Util::memzero($nonce); } - \sodium_memzero($nonce); // Check that our input file was not modified before we MAC it - if (!\hash_equals($input->getHash(), $initHash)) { + if (!hash_equals($input->getHash(), $initHash)) { // @codeCoverageIgnoreStart throw new FileModified( 'Read-only file has been modified since it was opened for reading' @@ -1246,7 +1442,7 @@ final private static function streamEncrypt( // @codeCoverageIgnoreEnd } $written += $output->writeBytes( - \sodium_crypto_generichash_final($mac, (int) $config->MAC_SIZE), + sodium_crypto_generichash_final($mac, (int) $config->MAC_SIZE), (int) $config->MAC_SIZE ); return $written; @@ -1261,7 +1457,7 @@ final private static function streamEncrypt( * @param string $nonce * @param string $mac (hash context for BLAKE2b) * @param Config $config - * @param array &$chunk_macs + * @param string[] &$chunk_macs * * @return bool * @@ -1270,10 +1466,10 @@ final private static function streamEncrypt( * @throws FileAccessDenied * @throws FileModified * @throws InvalidMessage - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ - final private static function streamDecrypt( + private static function streamDecrypt( ReadOnlyFile $input, MutableFile $output, EncryptionKey $encKey, @@ -1283,7 +1479,6 @@ final private static function streamDecrypt( array &$chunk_macs ): bool { $start = $input->getPos(); - /** @var int $cipher_end */ $cipher_end = $input->getSize() - (int) $config->MAC_SIZE; // Begin the streaming decryption $input->reset($start); @@ -1304,10 +1499,12 @@ final private static function streamDecrypt( } // Version 2+ uses a keyed BLAKE2b hash instead of HMAC - \sodium_crypto_generichash_update($mac, $read); - /** @var string $mac */ + sodium_crypto_generichash_update($mac, $read); + if (!is_string($mac)) { + throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); + } $calcMAC = Util::safeStrcpy($mac); - $calc = \sodium_crypto_generichash_final($calcMAC, (int) $config->MAC_SIZE); + $calc = sodium_crypto_generichash_final($calcMAC, (int) $config->MAC_SIZE); if (empty($chunk_macs)) { // @codeCoverageIgnoreStart @@ -1317,9 +1514,8 @@ final private static function streamDecrypt( ); // @codeCoverageIgnoreEnd } else { - /** @var string $chunkMAC */ - $chunkMAC = \array_shift($chunk_macs); - if (!\hash_equals($chunkMAC, $calc)) { + $chunkMAC = array_shift($chunk_macs); + if (!hash_equals($chunkMAC, $calc)) { // This chunk was altered after the original MAC was verified // @codeCoverageIgnoreStart throw new InvalidMessage( @@ -1330,15 +1526,25 @@ final private static function streamDecrypt( } // This is where the decryption actually occurs: - $decrypted = \sodium_crypto_stream_xor( - $read, - (string) $nonce, - $encKey->getRawKeyMaterial() - ); + if ($config->ENC_ALGO === 'XChaCha20') { + $decrypted = sodium_crypto_stream_xchacha20_xor( + $read, + (string)$nonce, + $encKey->getRawKeyMaterial() + ); + } else { + $decrypted = sodium_crypto_stream_xor( + $read, + (string)$nonce, + $encKey->getRawKeyMaterial() + ); + } $output->writeBytes($decrypted); - \sodium_increment($nonce); + sodium_increment($nonce); + } + if (is_string($nonce)) { + Util::memzero($nonce); } - \sodium_memzero($nonce); return true; } @@ -1346,28 +1552,26 @@ final private static function streamDecrypt( * Recalculate and verify the HMAC of the input file * * @param ReadOnlyFile $input The file we are verifying - * @param string $mac (hash context) + * @param string $mac (hash context) * @param Config $config Version-specific settings * - * @return array Hashes of various chunks + * @return string[] Hashes of various chunks * * @throws CannotPerformOperation * @throws FileAccessDenied * @throws FileModified * @throws InvalidMessage - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ - final private static function streamVerify( + private static function streamVerify( ReadOnlyFile $input, - $mac, + string $mac, Config $config ): array { - /** @var int $start */ $start = $input->getPos(); // Grab the stored MAC: - /** @var int $cipher_end */ $cipher_end = $input->getSize() - (int) $config->MAC_SIZE; $input->reset($cipher_end); $stored_mac = $input->readBytes((int) $config->MAC_SIZE); @@ -1377,7 +1581,6 @@ final private static function streamVerify( $break = false; while (!$break && $input->getPos() < $cipher_end) { - /** * Would a full BUFFER read put it past the end of the * ciphertext? If so, only return a portion of the file. @@ -1394,12 +1597,13 @@ final private static function streamVerify( /** * We're updating our HMAC and nothing else */ - \sodium_crypto_generichash_update($mac, $read); - $mac = (string) $mac; + sodium_crypto_generichash_update($mac, $read); + if (!is_string($mac)) { + throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); + } // Copy the hash state then store the MAC of this chunk - /** @var string $chunkMAC */ $chunkMAC = Util::safeStrcpy($mac); - $chunkMACs []= \sodium_crypto_generichash_final( + $chunkMACs []= sodium_crypto_generichash_final( // @codeCoverageIgnoreStart $chunkMAC, // @codeCoverageIgnoreEnd @@ -1410,7 +1614,7 @@ final private static function streamVerify( /** * We should now have enough data to generate an identical MAC */ - $finalHMAC = \sodium_crypto_generichash_final( + $finalHMAC = sodium_crypto_generichash_final( // @codeCoverageIgnoreStart $mac, // @codeCoverageIgnoreEnd @@ -1420,7 +1624,7 @@ final private static function streamVerify( /** * Use hash_equals() to be timing-invariant */ - if (!\hash_equals($finalHMAC, $stored_mac)) { + if (!hash_equals($finalHMAC, $stored_mac)) { throw new InvalidMessage( 'Invalid message authentication code' ); diff --git a/src/Halite.php b/src/Halite.php index f5fa60d5..9ec561e8 100644 --- a/src/Halite.php +++ b/src/Halite.php @@ -2,6 +2,7 @@ declare(strict_types=1); namespace ParagonIE\Halite; +use Error; use ParagonIE\ConstantTime\{ Base32, Base32Hex, @@ -10,6 +11,12 @@ Hex }; use ParagonIE\Halite\Alerts\InvalidType; +use const + SODIUM_LIBRARY_MAJOR_VERSION, + SODIUM_LIBRARY_VERSION; +use function + extension_loaded, + implode; /** * Class Halite @@ -26,26 +33,26 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class Halite { - const VERSION = '4.4.0'; + const VERSION = '5.0.0'; - const HALITE_VERSION_KEYS = "\x31\x40\x04\x00"; - const HALITE_VERSION_FILE = "\x31\x41\x04\x00"; - const HALITE_VERSION = "\x31\x42\x04\x00"; + const HALITE_VERSION_KEYS = "\x31\x40\x05\x00"; + const HALITE_VERSION_FILE = "\x31\x41\x05\x00"; + const HALITE_VERSION = "\x31\x42\x05\x00"; /* Raw bytes (decoded) of the underlying ciphertext */ const VERSION_TAG_LEN = 4; - const VERSION_PREFIX = 'MUIEA'; - const VERSION_OLD_PREFIX = 'MUIDA'; + const VERSION_PREFIX = 'MUIFA'; + const VERSION_OLD_PREFIX = 'MUIEA'; const ENCODE_HEX = 'hex'; const ENCODE_BASE32 = 'base32'; @@ -56,29 +63,33 @@ final class Halite /** * Don't allow this to be instantiated. * - * @throws \Error + * @throws Error * @codeCoverageIgnore */ - final private function __construct() + private function __construct() { - throw new \Error('Do not instantiate'); + throw new Error('Do not instantiate'); } /** * Select which encoding/decoding function to use. * * @internal - * @param mixed $chosen + * @param string|bool $chosen * @param bool $decode - * @return callable|null + * @return ?callable + * * @throws InvalidType + * + * @psalm-suppress InvalidReturnStatement + * @psalm-suppress InvalidReturnType */ - public static function chooseEncoder($chosen, bool $decode = false) + public static function chooseEncoder(string|bool $chosen, bool $decode = false) { if ($chosen === true) { return null; } elseif ($chosen === false) { - return \implode( + return implode( '::', [ Hex::class, @@ -86,7 +97,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_BASE32) { - return \implode( + return implode( '::', [ Base32::class, @@ -94,7 +105,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_BASE32HEX) { - return \implode( + return implode( '::', [ Base32Hex::class, @@ -102,7 +113,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_BASE64) { - return \implode( + return implode( '::', [ Base64::class, @@ -110,7 +121,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_BASE64URLSAFE) { - return \implode( + return implode( '::', [ Base64UrlSafe::class, @@ -118,7 +129,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_HEX) { - return \implode( + return implode( '::', [ Hex::class, @@ -141,7 +152,7 @@ public static function chooseEncoder($chosen, bool $decode = false) */ public static function isLibsodiumSetupCorrectly(bool $echo = false): bool { - if (!\extension_loaded('sodium')) { + if (!extension_loaded('sodium')) { if ($echo) { echo "You do not have the sodium extension enabled.\n"; } @@ -149,11 +160,11 @@ public static function isLibsodiumSetupCorrectly(bool $echo = false): bool } // Require libsodium 1.0.15 - $major = \SODIUM_LIBRARY_MAJOR_VERSION; + $major = SODIUM_LIBRARY_MAJOR_VERSION; if ($major < 10) { if ($echo) { echo 'Halite needs libsodium 1.0.15 or higher. You have: ', - \SODIUM_LIBRARY_VERSION, "\n"; + SODIUM_LIBRARY_VERSION, "\n"; } return false; } diff --git a/src/Key.php b/src/Key.php index 0af8adad..c29b86ba 100644 --- a/src/Key.php +++ b/src/Key.php @@ -7,6 +7,7 @@ CannotSerializeKey }; use ParagonIE\HiddenString\HiddenString; +use TypeError; /** * Class Key @@ -16,35 +17,20 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class Key { - /** - * @var bool - */ - protected $isPublicKey = false; - - /** - * @var bool - */ - protected $isSigningKey = false; - - /** - * @var bool - */ - protected $isAsymmetricKey = false; - - /** - * @var string - */ - private $keyMaterial = ''; + protected bool $isPublicKey = false; + protected bool $isSigningKey = false; + protected bool $isAsymmetricKey = false; + private string $keyMaterial = ''; /** * Don't let this ever succeed @@ -90,7 +76,7 @@ public function __debugInfo() public function __destruct() { if (!$this->isPublicKey) { - \sodium_memzero($this->keyMaterial); + Util::memzero($this->keyMaterial); $this->keyMaterial = ''; } } @@ -133,7 +119,7 @@ public function __toString() * Get the actual key material * * @return string - * @throws \TypeError + * @throws TypeError */ public function getRawKeyMaterial(): string { diff --git a/src/KeyFactory.php b/src/KeyFactory.php index ec80affe..93f29c43 100644 --- a/src/KeyFactory.php +++ b/src/KeyFactory.php @@ -21,6 +21,40 @@ Symmetric\EncryptionKey }; use ParagonIE\HiddenString\HiddenString; +use SodiumException; +use Throwable; +use TypeError; +use const + SODIUM_CRYPTO_AUTH_KEYBYTES, + SODIUM_CRYPTO_BOX_SEEDBYTES, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13, + SODIUM_CRYPTO_PWHASH_SALTBYTES, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE, + SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE, + SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE, + SODIUM_CRYPTO_SIGN_SEEDBYTES, + SODIUM_CRYPTO_STREAM_KEYBYTES; +use function + file_get_contents, + file_put_contents, + hash_equals, + is_int, + is_readable, + random_bytes, + sodium_crypto_box_keypair, + sodium_crypto_box_publickey, + sodium_crypto_box_secretkey, + sodium_crypto_box_seed_keypair, + sodium_crypto_generichash, + sodium_crypto_pwhash, + sodium_crypto_sign_keypair, + sodium_crypto_sign_publickey, + sodium_crypto_sign_secretkey, + sodium_crypto_sign_seed_keypair; /** * Class KeyFactory @@ -30,13 +64,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class KeyFactory { @@ -46,7 +80,7 @@ final class KeyFactory const SENSITIVE = 'sensitive'; /** - * Generate an an authentication key (symmetric-key cryptography) + * Generate an authentication key (symmetric-key cryptography) * * @return AuthenticationKey * @throws CannotPerformOperation @@ -57,8 +91,8 @@ public static function generateAuthenticationKey(): AuthenticationKey { // @codeCoverageIgnoreStart try { - $secretKey = \random_bytes(\SODIUM_CRYPTO_AUTH_KEYBYTES); - } catch (\Throwable $ex) { + $secretKey = random_bytes(SODIUM_CRYPTO_AUTH_KEYBYTES); + } catch (Throwable $ex) { throw new CannotPerformOperation($ex->getMessage()); } // @codeCoverageIgnoreEnd @@ -68,19 +102,20 @@ public static function generateAuthenticationKey(): AuthenticationKey } /** - * Generate an an encryption key (symmetric-key cryptography) + * Generate an encryption key (symmetric-key cryptography) * * @return EncryptionKey + * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ public static function generateEncryptionKey(): EncryptionKey { // @codeCoverageIgnoreStart try { - $secretKey = \random_bytes(\SODIUM_CRYPTO_STREAM_KEYBYTES); - } catch (\Throwable $ex) { + $secretKey = random_bytes(SODIUM_CRYPTO_STREAM_KEYBYTES); + } catch (Throwable $ex) { throw new CannotPerformOperation($ex->getMessage()); } // @codeCoverageIgnoreEnd @@ -92,22 +127,25 @@ public static function generateEncryptionKey(): EncryptionKey /** * Generate a key pair for public key encryption * - * @return \ParagonIE\Halite\EncryptionKeyPair + * @return EncryptionKeyPair * * @throws InvalidKey - * @throws \TypeError + * @throws TypeError + * @throws SodiumException */ public static function generateEncryptionKeyPair(): EncryptionKeyPair { // Encryption keypair - $kp = \sodium_crypto_box_keypair(); - $secretKey = \sodium_crypto_box_secretkey($kp); + $kp = sodium_crypto_box_keypair(); + $secretKey = sodium_crypto_box_secretkey($kp); + $publicKey = sodium_crypto_box_publickey($kp); // Let's wipe our $kp variable - \sodium_memzero($kp); + Util::memzero($kp); return new EncryptionKeyPair( new EncryptionSecretKey( - new HiddenString($secretKey) + new HiddenString($secretKey), + new HiddenString($publicKey) ) ); } @@ -116,20 +154,25 @@ public static function generateEncryptionKeyPair(): EncryptionKeyPair * Generate a key pair for public key digital signatures * * @return SignatureKeyPair + * + * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function generateSignatureKeyPair(): SignatureKeyPair { // Encryption keypair - $kp = \sodium_crypto_sign_keypair(); - $secretKey = \sodium_crypto_sign_secretkey($kp); + $kp = sodium_crypto_sign_keypair(); + $secretKey = sodium_crypto_sign_secretkey($kp); + $publicKey = sodium_crypto_sign_publickey($kp); // Let's wipe our $kp variable - \sodium_memzero($kp); + Util::memzero($kp); return new SignatureKeyPair( new SignatureSecretKey( - new HiddenString($secretKey) + new HiddenString($secretKey), + new HiddenString($publicKey) ) ); } @@ -148,7 +191,8 @@ public static function generateSignatureKeyPair(): SignatureKeyPair * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function deriveAuthenticationKey( HiddenString $password, @@ -158,16 +202,15 @@ public static function deriveAuthenticationKey( ): AuthenticationKey { $kdfLimits = self::getSecurityLevels($level, $alg); // VERSION 2+ (argon2) - if (Binary::safeStrlen($salt) !== \SODIUM_CRYPTO_PWHASH_SALTBYTES) { + if (Binary::safeStrlen($salt) !== SODIUM_CRYPTO_PWHASH_SALTBYTES) { // @codeCoverageIgnoreStart throw new InvalidSalt( - 'Expected ' . \SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) + 'Expected ' . SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) ); // @codeCoverageIgnoreEnd } - /** @var string $secretKey */ - $secretKey = @\sodium_crypto_pwhash( - \SODIUM_CRYPTO_AUTH_KEYBYTES, + $secretKey = sodium_crypto_pwhash( + SODIUM_CRYPTO_AUTH_KEYBYTES, $password->getString(), $salt, $kdfLimits[0], @@ -190,10 +233,12 @@ public static function deriveAuthenticationKey( * (You can safely use the default) * * @return EncryptionKey + * * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function deriveEncryptionKey( HiddenString $password, @@ -203,16 +248,15 @@ public static function deriveEncryptionKey( ): EncryptionKey { $kdfLimits = self::getSecurityLevels($level, $alg); // VERSION 2+ (argon2) - if (Binary::safeStrlen($salt) !== \SODIUM_CRYPTO_PWHASH_SALTBYTES) { + if (Binary::safeStrlen($salt) !== SODIUM_CRYPTO_PWHASH_SALTBYTES) { // @codeCoverageIgnoreStart throw new InvalidSalt( - 'Expected ' . \SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) + 'Expected ' . SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) ); // @codeCoverageIgnoreEnd } - /** @var string $secretKey */ - $secretKey = @\sodium_crypto_pwhash( - \SODIUM_CRYPTO_STREAM_KEYBYTES, + $secretKey = sodium_crypto_pwhash( + SODIUM_CRYPTO_STREAM_KEYBYTES, $password->getString(), $salt, $kdfLimits[0], @@ -238,7 +282,8 @@ public static function deriveEncryptionKey( * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function deriveEncryptionKeyPair( HiddenString $password, @@ -248,31 +293,32 @@ public static function deriveEncryptionKeyPair( ): EncryptionKeyPair { $kdfLimits = self::getSecurityLevels($level, $alg); // VERSION 2+ (argon2) - if (Binary::safeStrlen($salt) !== \SODIUM_CRYPTO_PWHASH_SALTBYTES) { + if (Binary::safeStrlen($salt) !== SODIUM_CRYPTO_PWHASH_SALTBYTES) { // @codeCoverageIgnoreStart throw new InvalidSalt( - 'Expected ' . \SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) + 'Expected ' . SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) ); // @codeCoverageIgnoreEnd } // Diffie Hellman key exchange key pair - /** @var string $seed */ - $seed = @\sodium_crypto_pwhash( - \SODIUM_CRYPTO_BOX_SEEDBYTES, + $seed = sodium_crypto_pwhash( + SODIUM_CRYPTO_BOX_SEEDBYTES, $password->getString(), $salt, $kdfLimits[0], $kdfLimits[1], $alg ); - $keyPair = \sodium_crypto_box_seed_keypair($seed); - $secretKey = \sodium_crypto_box_secretkey($keyPair); + $keyPair = sodium_crypto_box_seed_keypair($seed); + $secretKey = sodium_crypto_box_secretkey($keyPair); + $publicKey = sodium_crypto_box_publickey($keyPair); // Let's wipe our $kp variable - \sodium_memzero($keyPair); + Util::memzero($keyPair); return new EncryptionKeyPair( new EncryptionSecretKey( - new HiddenString($secretKey) + new HiddenString($secretKey), + new HiddenString($publicKey) ) ); } @@ -288,10 +334,11 @@ public static function deriveEncryptionKeyPair( * * @return SignatureKeyPair * + * @throws CannotPerformOperation * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType - * @throws \TypeError + * @throws SodiumException */ public static function deriveSignatureKeyPair( HiddenString $password, @@ -301,31 +348,32 @@ public static function deriveSignatureKeyPair( ): SignatureKeyPair { $kdfLimits = self::getSecurityLevels($level, $alg); // VERSION 2+ (argon2) - if (Binary::safeStrlen($salt) !== \SODIUM_CRYPTO_PWHASH_SALTBYTES) { + if (Binary::safeStrlen($salt) !== SODIUM_CRYPTO_PWHASH_SALTBYTES) { // @codeCoverageIgnoreStart throw new InvalidSalt( - 'Expected ' . \SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) + 'Expected ' . SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) ); // @codeCoverageIgnoreEnd } // Digital signature keypair - /** @var string $seed */ - $seed = @\sodium_crypto_pwhash( - \SODIUM_CRYPTO_SIGN_SEEDBYTES, + $seed = sodium_crypto_pwhash( + SODIUM_CRYPTO_SIGN_SEEDBYTES, $password->getString(), $salt, $kdfLimits[0], $kdfLimits[1], $alg ); - $keyPair = \sodium_crypto_sign_seed_keypair($seed); - $secretKey = \sodium_crypto_sign_secretkey($keyPair); + $keyPair = sodium_crypto_sign_seed_keypair($seed); + $secretKey = sodium_crypto_sign_secretkey($keyPair); + $publicKey = sodium_crypto_sign_publickey($keyPair); // Let's wipe our $kp variable - \sodium_memzero($keyPair); + Util::memzero($keyPair); return new SignatureKeyPair( new SignatureSecretKey( - new HiddenString($secretKey) + new HiddenString($secretKey), + new HiddenString($publicKey) ) ); } @@ -335,7 +383,9 @@ public static function deriveSignatureKeyPair( * * @param string $level * @param int $alg + * * @return int[] + * * @throws InvalidType * @codeCoverageIgnore */ @@ -350,8 +400,8 @@ public static function getSecurityLevels( return [4, 33554432]; } return [ - \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, - \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE + SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE ]; case self::MODERATE: if ($alg === SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13) { @@ -359,8 +409,8 @@ public static function getSecurityLevels( return [6, 134217728]; } return [ - \SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE, - \SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE + SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE ]; case self::SENSITIVE: if ($alg === SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13) { @@ -368,8 +418,8 @@ public static function getSecurityLevels( return [8, 536870912]; } return [ - \SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE, - \SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE + SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE ]; default: throw new InvalidType( @@ -382,10 +432,12 @@ public static function getSecurityLevels( * Load a symmetric authentication key from a string * * @param HiddenString $keyData + * * @return AuthenticationKey * * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importAuthenticationKey(HiddenString $keyData): AuthenticationKey { @@ -402,10 +454,12 @@ public static function importAuthenticationKey(HiddenString $keyData): Authentic * Load a symmetric encryption key from a string * * @param HiddenString $keyData + * * @return EncryptionKey * * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importEncryptionKey(HiddenString $keyData): EncryptionKey { @@ -422,10 +476,12 @@ public static function importEncryptionKey(HiddenString $keyData): EncryptionKey * Load, specifically, an encryption public key from a string * * @param HiddenString $keyData + * * @return EncryptionPublicKey * * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importEncryptionPublicKey(HiddenString $keyData): EncryptionPublicKey { @@ -442,10 +498,12 @@ public static function importEncryptionPublicKey(HiddenString $keyData): Encrypt * Load, specifically, an encryption secret key from a string * * @param HiddenString $keyData + * * @return EncryptionSecretKey * * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importEncryptionSecretKey(HiddenString $keyData): EncryptionSecretKey { @@ -462,10 +520,12 @@ public static function importEncryptionSecretKey(HiddenString $keyData): Encrypt * Load, specifically, a signature public key from a string * * @param HiddenString $keyData + * * @return SignaturePublicKey * * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importSignaturePublicKey(HiddenString $keyData): SignaturePublicKey { @@ -482,10 +542,12 @@ public static function importSignaturePublicKey(HiddenString $keyData): Signatur * Load, specifically, a signature secret key from a string * * @param HiddenString $keyData + * * @return SignatureSecretKey * * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importSignatureSecretKey(HiddenString $keyData): SignatureSecretKey { @@ -502,10 +564,12 @@ public static function importSignatureSecretKey(HiddenString $keyData): Signatur * Load an asymmetric encryption key pair from a string * * @param HiddenString $keyData + * * @return EncryptionKeyPair * * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importEncryptionKeyPair(HiddenString $keyData): EncryptionKeyPair { @@ -526,8 +590,10 @@ public static function importEncryptionKeyPair(HiddenString $keyData): Encryptio * @param HiddenString $keyData * @return SignatureKeyPair * + * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importSignatureKeyPair(HiddenString $keyData): SignatureKeyPair { @@ -546,16 +612,18 @@ public static function importSignatureKeyPair(HiddenString $keyData): SignatureK * Load a symmetric authentication key from a file * * @param string $filePath + * * @return AuthenticationKey * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadAuthenticationKey(string $filePath): AuthenticationKey { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -569,16 +637,18 @@ public static function loadAuthenticationKey(string $filePath): AuthenticationKe * Load a symmetric encryption key from a file * * @param string $filePath + * * @return EncryptionKey * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadEncryptionKey(string $filePath): EncryptionKey { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -592,16 +662,18 @@ public static function loadEncryptionKey(string $filePath): EncryptionKey * Load, specifically, an encryption public key from a file * * @param string $filePath + * * @return EncryptionPublicKey * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadEncryptionPublicKey(string $filePath): EncryptionPublicKey { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -615,16 +687,18 @@ public static function loadEncryptionPublicKey(string $filePath): EncryptionPubl * Load, specifically, an encryption public key from a file * * @param string $filePath + * * @return EncryptionSecretKey * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadEncryptionSecretKey(string $filePath): EncryptionSecretKey { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -638,16 +712,18 @@ public static function loadEncryptionSecretKey(string $filePath): EncryptionSecr * Load, specifically, a signature public key from a file * * @param string $filePath + * * @return SignaturePublicKey * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadSignaturePublicKey(string $filePath): SignaturePublicKey { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -661,16 +737,18 @@ public static function loadSignaturePublicKey(string $filePath): SignaturePublic * Load, specifically, a signature secret key from a file * * @param string $filePath + * * @return SignatureSecretKey * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadSignatureSecretKey(string $filePath): SignatureSecretKey { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -684,16 +762,18 @@ public static function loadSignatureSecretKey(string $filePath): SignatureSecret * Load an asymmetric encryption key pair from a file * * @param string $filePath + * * @return EncryptionKeyPair * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadEncryptionKeyPair(string $filePath): EncryptionKeyPair { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -709,16 +789,18 @@ public static function loadEncryptionKeyPair(string $filePath): EncryptionKeyPai * Load an asymmetric signature key pair from a file * * @param string $filePath + * * @return SignatureKeyPair * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadSignatureKeyPair(string $filePath): SignatureKeyPair { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -733,32 +815,32 @@ public static function loadSignatureKeyPair(string $filePath): SignatureKeyPair /** * Export a cryptography key to a string (with a checksum) * - * @param object $key + * @param Key|KeyPair $key + * * @return HiddenString * * @throws CannotPerformOperation * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ - public static function export($key): HiddenString + public static function export(Key|KeyPair $key): HiddenString { if ($key instanceof KeyPair) { return self::export( $key->getSecretKey() ); - } elseif ($key instanceof Key) { - return new HiddenString( - Hex::encode( - Halite::HALITE_VERSION_KEYS . $key->getRawKeyMaterial() . - \sodium_crypto_generichash( - Halite::HALITE_VERSION_KEYS . $key->getRawKeyMaterial(), - '', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX - ) - ) - ); } - throw new \TypeError('Expected a Key.'); + return new HiddenString( + Hex::encode( + Halite::HALITE_VERSION_KEYS . $key->getRawKeyMaterial() . + sodium_crypto_generichash( + Halite::HALITE_VERSION_KEYS . $key->getRawKeyMaterial(), + '', + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + ) + ) + ); } /** @@ -766,10 +848,13 @@ public static function export($key): HiddenString * * @param Key|KeyPair $key * @param string $filename + * * @return bool - * @throws \TypeError + * + * @throws SodiumException + * @throws TypeError */ - public static function save($key, string $filename = ''): bool + public static function save(Key|KeyPair $key, string $filename = ''): bool { if ($key instanceof KeyPair) { return self::saveKeyFile( @@ -784,14 +869,17 @@ public static function save($key, string $filename = ''): bool * Read a key from a file, verify its checksum * * @param string $filePath + * * @return HiddenString + * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ protected static function loadKeyFile(string $filePath): HiddenString { - $fileData = \file_get_contents($filePath); + $fileData = file_get_contents($filePath); if ($fileData === false) { // @codeCoverageIgnoreStart throw new CannotPerformOperation( @@ -800,7 +888,7 @@ protected static function loadKeyFile(string $filePath): HiddenString // @codeCoverageIgnoreEnd } $data = Hex::decode($fileData); - \sodium_memzero($fileData); + Util::memzero($fileData); return new HiddenString( self::getKeyDataFromString($data) ); @@ -811,9 +899,12 @@ protected static function loadKeyFile(string $filePath): HiddenString * checksum) * * @param string $data + * * @return string + * * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function getKeyDataFromString(string $data): string { @@ -821,29 +912,29 @@ public static function getKeyDataFromString(string $data): string $keyData = Binary::safeSubstr( $data, Halite::VERSION_TAG_LEN, - -\SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + -SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ); $checksum = Binary::safeSubstr( $data, - -\SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + -SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ); - $calc = \sodium_crypto_generichash( + $calc = sodium_crypto_generichash( $versionTag . $keyData, '', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ); - if (!\hash_equals($calc, $checksum)) { + if (!hash_equals($calc, $checksum)) { // @codeCoverageIgnoreStart throw new InvalidKey( 'Checksum validation fail' ); // @codeCoverageIgnoreEnd } - \sodium_memzero($data); - \sodium_memzero($versionTag); - \sodium_memzero($calc); - \sodium_memzero($checksum); + Util::memzero($data); + Util::memzero($versionTag); + Util::memzero($calc); + Util::memzero($checksum); return $keyData; } @@ -854,23 +945,24 @@ public static function getKeyDataFromString(string $data): string * @param string $keyData * @return bool * - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ protected static function saveKeyFile( string $filePath, string $keyData ): bool { - $saved = \file_put_contents( + $saved = file_put_contents( $filePath, Hex::encode( Halite::HALITE_VERSION_KEYS . $keyData . - \sodium_crypto_generichash( + sodium_crypto_generichash( Halite::HALITE_VERSION_KEYS . $keyData, '', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ) ) ); - return $saved !== false; + return is_int($saved ); } } diff --git a/src/KeyPair.php b/src/KeyPair.php index 1cd462a9..4fab3308 100644 --- a/src/KeyPair.php +++ b/src/KeyPair.php @@ -15,25 +15,18 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class KeyPair { - /** - * @var SecretKey - */ - protected $secretKey; - - /** - * @var PublicKey - */ - protected $publicKey; + protected SecretKey $secretKey; + protected PublicKey $publicKey; /** * Hide this from var_dump(), etc. diff --git a/src/Password.php b/src/Password.php index 6443f930..0989f0a0 100644 --- a/src/Password.php +++ b/src/Password.php @@ -11,6 +11,7 @@ CannotPerformOperation, InvalidDigestLength, InvalidMessage, + InvalidSignature, InvalidType }; use ParagonIE\Halite\Symmetric\{ @@ -19,6 +20,13 @@ EncryptionKey }; use ParagonIE\HiddenString\HiddenString; +use SodiumException; +use TypeError; +use const SODIUM_CRYPTO_PWHASH_STRPREFIX; +use function + hash_equals, + sodium_crypto_pwhash_str, + sodium_crypto_pwhash_str_verify; /** * Class Password @@ -28,13 +36,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class Password { @@ -45,23 +53,28 @@ final class Password * @param EncryptionKey $secretKey The master key for all passwords * @param string $level The security level for this password * @param string $additionalData Additional authenticated data + * * @return string An encrypted hash to store * - * @throws InvalidDigestLength * @throws CannotPerformOperation + * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function hash( + #[\SensitiveParameter] HiddenString $password, + #[\SensitiveParameter] EncryptionKey $secretKey, string $level = KeyFactory::INTERACTIVE, + #[\SensitiveParameter] string $additionalData = '' ): string { $kdfLimits = KeyFactory::getSecurityLevels($level); // First, let's calculate the hash - $hashed = \sodium_crypto_pwhash_str( + $hashed = sodium_crypto_pwhash_str( $password->getString(), $kdfLimits[0], $kdfLimits[1] @@ -82,63 +95,65 @@ public static function hash( * @param EncryptionKey $secretKey The master key for all passwords * @param string $level The security level for this password * @param string $additionalData Additional authenticated data (if used to encrypt, mandatory) + * * @return bool Do we need to regenerate the hash or * ciphertext? * - * @throws Alerts\InvalidSignature * @throws CannotPerformOperation * @throws InvalidDigestLength * @throws InvalidMessage + * @throws InvalidSignature * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function needsRehash( + #[\SensitiveParameter] string $stored, + #[\SensitiveParameter] EncryptionKey $secretKey, string $level = KeyFactory::INTERACTIVE, + #[\SensitiveParameter] string $additionalData = '' ): bool { $config = self::getConfig($stored); if (Binary::safeStrlen($stored) < ((int) $config->SHORTEST_CIPHERTEXT_LENGTH * 4 / 3)) { throw new InvalidMessage('Encrypted password hash is too short.'); } + $encoding = $config->ENCODING; // First let's decrypt the hash $hash_str = Crypto::decryptWithAd( $stored, $secretKey, $additionalData, - $config->ENCODING + $encoding )->getString(); - // Upon successful decryption, verify that we're using Argon2i - if (!\hash_equals( + // Upon successful decryption, verify that we're using Argon2id + if (!hash_equals( Binary::safeSubstr($hash_str, 0, 10), - \SODIUM_CRYPTO_PWHASH_STRPREFIX + SODIUM_CRYPTO_PWHASH_STRPREFIX )) { return true; } // Parse the cost parameters: - switch ($level) { - case KeyFactory::INTERACTIVE: - return !\hash_equals( - '$argon2id$v=19$m=65536,t=2,p=1$', - Binary::safeSubstr($hash_str, 0, 31) - ); - case KeyFactory::MODERATE: - return !\hash_equals( - '$argon2id$v=19$m=262144,t=3,p=1$', - Binary::safeSubstr($hash_str, 0, 32) - ); - case KeyFactory::SENSITIVE: - return !\hash_equals( - '$argon2id$v=19$m=1048576,t=4,p=1$', - Binary::safeSubstr($hash_str, 0, 33) - ); - default: - return true; - } + return match ($level) { + KeyFactory::INTERACTIVE => !hash_equals( + '$argon2id$v=19$m=65536,t=2,p=1$', + Binary::safeSubstr($hash_str, 0, 31) + ), + KeyFactory::MODERATE => !hash_equals( + '$argon2id$v=19$m=262144,t=3,p=1$', + Binary::safeSubstr($hash_str, 0, 32) + ), + KeyFactory::SENSITIVE => !hash_equals( + '$argon2id$v=19$m=1048576,t=4,p=1$', + Binary::safeSubstr($hash_str, 0, 33) + ), + default => true, + }; } /** @@ -158,18 +173,16 @@ protected static function getConfig(string $stored): SymmetricConfig 'Encrypted password hash is way too short.' ); } + $prefix = Binary::safeSubstr($stored, 0, 5); if ( - \hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX) + hash_equals($prefix, Halite::VERSION_PREFIX) || - \hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_OLD_PREFIX) + hash_equals($prefix, Halite::VERSION_OLD_PREFIX) ) { - /** @var string $decoded */ $decoded = Base64UrlSafe::decode($stored); - return SymmetricConfig::getConfig( - $decoded, - 'encrypt' - ); + return SymmetricConfig::getConfig($decoded, 'encrypt'); } + // @codeCoverageIgnoreStart $v = Hex::decode(Binary::safeSubstr($stored, 0, 8)); return SymmetricConfig::getConfig($v, 'encrypt'); @@ -183,6 +196,7 @@ protected static function getConfig(string $stored): SymmetricConfig * @param string $stored The encrypted password hash * @param EncryptionKey $secretKey The master key for all passwords * @param string $additionalData Additional authenticated data (needed to decrypt) + * * @return bool Is this password valid? * * @throws Alerts\InvalidSignature @@ -190,12 +204,17 @@ protected static function getConfig(string $stored): SymmetricConfig * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function verify( + #[\SensitiveParameter] HiddenString $password, + #[\SensitiveParameter] string $stored, + #[\SensitiveParameter] EncryptionKey $secretKey, + #[\SensitiveParameter] string $additionalData = '' ): bool { $config = self::getConfig($stored); @@ -205,10 +224,12 @@ public static function verify( 'Encrypted password hash is too short.' ); } + $encoding = $config->ENCODING; + // First let's decrypt the hash - $hash_str = Crypto::decryptWithAd($stored, $secretKey, $additionalData, $config->ENCODING); + $hash_str = Crypto::decryptWithAd($stored, $secretKey, $additionalData, $encoding); // Upon successful decryption, verify the password is correct - return \sodium_crypto_pwhash_str_verify( + return sodium_crypto_pwhash_str_verify( $hash_str->getString(), $password->getString() ); diff --git a/src/SignatureKeyPair.php b/src/SignatureKeyPair.php index d866b159..b51d5ae7 100644 --- a/src/SignatureKeyPair.php +++ b/src/SignatureKeyPair.php @@ -2,12 +2,21 @@ declare(strict_types=1); namespace ParagonIE\Halite; -use ParagonIE\Halite\Alerts\InvalidKey; +use InvalidArgumentException; +use ParagonIE\Halite\Alerts\{ + CannotPerformOperation, + InvalidKey +}; use ParagonIE\Halite\Asymmetric\{ + PublicKey, + SecretKey, SignaturePublicKey, SignatureSecretKey }; use ParagonIE\HiddenString\HiddenString; +use SodiumException; +use TypeError; +use function count; /** * Class SignatureKeyPair @@ -17,37 +26,40 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class SignatureKeyPair extends KeyPair { /** * @var SignatureSecretKey */ - protected $secretKey; + protected SecretKey $secretKey; /** * @var SignaturePublicKey */ - protected $publicKey; + protected PublicKey $publicKey; /** * Pass it a secret key, it will automatically generate a public key * - * @param array $keys + * @param Key ...$keys * + * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws InvalidArgumentException + * @throws SodiumException + * @throws TypeError */ public function __construct(Key ...$keys) { - switch (\count($keys)) { + switch (count($keys)) { /** * If we received two keys, it must be an asymmetric secret key and * an asymmetric public key, in either order. @@ -115,16 +127,18 @@ public function __construct(Key ...$keys) ); break; default: - throw new \InvalidArgumentException( - 'Halite\\EncryptionKeyPair expects 1 or 2 keys' + throw new InvalidArgumentException( + 'EncryptionKeyPair expects 1 or 2 keys' ); } } /** * @return EncryptionKeyPair + * * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public function getEncryptionKeyPair(): EncryptionKeyPair { @@ -141,10 +155,12 @@ public function getEncryptionKeyPair(): EncryptionKeyPair * @return void * * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException */ - protected function setupKeyPair(SignatureSecretKey $secret): void - { + protected function setupKeyPair( + #[\SensitiveParameter] + SignatureSecretKey $secret + ): void { $this->secretKey = $secret; $this->publicKey = $this->secretKey->derivePublicKey(); } diff --git a/src/Stream/MutableFile.php b/src/Stream/MutableFile.php index d4e9de6d..60fbd542 100644 --- a/src/Stream/MutableFile.php +++ b/src/Stream/MutableFile.php @@ -9,6 +9,26 @@ FileAccessDenied, InvalidType }; +use TypeError; +use function + clearstatcache, + file_exists, + fclose, + fopen, + fread, + fseek, + fstat, + ftell, + fwrite, + in_array, + is_int, + is_readable, + is_resource, + is_string, + is_writable, + min, + stream_get_meta_data, + touch; /** * Class MutableFile @@ -18,23 +38,23 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Stream * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class MutableFile implements StreamInterface { - const ALLOWED_MODES = ['r+b', 'w+b', 'cb', 'c+b']; + const ALLOWED_MODES = ['r+b', 'w+b', 'cb', 'c+b', 'wb']; const CHUNK = 8192; // PHP's fread() buffer is set to 8192 by default /** * @var bool */ - private $closeAfter = false; + private bool $closeAfter = false; /** * @var resource @@ -44,7 +64,7 @@ class MutableFile implements StreamInterface /** * @var int */ - private $pos; + private int $pos; /** * @var array @@ -54,34 +74,35 @@ class MutableFile implements StreamInterface /** * MutableFile constructor. * @param string|resource $file + * * @throws InvalidType * @throws FileAccessDenied * @psalm-suppress RedundantConditionGivenDocblockType */ public function __construct($file) { - if (\is_string($file)) { - if (!\file_exists($file)) { - if (!\is_writable(\dirname($file))) { + if (is_string($file)) { + if (!file_exists($file)) { + if (!is_writable(dirname($file))) { throw new FileAccessDenied( 'Could not write to directory that contains file' ); } - \touch($file); // Make the file exist + touch($file); // Make the file exist } - if (!\is_readable($file)) { + if (!is_readable($file)) { throw new FileAccessDenied( 'Could not open file for reading' ); } - if (!\is_writable($file)) { + if (!is_writable($file)) { throw new FileAccessDenied( 'Could not open file for writing' ); } - $fp = \fopen($file, 'w+b'); + $fp = fopen($file, 'w+b'); // @codeCoverageIgnoreStart - if (!\is_resource($fp)) { + if (!is_resource($fp)) { throw new FileAccessDenied( 'Could not open file for reading' ); @@ -90,18 +111,18 @@ public function __construct($file) $this->fp = $fp; $this->closeAfter = true; $this->pos = 0; - $this->stat = \fstat($this->fp); - } elseif (\is_resource($file)) { + $this->stat = fstat($this->fp); + } elseif (is_resource($file)) { /** @var array $metadata */ - $metadata = \stream_get_meta_data($file); - if (!\in_array($metadata['mode'], self::ALLOWED_MODES, true)) { + $metadata = stream_get_meta_data($file); + if (!in_array($metadata['mode'], self::ALLOWED_MODES, true)) { throw new FileAccessDenied( 'Resource is in ' . $metadata['mode'] . ' mode, which is not allowed.' ); } $this->fp = $file; - $this->pos = \ftell($this->fp); - $this->stat = \fstat($this->fp); + $this->pos = ftell($this->fp); + $this->stat = fstat($this->fp); } else { throw new InvalidType( 'Argument 1: Expected a filename or resource' @@ -111,13 +132,17 @@ public function __construct($file) /** * Close the file handle. + * + * @return void + * + * @psalm-suppress InvalidPropertyAssignmentValue */ public function close(): void { if ($this->closeAfter) { $this->closeAfter = false; - \fclose($this->fp); - \clearstatcache(); + fclose($this->fp); + clearstatcache(); } } @@ -136,7 +161,7 @@ public function __destruct() */ public function getPos(): int { - return \ftell($this->fp); + return ftell($this->fp); } /** @@ -146,7 +171,7 @@ public function getPos(): int */ public function getSize(): int { - $stat = \fstat($this->fp); + $stat = fstat($this->fp); return (int) $stat['size']; } @@ -157,7 +182,7 @@ public function getSize(): int */ public function getStreamMetadata(): array { - return \stream_get_meta_data($this->fp); + return stream_get_meta_data($this->fp); } /** @@ -165,7 +190,9 @@ public function getStreamMetadata(): array * * @param int $num * @param bool $skipTests + * * @return string + * * @throws CannotPerformOperation * @throws FileAccessDenied */ @@ -189,11 +216,9 @@ public function readBytes(int $num, bool $skipTests = false): string break; // @codeCoverageIgnoreEnd } - /** @var int $bufSize */ - $bufSize = \min($remaining, self::CHUNK); - /** @var string|bool $read */ - $read = \fread($this->fp, $bufSize); - if (!\is_string($read)) { + $bufSize = min($remaining, self::CHUNK); + $read = fread($this->fp, $bufSize); + if (!is_string($read)) { // @codeCoverageIgnoreStart throw new FileAccessDenied( 'Could not read from the file' @@ -216,9 +241,9 @@ public function readBytes(int $num, bool $skipTests = false): string public function remainingBytes(): int { /** @var array $stat */ - $stat = \fstat($this->fp); + $stat = fstat($this->fp); /** @var int $pos */ - $pos = \ftell($this->fp); + $pos = ftell($this->fp); return (int) ( PHP_INT_MAX & ( (int) $stat['size'] - $pos @@ -229,15 +254,17 @@ public function remainingBytes(): int /** * Set the current cursor position to the desired location * - * @param int $i + * @param int $position + * * @return bool + * * @throws CannotPerformOperation * @codeCoverageIgnore */ - public function reset(int $i = 0): bool + public function reset(int $position = 0): bool { - $this->pos = $i; - if (\fseek($this->fp, $i, SEEK_SET) === 0) { + $this->pos = $position; + if (fseek($this->fp, $position, SEEK_SET) === 0) { return true; } throw new CannotPerformOperation( @@ -249,17 +276,18 @@ public function reset(int $i = 0): bool * Write to a stream; prevent partial writes * * @param string $buf - * @param int|null $num (number of bytes) + * @param ?int $num (number of bytes) + * * @return int * * @throws CannotPerformOperation * @throws FileAccessDenied - * @throws \TypeError + * @throws TypeError */ - public function writeBytes(string $buf, int $num = null): int + public function writeBytes(string $buf, ?int $num = null): int { $bufSize = Binary::safeStrlen($buf); - if (!\is_int($num) || $num > $bufSize) { + if (!is_int($num) || $num > $bufSize) { $num = $bufSize; } // @codeCoverageIgnoreStart @@ -274,7 +302,7 @@ public function writeBytes(string $buf, int $num = null): int break; } // @codeCoverageIgnoreEnd - $written = \fwrite($this->fp, $buf, $remaining); + $written = fwrite($this->fp, $buf, $remaining); if ($written === false) { // @codeCoverageIgnoreStart throw new FileAccessDenied( @@ -284,7 +312,7 @@ public function writeBytes(string $buf, int $num = null): int } $buf = Binary::safeSubstr($buf, $written, null); $this->pos += $written; - $this->stat = \fstat($this->fp); + $this->stat = fstat($this->fp); $remaining -= $written; } while ($remaining > 0); return $num; diff --git a/src/Stream/ReadOnlyFile.php b/src/Stream/ReadOnlyFile.php index 7dd0e143..c30f1955 100644 --- a/src/Stream/ReadOnlyFile.php +++ b/src/Stream/ReadOnlyFile.php @@ -12,6 +12,27 @@ InvalidType, }; use ParagonIE\Halite\Key; +use SodiumException; +use TypeError; +use const + SEEK_SET, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX; +use function + clearstatcache, + fclose, + fopen, + fread, + fseek, + fstat, + ftell, + in_array, + is_readable, + is_resource, + is_string, + sodium_crypto_generichash_init, + sodium_crypto_generichash_update, + sodium_crypto_generichash_final, + stream_get_meta_data; /** * Class ReadOnlyFile @@ -19,48 +40,30 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Stream * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class ReadOnlyFile implements StreamInterface { const ALLOWED_MODES = ['rb']; const CHUNK = 8192; // PHP's fread() buffer is set to 8192 by default - /** - * @var bool - */ - private $closeAfter = false; + private bool $closeAfter = false; /** * @var resource */ private $fp; - /** - * @var string - */ - private $hash; - - /** - * @var int - */ - private $pos = 0; - - /** - * @var null|string - */ - private $hashKey = null; - - /** - * @var array - */ - private $stat = []; + private string $hash = ''; + private int $pos = 0; + private ?string $hashKey = null; + private array $stat = []; /** * ReadOnlyFile constructor. @@ -71,21 +74,21 @@ class ReadOnlyFile implements StreamInterface * @throws FileAccessDenied * @throws FileError * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @psalm-suppress RedundantConditionGivenDocblockType */ - public function __construct($file, Key $key = null) + public function __construct($file, ?Key $key = null) { - if (\is_string($file)) { - if (!\is_readable($file)) { + if (is_string($file)) { + if (!is_readable($file)) { throw new FileAccessDenied( 'Could not open file for reading' ); } - /** @var resource|bool $fp */ - $fp = \fopen($file, 'rb'); + $fp = fopen($file, 'rb'); // @codeCoverageIgnoreStart - if (!\is_resource($fp)) { + if (!is_resource($fp)) { throw new FileAccessDenied( 'Could not open file for reading' ); @@ -95,18 +98,18 @@ public function __construct($file, Key $key = null) $this->closeAfter = true; $this->pos = 0; - $this->stat = \fstat($this->fp); - } elseif (\is_resource($file)) { + $this->stat = $this->fstat(); + } elseif (is_resource($file)) { /** @var array $metadata */ - $metadata = \stream_get_meta_data($file); - if (!\in_array($metadata['mode'], (array) static::ALLOWED_MODES, true)) { + $metadata = stream_get_meta_data($file); + if (!in_array($metadata['mode'], (array) static::ALLOWED_MODES, true)) { throw new FileAccessDenied( 'Resource is in ' . $metadata['mode'] . ' mode, which is not allowed.' ); } $this->fp = $file; - $this->pos = \ftell($this->fp); - $this->stat = \fstat($this->fp); + $this->pos = ftell($this->fp); + $this->stat = $this->fstat(); } else { throw new InvalidType( 'Argument 1: Expected a filename or resource' @@ -131,13 +134,15 @@ public function __destruct() /** * Close the file handle. * @return void + * + * @psalm-suppress InvalidPropertyAssignmentValue */ public function close(): void { if ($this->closeAfter) { $this->closeAfter = false; - \fclose($this->fp); - \clearstatcache(); + fclose($this->fp); + clearstatcache(); } } @@ -145,39 +150,46 @@ public function close(): void * Calculate a BLAKE2b hash of a file * * @return string - * @throws + * + * @throws SodiumException + * @throws FileModified + * @throws FileError */ public function getHash(): string { + if ($this->hash) { + $this->toctouTest(); + return $this->hash; + } $init = $this->pos; - \fseek($this->fp, 0, SEEK_SET); + fseek($this->fp, 0, SEEK_SET); // Create a hash context: - $h = \sodium_crypto_generichash_init( + $h = sodium_crypto_generichash_init( $this->hashKey, - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ); for ($i = 0; $i < $this->stat['size']; $i += self::CHUNK) { if (($i + self::CHUNK) > $this->stat['size']) { - $c = \fread($this->fp, ((int) $this->stat['size'] - $i)); + $c = fread($this->fp, ((int) $this->stat['size'] - $i)); } else { - $c = \fread($this->fp, self::CHUNK); + $c = fread($this->fp, self::CHUNK); } - if (!\is_string($c)) { + if (!is_string($c)) { // @codeCoverageIgnoreStart throw new FileError('Could not read file'); // @codeCoverageIgnoreEnd } - \sodium_crypto_generichash_update($h, $c); + sodium_crypto_generichash_update($h, $c); } // Reset the file pointer's internal cursor to where it was: - \fseek($this->fp, $init, SEEK_SET); - return \sodium_crypto_generichash_final($h); + fseek($this->fp, $init, SEEK_SET); + return sodium_crypto_generichash_final($h); } /** * Where are we in the buffer? - * + * * @return int */ public function getPos(): int @@ -187,7 +199,7 @@ public function getPos(): int /** * How big is this buffer? - * + * * @return int */ public function getSize(): int @@ -202,20 +214,22 @@ public function getSize(): int */ public function getStreamMetadata(): array { - return \stream_get_meta_data($this->fp); + return stream_get_meta_data($this->fp); } - + /** * Read from a stream; prevent partial reads (also uses run-time testing to * prevent partial reads -- you can turn this off if you need performance * and aren't concerned about race condition attacks, but this isn't a * decision to make lightly!) - * + * * @param int $num - * @param bool $skipTests Only set this to TRUE if you're absolutely sure - * that you don't want to defend against TOCTOU / - * race condition attacks on the filesystem! + * @param bool $skipTests Only set this to TRUE if you're absolutely sure + * that you don't want to defend against TOCTOU / + * race condition attacks on the filesystem! + * * @return string + * * @throws CannotPerformOperation * @throws FileAccessDenied * @throws FileModified @@ -243,9 +257,8 @@ public function readBytes(int $num, bool $skipTests = false): string break; } // @codeCoverageIgnoreEnd - /** @var string|bool $read */ - $read = \fread($this->fp, $remaining); - if (!\is_string($read)) { + $read = fread($this->fp, $remaining); + if (!is_string($read)) { // @codeCoverageIgnoreStart throw new FileAccessDenied( 'Could not read from the file' @@ -259,10 +272,10 @@ public function readBytes(int $num, bool $skipTests = false): string } while ($remaining > 0); return $buf; } - + /** * Get number of bytes remaining - * + * * @return int */ public function remainingBytes(): int @@ -278,13 +291,15 @@ public function remainingBytes(): int * Set the current cursor position to the desired location * * @param int $position + * * @return bool + * * @throws CannotPerformOperation */ public function reset(int $position = 0): bool { $this->pos = $position; - if (\fseek($this->fp, $position, SEEK_SET) === 0) { + if (fseek($this->fp, $position, SEEK_SET) === 0) { return true; } // @codeCoverageIgnoreStart @@ -299,35 +314,38 @@ public function reset(int $position = 0): bool * verifying that the hash matches and the current cursor position/file * size matches their values when the file was first opened. * - * @throws FileModified * @return void + * + * @throws FileModified */ - public function toctouTest() + public function toctouTest(): void { - if (\ftell($this->fp) !== $this->pos) { + if (ftell($this->fp) !== $this->pos) { // @codeCoverageIgnoreStart throw new FileModified( 'Read-only file has been modified since it was opened for reading' ); // @codeCoverageIgnoreEnd } - $stat = \fstat($this->fp); + $stat = $this->fstat(); if ($stat['size'] !== $this->stat['size']) { throw new FileModified( 'Read-only file has been modified since it was opened for reading' ); } } - + /** * This is a meaningless operation for a Read-Only File! - * + * * @param string $buf - * @param int $num (number of bytes) + * @param ?int $num (number of bytes) + * * @return int + * * @throws FileAccessDenied */ - public function writeBytes(string $buf, int $num = null): int + public function writeBytes(string $buf, ?int $num = null): int { unset($buf); unset($num); @@ -335,4 +353,26 @@ public function writeBytes(string $buf, int $num = null): int 'This is a read-only file handle.' ); } + + /** + * Wraps fstat to allow calculation of file-size on stream wrappers. + * + * @return array + */ + private function fstat() : array { + $stat = fstat($this->fp); + if ($stat) { + return $stat; + } + // The resource is remote or a stream wrapper like php://input + $stat = [ + 'size' => 0, + ]; + fseek($this->fp, 0); + while (!feof($this->fp)) { + $stat['size'] += Binary::safeStrlen(fread($this->fp, self::CHUNK)); + } + fseek($this->fp, $this->pos); + return $stat; + } } diff --git a/src/Structure/MerkleTree.php b/src/Structure/MerkleTree.php index dcd94c0a..ec535d1a 100644 --- a/src/Structure/MerkleTree.php +++ b/src/Structure/MerkleTree.php @@ -8,6 +8,15 @@ InvalidDigestLength }; use ParagonIE\Halite\Util; +use SodiumException; +use TypeError; +use const SODIUM_CRYPTO_GENERICHASH_BYTES, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + SODIUM_CRYPTO_GENERICHASH_BYTES_MIN; +use function + array_shift, + count, + sprintf; /** * Class MerkleTree @@ -18,48 +27,33 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Structure * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class MerkleTree { const MERKLE_LEAF = "\x01"; const MERKLE_BRANCH = "\x00"; - /** - * @var bool - */ - protected $rootCalculated = false; - - /** - * @var string - */ - protected $root = ''; + protected bool $rootCalculated = false; + protected string $root = ''; /** * @var Node[] */ - protected $nodes = []; - - /** - * @var string - */ - protected $personalization = ''; - - /** - * @var int - */ - protected $outputSize = \SODIUM_CRYPTO_GENERICHASH_BYTES; + protected array $nodes = []; + protected string $personalization = ''; + protected int $outputSize = SODIUM_CRYPTO_GENERICHASH_BYTES; /** * Instantiate a Merkle tree * - * @param array $nodes + * @param Node ...$nodes */ public function __construct(Node ...$nodes) { @@ -72,11 +66,10 @@ public function __construct(Node ...$nodes) * @param bool $raw - Do we want a raw string instead of a hex string? * * @return string - * @throws CannotPerformOperation * - * @return string * @throws CannotPerformOperation - * @throws \TypeError + * @throws TypeError + * @throws SodiumException */ public function getRoot(bool $raw = false): string { @@ -91,8 +84,10 @@ public function getRoot(bool $raw = false): string /** * Merkle Trees are immutable. Return a replacement with extra nodes. * - * @param array $nodes + * @param Node ...$nodes + * * @return MerkleTree + * * @throws InvalidDigestLength */ public function getExpandedTree(Node ...$nodes): MerkleTree @@ -110,24 +105,26 @@ public function getExpandedTree(Node ...$nodes): MerkleTree * Set the hash output size. * * @param int $size + * * @return self + * * @throws InvalidDigestLength */ public function setHashSize(int $size): self { - if ($size < \SODIUM_CRYPTO_GENERICHASH_BYTES_MIN) { + if ($size < SODIUM_CRYPTO_GENERICHASH_BYTES_MIN) { throw new InvalidDigestLength( - \sprintf( + sprintf( 'Merkle roots must be at least %d long.', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MIN + SODIUM_CRYPTO_GENERICHASH_BYTES_MIN ) ); } - if ($size > \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX) { + if ($size > SODIUM_CRYPTO_GENERICHASH_BYTES_MAX) { throw new InvalidDigestLength( - \sprintf( + sprintf( 'Merkle roots must be at most %d long.', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ) ); } @@ -142,6 +139,7 @@ public function setHashSize(int $size): self * Sets the personalization string for the Merkle root calculation * * @param string $str + * * @return self */ public function setPersonalizationString(string $str = ''): self @@ -157,8 +155,10 @@ public function setPersonalizationString(string $str = ''): self * Explicitly recalculate the Merkle root * * @return self + * * @throws CannotPerformOperation - * @throws \TypeError + * @throws TypeError + * @throws SodiumException * @codeCoverageIgnore */ public function triggerRootCalculation(): self @@ -173,12 +173,14 @@ public function triggerRootCalculation(): self * to protect against second-preimage attacks * * @return string + * * @throws CannotPerformOperation - * @throws \TypeError + * @throws TypeError + * @throws SodiumException */ protected function calculateRoot(): string { - $size = \count($this->nodes); + $size = count($this->nodes); if ($size < 1) { return ''; } @@ -212,7 +214,6 @@ protected function calculateRoot(): string $tmp = []; $j = 0; for ($i = 0; $i < $order; $i += 2) { - /** @var string $prev */ $curr = (string) ($hash[$i] ?? ''); if (empty($hash[$i + 1])) { // @codeCoverageIgnoreStart @@ -225,10 +226,8 @@ protected function calculateRoot(): string ); // @codeCoverageIgnoreEnd } else { - /** @var string $curr */ - $curr = (string) ($hash[$i] ?? ''); - /** @var string $next */ - $next = (string) ($hash[$i + 1] ?? ''); + $curr = ($hash[$i] ?? ''); + $next = ($hash[$i + 1] ?? ''); $tmp[$j] = Util::raw_hash( self::MERKLE_BRANCH . $this->personalization . @@ -241,17 +240,16 @@ protected function calculateRoot(): string $hash = $tmp; $order >>= 1; } while ($order > 1); - // We should only have one value left:t + // We should only have one value left: $this->rootCalculated = true; - /** @var string $first */ - $first = \array_shift($hash); - return $first; + return (string) array_shift($hash); } /** * Let's go ahead and round up to the nearest multiple of 2 * * @param int $inputSize + * * @return int */ public static function getSizeRoundedUp(int $inputSize): int diff --git a/src/Structure/Node.php b/src/Structure/Node.php index 034044d6..0f048a7e 100644 --- a/src/Structure/Node.php +++ b/src/Structure/Node.php @@ -4,6 +4,9 @@ use ParagonIE\Halite\Alerts\CannotPerformOperation; use ParagonIE\Halite\Util; +use SodiumException; +use TypeError; +use const SODIUM_CRYPTO_GENERICHASH_BYTES; /** * Class Node @@ -11,23 +14,21 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Structure * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class Node { - /** - * @var string - */ - private $data; + private string $data; /** * Node constructor. + * * @param string $data */ public function __construct(string $data) @@ -55,12 +56,14 @@ public function getData(): string * @param string $personalization * * @return string + * * @throws CannotPerformOperation - * @throws \TypeError + * @throws TypeError + * @throws SodiumException */ public function getHash( bool $raw = false, - int $outputSize = \SODIUM_CRYPTO_GENERICHASH_BYTES, + int $outputSize = SODIUM_CRYPTO_GENERICHASH_BYTES, string $personalization = '' ): string { if ($raw) { @@ -79,6 +82,7 @@ public function getHash( * Nodes are immutable, but you can create one with extra data. * * @param string $concat + * * @return Node */ public function getExpandedNode(string $concat): Node diff --git a/src/Structure/TrimmedMerkleTree.php b/src/Structure/TrimmedMerkleTree.php index 90a22882..2f213b5c 100644 --- a/src/Structure/TrimmedMerkleTree.php +++ b/src/Structure/TrimmedMerkleTree.php @@ -7,6 +7,9 @@ InvalidDigestLength }; use ParagonIE\Halite\Util; +use SodiumException; +use TypeError; +use function count; /** * Class TrimmedMerkleTree @@ -20,13 +23,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Structure * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class TrimmedMerkleTree extends MerkleTree { @@ -36,13 +39,15 @@ class TrimmedMerkleTree extends MerkleTree * to protect against second-preimage attacks * * @return string + * * @throws CannotPerformOperation - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @psalm-suppress EmptyArrayAccess Psalm is misreading array elements */ protected function calculateRoot(): string { - $size = \count($this->nodes); + $size = count($this->nodes); if ($size < 1) { return ''; } @@ -78,26 +83,25 @@ protected function calculateRoot(): string } ++$j; } - /** @var array $hash */ $hash = $tmp; $size >>= 1; } while ($size > 1); // We should only have one value left: $this->rootCalculated = true; - /** @var string $first */ - $first = \array_shift($hash); - return $first; + return (string) array_shift($hash); } /** * Merkle Trees are immutable. Return a replacement with extra nodes. * - * @param array $nodes + * @param Node ...$nodes + * * @return TrimmedMerkleTree + * * @throws InvalidDigestLength */ - public function getExpandedTree(Node ...$nodes): MerkleTree + public function getExpandedTree(Node ...$nodes): TrimmedMerkleTree { $thisTree = $this->nodes; foreach ($nodes as $node) { diff --git a/src/Symmetric/AuthenticationKey.php b/src/Symmetric/AuthenticationKey.php index ff990a16..1c065a0e 100644 --- a/src/Symmetric/AuthenticationKey.php +++ b/src/Symmetric/AuthenticationKey.php @@ -5,6 +5,9 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; +use TypeError; +use const SODIUM_CRYPTO_AUTH_KEYBYTES; +use function sprintf; /** * Class AuthenticationKey @@ -12,22 +15,28 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class AuthenticationKey extends SecretKey { /** * AuthenticationKey constructor. + * * @param HiddenString $keyMaterial - The actual key data * * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ - public function __construct(HiddenString $keyMaterial) - { - if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_AUTH_KEYBYTES) { + public function __construct( + #[\SensitiveParameter] + HiddenString $keyMaterial + ) { + if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_AUTH_KEYBYTES) { throw new InvalidKey( - 'Authentication key must be CRYPTO_AUTH_KEYBYTES bytes long' + sprintf( + 'Authentication key must be CRYPTO_AUTH_KEYBYTES (%d) bytes long', + SODIUM_CRYPTO_AUTH_KEYBYTES + ) ); } parent::__construct($keyMaterial); diff --git a/src/Symmetric/Config.php b/src/Symmetric/Config.php index 2952ca66..3a1f7bef 100644 --- a/src/Symmetric/Config.php +++ b/src/Symmetric/Config.php @@ -6,24 +6,27 @@ use ParagonIE\Halite\Alerts\InvalidMessage; use ParagonIE\Halite\{ Config as BaseConfig, - Halite + Halite, + Util }; +use const + SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + SODIUM_CRYPTO_STREAM_NONCEBYTES; /** * Class Config * - * Secure encrypted cookies - * * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Symmetric * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class Config extends BaseConfig { @@ -32,6 +35,7 @@ final class Config extends BaseConfig * * @param string $header * @param string $mode + * * @return self * * @throws InvalidMessage @@ -45,13 +49,13 @@ public static function getConfig( 'Invalid version tag' ); } - if (\ord($header[0]) !== 49 || \ord($header[1]) !== 66) { + if (Util::chrToInt($header[0]) !== 49 || Util::chrToInt($header[1]) !== 66) { throw new InvalidMessage( 'Invalid version tag' ); } - $major = \ord($header[2]); - $minor = \ord($header[3]); + $major = Util::chrToInt($header[2]); + $minor = Util::chrToInt($header[3]); if ($mode === 'encrypt') { return new Config( self::getConfigEncrypt($major, $minor) @@ -71,21 +75,44 @@ public static function getConfig( * * @param int $major * @param int $minor + * * @return array + * * @throws InvalidMessage */ public static function getConfigEncrypt(int $major, int $minor): array { - if ($major === 3 || $major === 4) { + if ($major === 5) { + switch ($minor) { + case 0: + return [ + 'ENCODING' => Halite::ENCODE_BASE64URLSAFE, + 'SHORTEST_CIPHERTEXT_LENGTH' => 124, + 'NONCE_BYTES' => SODIUM_CRYPTO_STREAM_NONCEBYTES, + 'HKDF_SALT_LEN' => 32, + 'ENC_ALGO' => 'XChaCha20', + 'USE_PAE' => true, + 'MAC_ALGO' => 'BLAKE2b', + 'MAC_SIZE' => SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + 'HKDF_USE_INFO' => true, + 'HKDF_SBOX' => 'Halite|EncryptionKey', + 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' + ]; + } + } + if ($major === 4 || $major === 3) { switch ($minor) { case 0: return [ 'ENCODING' => Halite::ENCODE_BASE64URLSAFE, 'SHORTEST_CIPHERTEXT_LENGTH' => 124, - 'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES, + 'NONCE_BYTES' => SODIUM_CRYPTO_STREAM_NONCEBYTES, 'HKDF_SALT_LEN' => 32, + 'ENC_ALGO' => 'XSalsa20', + 'USE_PAE' => false, 'MAC_ALGO' => 'BLAKE2b', - 'MAC_SIZE' => \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + 'MAC_SIZE' => SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + 'HKDF_USE_INFO' => false, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; @@ -101,19 +128,23 @@ public static function getConfigEncrypt(int $major, int $minor): array * * @param int $major * @param int $minor + * * @return array + * * @throws InvalidMessage */ public static function getConfigAuth(int $major, int $minor): array { - if ($major === 3 || $major === 4) { + if ($major === 4 || $major === 5) { switch ($minor) { case 0: return [ + 'USE_PAE' => $major >= 5, 'HKDF_SALT_LEN' => 32, 'MAC_ALGO' => 'BLAKE2b', - 'MAC_SIZE' => \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, - 'PUBLICKEY_BYTES' => \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + 'MAC_SIZE' => SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + 'PUBLICKEY_BYTES' => SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + 'HKDF_USE_INFO' => $major > 4, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; diff --git a/src/Symmetric/Crypto.php b/src/Symmetric/Crypto.php index 98dd0e18..b95c70b2 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -2,6 +2,7 @@ declare(strict_types=1); namespace ParagonIE\Halite\Symmetric; +use Error; use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\{ CannotPerformOperation, @@ -11,12 +12,29 @@ InvalidType }; use ParagonIE\Halite\{ - Config as BaseConfig, - Halite, + Config as BaseConfig, + Halite, Symmetric\Config as SymmetricConfig, - Util as CryptoUtil + Util }; use ParagonIE\HiddenString\HiddenString; +use RangeException; +use SodiumException; +use Throwable; +use TypeError; +use const + SODIUM_CRYPTO_AUTH_KEYBYTES, + SODIUM_CRYPTO_SECRETBOX_KEYBYTES, + SODIUM_CRYPTO_STREAM_NONCEBYTES; +use function + hash_equals, + is_callable, + is_null, + random_bytes, + sodium_crypto_generichash, + sodium_crypto_stream_xchacha20_xor, + sodium_crypto_stream_xor, + str_repeat; /** * Class Crypto @@ -26,25 +44,25 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Symmetric * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class Crypto { /** * Don't allow this to be instantiated. * - * @throws \Error + * @throws Error * @codeCoverageIgnore */ - final private function __construct() + private function __construct() { - throw new \Error('Do not instantiate'); + throw new Error('Do not instantiate'); } /** @@ -52,23 +70,24 @@ final private function __construct() * * @param string $message * @param AuthenticationKey $secretKey - * @param mixed $encoding + * @param string|bool $encoding + * * @return string * * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function authenticate( string $message, AuthenticationKey $secretKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): string { $config = SymmetricConfig::getConfig( Halite::HALITE_VERSION, 'auth' ); - /** @var string $mac */ $mac = self::calculateMAC( $message, $secretKey->getRawKeyMaterial(), @@ -78,7 +97,7 @@ public static function authenticate( if ($encoder) { return (string) $encoder($mac); } - return (string) $mac; + return $mac; } /** @@ -86,7 +105,8 @@ public static function authenticate( * * @param string $ciphertext * @param EncryptionKey $secretKey - * @param mixed $encoding + * @param string|bool $encoding + * * @return HiddenString * * @throws CannotPerformOperation @@ -94,14 +114,15 @@ public static function authenticate( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function decrypt( string $ciphertext, EncryptionKey $secretKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { - return static::decryptWithAd( + return self::decryptWithAD( $ciphertext, $secretKey, '', @@ -112,10 +133,19 @@ public static function decrypt( /** * Decrypt a message using the Halite encryption protocol * + * Verifies the MAC before decryption + * - Halite 5+ verifies the BLAKE2b-MAC before decrypting with XChaCha20 + * - Halite 4 and below verifies the BLAKE2b-MAC before decrypting with XSalsa20 + * + * You don't need to worry about chosen-ciphertext attacks. + * You don't need to worry about Invisible Salamanders. + * You don't need to worry about timing attacks on MAC validation. + * * @param string $ciphertext * @param EncryptionKey $secretKey * @param string $additionalData - * @param mixed $encoding + * @param string|bool $encoding + * * @return HiddenString * * @throws CannotPerformOperation @@ -123,42 +153,45 @@ public static function decrypt( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ - public static function decryptWithAd( + public static function decryptWithAD( string $ciphertext, EncryptionKey $secretKey, string $additionalData = '', - $encoding = Halite::ENCODE_BASE64URLSAFE + bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { $decoder = Halite::chooseEncoder($encoding, true); - if (\is_callable($decoder)) { + if (is_callable($decoder)) { // We were given encoded data: // @codeCoverageIgnoreStart try { /** @var string $ciphertext */ $ciphertext = $decoder($ciphertext); - } catch (\RangeException $ex) { + } catch (RangeException $ex) { throw new InvalidMessage( 'Invalid character encoding' ); } // @codeCoverageIgnoreEnd } - /** @var array $pieces */ - $pieces = self::unpackMessageForDecryption($ciphertext); - /** @var string $version */ - $version = $pieces[0]; - /** @var Config $config */ - $config = $pieces[1]; - /** @var string $salt */ - $salt = $pieces[2]; - /** @var string $nonce */ - $nonce = $pieces[3]; - /** @var string $encrypted */ - $encrypted = $pieces[4]; - /** @var string $auth */ - $auth = $pieces[5]; + /** + * @var string $version + * @var Config $config + * @var string $salt + * @var string $nonce + * @var string $encrypted + * @var string $auth + */ + [ + $version, + $config, + $salt, + $nonce, + $encrypted, + $auth + ] = self::unpackMessageForDecryption($ciphertext); /* Split our key into two keys: One for encryption, the other for authentication. By using separate keys, we can reasonably dismiss @@ -166,71 +199,76 @@ public static function decryptWithAd( This uses salted HKDF to split the keys, which is why we need the salt in the first place. */ - /** - * @var array $split - * @var string $encKey - * @var string $authKey - */ - $split = self::splitKeys($secretKey, (string) $salt, $config); + /** @var array $split */ + $split = Util::splitKeys($secretKey, $salt, $config); $encKey = $split[0]; $authKey = $split[1]; // Check the MAC first - if (!self::verifyMAC( + if ($config->USE_PAE) { + $verified = self::verifyMAC( + $auth, + Util::PAE($version, $salt, $nonce, $additionalData, $encrypted), + $authKey, + $config + ); + } else { + $verified = self::verifyMAC( // @codeCoverageIgnoreStart - (string) $auth, - (string) $version . - (string) $salt . - (string) $nonce . - (string) $additionalData . - (string) $encrypted, - // @codeCoverageIgnoreEnd - $authKey, - $config - )) { + $auth, + $version . + $salt . + $nonce . + $additionalData . + $encrypted, + // @codeCoverageIgnoreEnd + $authKey, + $config + ); + } + + if (!$verified) { throw new InvalidMessage( 'Invalid message authentication code' ); } - \sodium_memzero($salt); - \sodium_memzero($authKey); + Util::memzero($salt); + Util::memzero($authKey); // crypto_stream_xor() can be used to encrypt and decrypt - /** @var string $plaintext */ - $plaintext = \sodium_crypto_stream_xor( - (string) $encrypted, - (string) $nonce, - (string) $encKey - ); - \sodium_memzero($encrypted); - \sodium_memzero($nonce); - \sodium_memzero($encKey); + if ($config->ENC_ALGO === 'XChaCha20') { + $plaintext = sodium_crypto_stream_xchacha20_xor($encrypted, $nonce, $encKey); + } else { + $plaintext = sodium_crypto_stream_xor($encrypted, $nonce, $encKey); + } + Util::memzero($encrypted); + Util::memzero($nonce); + Util::memzero($encKey); return new HiddenString($plaintext); } /** * Encrypt a message using the Halite encryption protocol * - * (Encrypt then MAC -- xsalsa20 then keyed-Blake2b) - * You don't need to worry about chosen-ciphertext attacks. - * * @param HiddenString $plaintext * @param EncryptionKey $secretKey * @param string|bool $encoding + * * @return string * * @throws CannotPerformOperation * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function encrypt( HiddenString $plaintext, EncryptionKey $secretKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): string { - return static::encryptWithAd( + return self::encryptWithAD( $plaintext, $secretKey, '', @@ -239,32 +277,43 @@ public static function encrypt( } /** + * Encrypt a message using the Halite encryption protocol + * + * Encrypt then MAC. + * - Halite 5+ uses XChaCha20 then BLAKE2b-MAC + * - Halite 4 and below use XSalsa20 then BLAKE2b-MAC + * + * You don't need to worry about chosen-ciphertext attacks. + * You don't need to worry about Invisible Salamanders. + * * @param HiddenString $plaintext * @param EncryptionKey $secretKey * @param string $additionalData - * @param string|bool $encoding + * @param bool|string $encoding + * * @return string * * @throws CannotPerformOperation * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ - public static function encryptWithAd( + public static function encryptWithAD( HiddenString $plaintext, EncryptionKey $secretKey, string $additionalData = '', - $encoding = Halite::ENCODE_BASE64URLSAFE + bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): string { $config = SymmetricConfig::getConfig(Halite::HALITE_VERSION, 'encrypt'); // Generate a nonce and HKDF salt: // @codeCoverageIgnoreStart try { - $nonce = \random_bytes(\SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); - $salt = \random_bytes((int) $config->HKDF_SALT_LEN); - } catch (\Throwable $ex) { + $nonce = random_bytes(\SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $salt = random_bytes((int) $config->HKDF_SALT_LEN); + } catch (Throwable $ex) { throw new CannotPerformOperation($ex->getMessage()); } // @codeCoverageIgnoreEnd @@ -275,74 +324,60 @@ public static function encryptWithAd( This uses salted HKDF to split the keys, which is why we need the salt in the first place. */ - list($encKey, $authKey) = self::splitKeys($secretKey, $salt, $config); + [$encKey, $authKey] = Util::splitKeys($secretKey, $salt, $config); // Encrypt our message with the encryption key: - /** @var string $encrypted */ - $encrypted = \sodium_crypto_stream_xor( - $plaintext->getString(), - $nonce, - $encKey - ); - \sodium_memzero($encKey); + + if ($config->ENC_ALGO === 'XChaCha20') { + $encrypted = sodium_crypto_stream_xchacha20_xor( + $plaintext->getString(), + $nonce, + $encKey + ); + } else { + $encrypted = sodium_crypto_stream_xor( + $plaintext->getString(), + $nonce, + $encKey + ); + } + Util::memzero($encKey); // Calculate an authentication tag: - $auth = self::calculateMAC( - Halite::HALITE_VERSION . $salt . $nonce . $additionalData . $encrypted, - $authKey, - $config - ); - \sodium_memzero($authKey); + if ($config->USE_PAE) { + $auth = self::calculateMAC( + Util::PAE( + Halite::HALITE_VERSION, + $salt, + $nonce, + $additionalData, + $encrypted + ), + $authKey, + $config + ); + } else { + $auth = self::calculateMAC( + Halite::HALITE_VERSION . $salt . $nonce . $additionalData . $encrypted, + $authKey, + $config + ); + } + Util::memzero($authKey); - /** @var string $message */ $message = Halite::HALITE_VERSION . $salt . $nonce . $encrypted . $auth; // Wipe every superfluous piece of data from memory - \sodium_memzero($nonce); - \sodium_memzero($salt); - \sodium_memzero($encrypted); - \sodium_memzero($auth); + Util::memzero($nonce); + Util::memzero($salt); + Util::memzero($encrypted); + Util::memzero($auth); $encoder = Halite::chooseEncoder($encoding); if ($encoder) { return (string) $encoder($message); } - return (string) $message; - - } - - /** - * Split a key (using HKDF-BLAKE2b instead of HKDF-HMAC-*) - * - * @param EncryptionKey $master - * @param string $salt - * @param BaseConfig $config - * @return string[] - * - * @throws CannotPerformOperation - * @throws InvalidDigestLength - * @throws \TypeError - */ - public static function splitKeys( - EncryptionKey $master, - string $salt, - BaseConfig $config - ): array { - $binary = $master->getRawKeyMaterial(); - return [ - CryptoUtil::hkdfBlake2b( - $binary, - \SODIUM_CRYPTO_SECRETBOX_KEYBYTES, - (string) $config->HKDF_SBOX, - $salt - ), - CryptoUtil::hkdfBlake2b( - $binary, - \SODIUM_CRYPTO_AUTH_KEYBYTES, - (string) $config->HKDF_AUTH, - $salt - ) - ]; + return $message; } /** @@ -351,10 +386,11 @@ public static function splitKeys( * Should return exactly 6 elements. * * @param string $ciphertext + * * @return array * * @throws InvalidMessage - * @throws \TypeError + * @throws TypeError * @codeCoverageIgnore */ public static function unpackMessageForDecryption(string $ciphertext): array @@ -395,7 +431,7 @@ public static function unpackMessageForDecryption(string $ciphertext): array // 36: Halite::VERSION_TAG_LEN + (int) $config->HKDF_SALT_LEN, // 24: - \SODIUM_CRYPTO_STREAM_NONCEBYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES ); // This is the crypto_stream_xor()ed ciphertext @@ -404,12 +440,12 @@ public static function unpackMessageForDecryption(string $ciphertext): array // 60: Halite::VERSION_TAG_LEN + (int) $config->HKDF_SALT_LEN + - \SODIUM_CRYPTO_STREAM_NONCEBYTES, + SODIUM_CRYPTO_STREAM_NONCEBYTES, // $length - 124 $length - ( Halite::VERSION_TAG_LEN + (int) $config->HKDF_SALT_LEN + - \SODIUM_CRYPTO_STREAM_NONCEBYTES + + SODIUM_CRYPTO_STREAM_NONCEBYTES + (int) $config->MAC_SIZE ) ); @@ -421,7 +457,7 @@ public static function unpackMessageForDecryption(string $ciphertext): array ); // We don't need this anymore. - \sodium_memzero($ciphertext); + Util::memzero($ciphertext); // Now we return the pieces in a specific order: return [$version, $config, $salt, $nonce, $encrypted, $auth]; @@ -433,21 +469,23 @@ public static function unpackMessageForDecryption(string $ciphertext): array * @param string $message * @param AuthenticationKey $secretKey * @param string $mac - * @param mixed $encoding - * @param SymmetricConfig $config + * @param string|bool $encoding + * @param ?SymmetricConfig $config + * * @return bool * * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function verify( string $message, AuthenticationKey $secretKey, string $mac, - $encoding = Halite::ENCODE_BASE64URLSAFE, - SymmetricConfig $config = null + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE, + ?SymmetricConfig $config = null ): bool { $decoder = Halite::chooseEncoder($encoding, true); if ($decoder) { @@ -455,7 +493,7 @@ public static function verify( /** @var string $mac */ $mac = $decoder($mac); } - if ($config === null) { + if (is_null($config)) { // Default to the current version $config = SymmetricConfig::getConfig( Halite::HALITE_VERSION, @@ -482,8 +520,11 @@ public static function verify( * @param string $message * @param string $authKey * @param SymmetricConfig $config + * * @return string + * * @throws InvalidMessage + * @throws SodiumException */ protected static function calculateMAC( string $message, @@ -491,7 +532,7 @@ protected static function calculateMAC( SymmetricConfig $config ): string { if ($config->MAC_ALGO === 'BLAKE2b') { - return \sodium_crypto_generichash( + return sodium_crypto_generichash( $message, $authKey, (int) $config->MAC_SIZE @@ -508,14 +549,16 @@ protected static function calculateMAC( * Verify a Message Authentication Code (MAC) of a message, with a shared * key. * - * @param string $mac Message Authentication Code - * @param string $message The message to verify - * @param string $authKey Authentication key (symmetric) - * @param SymmetricConfig $config Configuration object + * @param string $mac Message Authentication Code + * @param string $message The message to verify + * @param string $authKey Authentication key (symmetric) + * @param SymmetricConfig $config Configuration object + * * @return bool * * @throws InvalidMessage * @throws InvalidSignature + * @throws SodiumException */ protected static function verifyMAC( string $mac, @@ -531,13 +574,13 @@ protected static function verifyMAC( // @codeCoverageIgnoreEnd } if ($config->MAC_ALGO === 'BLAKE2b') { - $calc = \sodium_crypto_generichash( + $calc = sodium_crypto_generichash( $message, $authKey, (int) $config->MAC_SIZE ); - $res = \hash_equals($mac, $calc); - \sodium_memzero($calc); + $res = hash_equals($mac, $calc); + Util::memzero($calc); return $res; } // @codeCoverageIgnoreStart diff --git a/src/Symmetric/EncryptionKey.php b/src/Symmetric/EncryptionKey.php index 61160b43..ce48f4bc 100644 --- a/src/Symmetric/EncryptionKey.php +++ b/src/Symmetric/EncryptionKey.php @@ -5,6 +5,9 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; +use TypeError; +use const SODIUM_CRYPTO_STREAM_KEYBYTES; +use function sprintf; /** * Class EncryptionKey @@ -12,7 +15,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class EncryptionKey extends SecretKey { @@ -20,13 +23,18 @@ final class EncryptionKey extends SecretKey * EncryptionKey constructor. * @param HiddenString $keyMaterial - The actual key data * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ - public function __construct(HiddenString $keyMaterial) - { - if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_STREAM_KEYBYTES) { + public function __construct( + #[\SensitiveParameter] + HiddenString $keyMaterial + ) { + if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_STREAM_KEYBYTES) { throw new InvalidKey( - 'Encryption key must be CRYPTO_STREAM_KEYBYTES bytes long' + sprintf( + 'Encryption key must be CRYPTO_STREAM_KEYBYTES (%d) bytes long', + SODIUM_CRYPTO_STREAM_KEYBYTES + ) ); } parent::__construct($keyMaterial); diff --git a/src/Symmetric/SecretKey.php b/src/Symmetric/SecretKey.php index 42588e72..22012839 100644 --- a/src/Symmetric/SecretKey.php +++ b/src/Symmetric/SecretKey.php @@ -10,7 +10,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class SecretKey extends Key { diff --git a/src/Util.php b/src/Util.php index 57fc5e17..3edf049b 100644 --- a/src/Util.php +++ b/src/Util.php @@ -2,6 +2,7 @@ declare(strict_types=1); namespace ParagonIE\Halite; +use Error; use ParagonIE\ConstantTime\{ Binary, Hex @@ -11,6 +12,26 @@ InvalidDigestLength, InvalidType }; +use ParagonIE\Halite\Symmetric\EncryptionKey; +use RangeException; +use SodiumException; +use Throwable; +use TypeError; +use const + SODIUM_CRYPTO_GENERICHASH_BYTES, + SODIUM_CRYPTO_GENERICHASH_BYTES_MIN, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + SODIUM_CRYPTO_GENERICHASH_KEYBYTES; +use function + array_values, + count, + implode, + pack, + sodium_crypto_generichash, + sodium_memzero, + sprintf, + str_repeat, + unpack; /** * Class Util @@ -20,56 +41,61 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class Util { /** * Don't allow this to be instantiated. - * @throws \Error + * @throws Error * @codeCoverageIgnore */ - final private function __construct() + private function __construct() { - throw new \Error('Do not instantiate'); + throw new Error('Do not instantiate'); } /** * Convert a character to an integer (without cache-timing side-channels) * * @param string $chr + * * @return int - * @throws \RangeException + * + * @throws RangeException */ public static function chrToInt(string $chr): int { if (Binary::safeStrlen($chr) !== 1) { - throw new \RangeException('Must be a string with a length of 1'); + throw new RangeException('Must be a string with a length of 1'); } - $result = \unpack('C', $chr); + $result = unpack('C', $chr); return (int) $result[1]; } /** - * Wrapper around SODIUM_CRypto_generichash() + * Wrapper around sodium_crypto_generichash() * * Returns hexadecimal characters. * * @param string $input * @param int $length + * * @return string + * * @throws CannotPerformOperation - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function hash( string $input, - int $length = \SODIUM_CRYPTO_GENERICHASH_BYTES + int $length = SODIUM_CRYPTO_GENERICHASH_BYTES ): string { return Hex::encode( self::raw_keyed_hash($input, '', $length) @@ -77,25 +103,28 @@ public static function hash( } /** - * Wrapper around SODIUM_CRypto_generichash() + * Wrapper around sodium_crypto_generichash() * * Returns raw binary. * * @param string $input * @param int $length + * * @return string + * * @throws CannotPerformOperation + * @throws SodiumException */ public static function raw_hash( string $input, - int $length = \SODIUM_CRYPTO_GENERICHASH_BYTES + int $length = SODIUM_CRYPTO_GENERICHASH_BYTES ): string { return self::raw_keyed_hash($input, '', $length); } /** * Use a derivative of HKDF to derive multiple keys from one. - * http://tools.ietf.org/html/rfc5869 + * https://datatracker.ietf.org/doc/html/rfc5869 * * This is a variant from hash_hkdf() and instead uses BLAKE2b provided by * libsodium. @@ -107,10 +136,13 @@ public static function raw_hash( * @param int $length How many bytes? * @param string $info What sort of key are we deriving? * @param string $salt + * * @return string + * * @throws CannotPerformOperation * @throws InvalidDigestLength - * @throws \TypeError + * @throws TypeError + * @throws SodiumException */ public static function hkdfBlake2b( string $ikm, @@ -119,7 +151,7 @@ public static function hkdfBlake2b( string $salt = '' ): string { // Sanity-check the desired output length. - if ($length < 0 || $length > (255 * \SODIUM_CRYPTO_GENERICHASH_KEYBYTES)) { + if ($length < 0 || $length > (255 * SODIUM_CRYPTO_GENERICHASH_KEYBYTES)) { throw new InvalidDigestLength( 'Argument 2: Bad HKDF Digest Length' ); @@ -127,19 +159,22 @@ public static function hkdfBlake2b( // "If [salt] not provided, is set to a string of HashLen zeroes." if (empty($salt)) { // @codeCoverageIgnoreStart - $salt = \str_repeat("\x00", \SODIUM_CRYPTO_GENERICHASH_KEYBYTES); + $salt = str_repeat("\x00", SODIUM_CRYPTO_GENERICHASH_KEYBYTES); // @codeCoverageIgnoreEnd } // HKDF-Extract: // PRK = HMAC-Hash(salt, IKM) // The salt is the HMAC key. + // + // Note: The notation used by the RFC is backwards from what we're doing here. + // They use (Key, Msg) while our API is (Msg, Key). $prk = self::raw_keyed_hash($ikm, $salt); // HKDF-Expand: // This check is useless, but it serves as a reminder to the spec. // @codeCoverageIgnoreStart - if (Binary::safeStrlen($prk) < \SODIUM_CRYPTO_GENERICHASH_KEYBYTES) { + if (Binary::safeStrlen($prk) < SODIUM_CRYPTO_GENERICHASH_KEYBYTES) { throw new CannotPerformOperation( 'An unknown error has occurred' ); @@ -151,22 +186,21 @@ public static function hkdfBlake2b( for ($block_index = 1; Binary::safeStrlen($t) < $length; ++$block_index) { // T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??) $last_block = self::raw_keyed_hash( - $last_block . $info . \chr($block_index), + $last_block . $info . pack('C', $block_index), $prk ); // T = T(1) | T(2) | T(3) | ... | T(N) $t .= $last_block; } // ORM = first L octets of T - /** @var string $orm */ - $orm = Binary::safeSubstr($t, 0, $length); - return $orm; + return Binary::safeSubstr($t, 0, $length); } /** * Convert an array of integers to a string * * @param array $integers + * * @return string */ public static function intArrayToString(array $integers): string @@ -175,8 +209,8 @@ public static function intArrayToString(array $integers): string foreach ($args as $i => $v) { $args[$i] = (int) ($v & 0xff); } - return \pack( - \str_repeat('C', \count($args)), + return pack( + str_repeat('C', count($args)), ...$args ); } @@ -189,7 +223,7 @@ public static function intArrayToString(array $integers): string */ public static function intToChr(int $int): string { - return \pack('C', $int); + return pack('C', $int); } /** @@ -201,20 +235,40 @@ public static function intToChr(int $int): string * @param string $input * @param string $key * @param int $length + * * @return string + * * @throws CannotPerformOperation - * @throws \TypeError + * @throws TypeError + * @throws SodiumException */ public static function keyed_hash( string $input, string $key, - int $length = \SODIUM_CRYPTO_GENERICHASH_BYTES + int $length = SODIUM_CRYPTO_GENERICHASH_BYTES ): string { return Hex::encode( self::raw_keyed_hash($input, $key, $length) ); } + /** + * Pre-authentication encoding + * + * @param string ...$pieces + * + * @return string + */ + public static function PAE(string ...$pieces): string + { + $out = []; + $out[] = pack('P', count($pieces)); + foreach ($pieces as $piece) { + $out[] = pack('P', Binary::safeStrlen($piece)) . $piece; + } + return implode($out); + } + /** * Wrapper around SODIUM_CRypto_generichash() * @@ -224,31 +278,34 @@ public static function keyed_hash( * @param string $input * @param string $key * @param int $length + * * @return string + * * @throws CannotPerformOperation + * @throws SodiumException */ public static function raw_keyed_hash( string $input, string $key, - int $length = \SODIUM_CRYPTO_GENERICHASH_BYTES + int $length = SODIUM_CRYPTO_GENERICHASH_BYTES ): string { - if ($length < \SODIUM_CRYPTO_GENERICHASH_BYTES_MIN) { + if ($length < SODIUM_CRYPTO_GENERICHASH_BYTES_MIN) { throw new CannotPerformOperation( - \sprintf( + sprintf( 'Output length must be at least %d bytes.', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MIN + SODIUM_CRYPTO_GENERICHASH_BYTES_MIN ) ); } - if ($length > \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX) { + if ($length > SODIUM_CRYPTO_GENERICHASH_BYTES_MAX) { throw new CannotPerformOperation( - \sprintf( + sprintf( 'Output length must be at most %d bytes.', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ) ); } - return \sodium_crypto_generichash($input, $key, $length); + return sodium_crypto_generichash($input, $key, $length); } /** @@ -256,14 +313,15 @@ public static function raw_keyed_hash( * the original string. * * @param string $string + * * @return string - * @throws \TypeError + * + * @throws TypeError */ public static function safeStrcpy(string $string): string { $length = Binary::safeStrlen($string); $return = ''; - /** @var int $chunk */ $chunk = $length >> 1; if ($chunk < 1) { $chunk = 1; @@ -274,31 +332,92 @@ public static function safeStrcpy(string $string): string return $return; } + /** + * Split a key (using HKDF-BLAKE2b instead of HKDF-HMAC-*) + * + * @param EncryptionKey $master + * @param string $salt + * @param Config $config + * + * @return string[] + * + * @throws CannotPerformOperation + * @throws InvalidDigestLength + * @throws SodiumException + * @throws TypeError + */ + public static function splitKeys( + EncryptionKey $master, + string $salt, + Config $config + ): array { + $binary = $master->getRawKeyMaterial(); + + /* + * From Halite version 5, we use the HKDF info parameter instead of the salt. + * This does two things: + * + * 1. It allows us to use the HKDF security definition (which is stronger than a PRF) + * 2. It allows us to reuse the intermediary step and make key derivation faster. + */ + if ($config->HKDF_USE_INFO) { + $prk = self::raw_keyed_hash( + $binary, + str_repeat("\x00", SODIUM_CRYPTO_GENERICHASH_KEYBYTES) + ); + $return = [ + self::raw_keyed_hash(((string) $config->HKDF_SBOX) . $salt . "\x01", $prk), + self::raw_keyed_hash(((string) $config->HKDF_AUTH) . $salt . "\x01", $prk) + ]; + self::memzero($prk); + return $return; + } + + /* + * Halite 4 and blow used this strategy: + */ + return [ + Util::hkdfBlake2b( + $binary, + SODIUM_CRYPTO_SECRETBOX_KEYBYTES, + (string) $config->HKDF_SBOX, + $salt + ), + Util::hkdfBlake2b( + $binary, + SODIUM_CRYPTO_AUTH_KEYBYTES, + (string) $config->HKDF_AUTH, + $salt + ) + ]; + } + /** * Turn a string into an array of integers * * @param string $string + * * @return array - * @throws \TypeError + * + * @throws TypeError */ public static function stringToIntArray(string $string): array { /** * @var array */ - $values = \array_values(\unpack('C*', $string)); + $values = array_values(unpack('C*', $string)); return $values; } /** * Calculate A xor B, given two binary strings of the same length. * - * Uses pack() and unpack() to avoid cache-timing leaks caused by - * chr(). - * * @param string $left * @param string $right + * * @return string + * * @throws InvalidType */ public static function xorStrings(string $left, string $right): string @@ -314,4 +433,26 @@ public static function xorStrings(string $left, string $right): string } return (string) ($left ^ $right); } + + /** + * Wrap memzero() without breaking on sodium_compat + * + * @param string &$var + * + * @return void + * + * @psalm-param-out null $var + * @psalm-suppress UnnecessaryVarAnnotation + * @psalm-suppress InvalidOperand + */ + public static function memzero(string &$var): void + { + try { + sodium_memzero($var); + } catch (Throwable $ex) { + // Best-effort: + $var ^= $var; + } + unset($var); + } } diff --git a/stub/Sodium.stub.php b/stub/Sodium.stub.php index aaac7def..fd199ce9 100644 --- a/stub/Sodium.stub.php +++ b/stub/Sodium.stub.php @@ -967,14 +967,14 @@ function compare( /** * Convert from hex without side-chanels * - * @param string $binary + * @param string $hex * @return string */ function hex2bin( - string $binary + string $hex ): string { if (\extension_loaded('sodium')) { - return \sodium_hex2bin($binary); + return \sodium_hex2bin($hex); } return ''; } @@ -1089,4 +1089,4 @@ function crypto_scalarmult_base( } return ''; } -} \ No newline at end of file +} diff --git a/stub/sodium_stub.php b/stub/sodium_stub.php index 46beb953..621b6166 100644 --- a/stub/sodium_stub.php +++ b/stub/sodium_stub.php @@ -970,14 +970,14 @@ function sodium_compare( /** * Convert from hex without side-chanels * - * @param string $binary + * @param string $hex * @return string */ function sodium_hex2bin( - string $binary + string $hex ): string { if (\extension_loaded('libsodium')) { - return \Sodium\hex2bin($binary); + return \Sodium\hex2bin($hex); } return ''; } @@ -1092,4 +1092,4 @@ function sodium_crypto_scalarmult_base( } return ''; } -} \ No newline at end of file +} diff --git a/test/unit/AsymmetricTest.php b/test/unit/AsymmetricTest.php index e59833f6..a473ac1e 100644 --- a/test/unit/AsymmetricTest.php +++ b/test/unit/AsymmetricTest.php @@ -4,9 +4,11 @@ use ParagonIE\Halite\Alerts as CryptoException; use ParagonIE\Halite\KeyFactory; use ParagonIE\Halite\Asymmetric\{ + Config, Crypto as Asymmetric, EncryptionPublicKey, - EncryptionSecretKey + EncryptionSecretKey, + SignatureSecretKey }; use ParagonIE\Halite\Halite; use ParagonIE\HiddenString\HiddenString; @@ -25,6 +27,9 @@ final class AsymmetricTest extends TestCase */ public function testEncrypt() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateEncryptionKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); @@ -59,6 +64,9 @@ public function testEncrypt() */ public function testEncryptWithAd() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateEncryptionKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); @@ -124,6 +132,9 @@ public function testEncryptWithAd() */ public function testEncryptEmpty() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateEncryptionKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); @@ -149,6 +160,9 @@ public function testEncryptEmpty() public function testEncryptFail() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateEncryptionKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); @@ -174,7 +188,7 @@ public function testEncryptFail() 'This should have thrown an InvalidMessage exception!' ); } catch (CryptoException\InvalidMessage $e) { - $this->assertTrue($e instanceof CryptoException\InvalidMessage); + $this->assertInstanceOf(CryptoException\InvalidMessage::class, $e); } } @@ -186,6 +200,9 @@ public function testEncryptFail() */ public function testSeal() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } if ( SODIUM_LIBRARY_MAJOR_VERSION < 7 || (SODIUM_LIBRARY_MAJOR_VERSION == 7 && SODIUM_LIBRARY_MINOR_VERSION < 5) @@ -242,6 +259,9 @@ public function testSeal() */ public function testSealFail() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } if ( SODIUM_LIBRARY_MAJOR_VERSION < 7 || (SODIUM_LIBRARY_MAJOR_VERSION == 7 && SODIUM_LIBRARY_MINOR_VERSION < 5) @@ -267,9 +287,9 @@ public function testSealFail() 'This should have thrown an InvalidMessage exception!' ); } catch (CryptoException\InvalidKey $e) { - $this->assertTrue($e instanceof CryptoException\InvalidKey); + $this->assertInstanceOf(CryptoException\InvalidKey::class, $e); } catch (CryptoException\InvalidMessage $e) { - $this->assertTrue($e instanceof CryptoException\InvalidMessage); + $this->assertInstanceOf(CryptoException\InvalidMessage::class, $e); } } @@ -281,6 +301,9 @@ public function testSealFail() */ public function testSign() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateSignatureKeyPair(); $message = 'test message'; @@ -304,10 +327,13 @@ public function testSign() */ public function testSignEncrypt() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateSignatureKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); - // http://time.com/4261796/tim-cook-transcript/ + // https://time.com/4261796/tim-cook-transcript/ $message = new HiddenString( 'When I think of civil liberties I think of the founding principles of the country. ' . 'The freedoms that are in the First Amendment. But also the fundamental right to privacy.' @@ -344,10 +370,13 @@ public function testSignEncrypt() */ public function testSignEncryptFail() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateSignatureKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); - // http://time.com/4261796/tim-cook-transcript/ + // https://time.com/4261796/tim-cook-transcript/ $junk = new HiddenString( // Instead of a signature, it's 64 random bytes random_bytes(SODIUM_CRYPTO_SIGN_BYTES) . @@ -367,10 +396,10 @@ public function testSignEncryptFail() ); $this->fail('Invalid signature was accepted.'); } catch (CryptoException\InvalidSignature $ex) { - $this->assertTrue(true); + $this->assertInstanceOf(CryptoException\InvalidSignature::class, $ex); } - // http://time.com/4261796/tim-cook-transcript/ + // https://time.com/4261796/tim-cook-transcript/ $message = new HiddenString( 'When I think of civil liberties I think of the founding principles of the country. ' . 'The freedoms that are in the First Amendment. But also the fundamental right to privacy.' @@ -414,6 +443,9 @@ public function testSignEncryptFail() */ public function testSignFail() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateSignatureKeyPair(); $message = 'test message'; @@ -459,4 +491,48 @@ public function testSignFail() } } } + + /** + * @return void + * @throws CryptoException\InvalidMessage + */ + public function testGetConfigInvalidMode(): void + { + $this->expectException(CryptoException\InvalidMessage::class); + $this->expectExceptionMessage('Invalid configuration mode: decrypt'); + Config::getConfig(str_repeat('A', 4), 'decrypt'); + } + + /** + * @throws CryptoException\InvalidMessage + */ + public function testGetConfigEncryptInvalidVersion(): void + { + $this->expectException(CryptoException\InvalidMessage::class); + $this->expectExceptionMessage('Invalid version tag'); + Config::getConfigEncrypt(1, 0); + } + + /** + * @throws CryptoException\InvalidKey + */ + public function testInvalidSignatureSecretKey(): void + { + $this->expectException(CryptoException\InvalidKey::class); + new SignatureSecretKey(new HiddenString('invalid')); + } + + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\InvalidKey + * @throws SodiumException + */ + public function testCachedPublicKey(): void + { + $keypair = KeyFactory::generateSignatureKeyPair(); + $secretKey = $keypair->getSecretKey(); + $secretKey->derivePublicKey(); + $encryptionSecretKey = $secretKey->getEncryptionSecretKey(); + $this->assertInstanceOf(\ParagonIE\Halite\Asymmetric\EncryptionSecretKey::class, $encryptionSecretKey); + } } diff --git a/test/unit/ConfigTest.php b/test/unit/ConfigTest.php index 21238c7a..a78caf55 100644 --- a/test/unit/ConfigTest.php +++ b/test/unit/ConfigTest.php @@ -9,6 +9,7 @@ class ConfigTest extends TestCase { public function testConfig() { + /** @var object{abc:12345}&Config $config */ $config = new Config([ 'abc' => 12345 ]); diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php index 38070855..91fdb36b 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -16,9 +16,89 @@ final class FileTest extends TestCase { - public function setUp() + + public function setUp(): void { chmod(__DIR__.'/tmp/', 0777); + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } + } + + /** + * @throws CryptoException\InvalidKey + * @throws SodiumException + */ + public function testAsymmetricEncrypt() + { + touch(__DIR__.'/tmp/paragon_avatar.a-encrypted.png'); + chmod(__DIR__.'/tmp/paragon_avatar.a-encrypted.png', 0777); + touch(__DIR__.'/tmp/paragon_avatar.a-encrypted-aad.png'); + chmod(__DIR__.'/tmp/paragon_avatar.a-encrypted-aad.png', 0777); + touch(__DIR__.'/tmp/paragon_avatar.a-decrypted.png'); + chmod(__DIR__.'/tmp/paragon_avatar.a-decrypted.png', 0777); + touch(__DIR__.'/tmp/paragon_avatar.a-decrypted-aad.png'); + chmod(__DIR__.'/tmp/paragon_avatar.a-decrypted-aad.png', 0777); + + $alice = KeyFactory::generateEncryptionKeyPair(); + $aliceSecret = $alice->getSecretKey(); + $alicePublic = $alice->getPublicKey(); + $bob = KeyFactory::generateEncryptionKeyPair(); + $bobSecret = $bob->getSecretKey(); + $bobPublic = $bob->getPublicKey(); + + File::asymmetricEncrypt( + __DIR__.'/tmp/paragon_avatar.png', + __DIR__.'/tmp/paragon_avatar.a-encrypted.png', + $bobPublic, + $aliceSecret + ); + File::asymmetricDecrypt( + __DIR__.'/tmp/paragon_avatar.a-encrypted.png', + __DIR__.'/tmp/paragon_avatar.a-decrypted.png', + $bobSecret, + $alicePublic + ); + $this->assertSame( + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.a-decrypted.png') + ); + + // Now with AAD: + $aad = 'Halite v5 test'; + File::asymmetricEncrypt( + __DIR__.'/tmp/paragon_avatar.png', + __DIR__.'/tmp/paragon_avatar.a-encrypted-aad.png', + $bobPublic, + $aliceSecret, + $aad + ); + + try { + File::asymmetricDecrypt( + __DIR__.'/tmp/paragon_avatar.a-encrypted-aad.png', + __DIR__.'/tmp/paragon_avatar.a-decrypted-aad.png', + $bobSecret, + $alicePublic + ); + } catch (CryptoException\HaliteAlert $ex) { + $this->assertSame( + 'Invalid message authentication code', + $ex->getMessage() + ); + } + File::asymmetricDecrypt( + __DIR__.'/tmp/paragon_avatar.a-encrypted-aad.png', + __DIR__.'/tmp/paragon_avatar.a-decrypted-aad.png', + $bobSecret, + $alicePublic, + $aad + ); + + unlink(__DIR__.'/tmp/paragon_avatar.a-encrypted.png'); + unlink(__DIR__.'/tmp/paragon_avatar.a-decrypted.png'); + unlink(__DIR__.'/tmp/paragon_avatar.a-encrypted-aad.png'); + unlink(__DIR__.'/tmp/paragon_avatar.a-decrypted-aad.png'); } /** @@ -62,6 +142,62 @@ public function testEncrypt() unlink(__DIR__.'/tmp/paragon_avatar.decrypted.png'); } + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\FileError + * @throws CryptoException\FileModified + * @throws CryptoException\InvalidDigestLength + * @throws CryptoException\InvalidKey + * @throws CryptoException\InvalidMessage + * @throws CryptoException\InvalidType + * @throws TypeError + */ + public function testEncryptWithAAD() + { + touch(__DIR__.'/tmp/paragon_avatar.encrypted-aad.png'); + chmod(__DIR__.'/tmp/paragon_avatar.encrypted-aad.png', 0777); + touch(__DIR__.'/tmp/paragon_avatar.decrypted-aad.png'); + chmod(__DIR__.'/tmp/paragon_avatar.decrypted-aad.png', 0777); + + $key = new EncryptionKey( + new HiddenString(\str_repeat('B', 32)) + ); + $aad = "Additional associated data"; + + File::encrypt( + __DIR__.'/tmp/paragon_avatar.png', + __DIR__.'/tmp/paragon_avatar.encrypted-aad.png', + $key, + $aad + ); + try { + File::decrypt( + __DIR__.'/tmp/paragon_avatar.encrypted-aad.png', + __DIR__.'/tmp/paragon_avatar.decrypted-aad.png', + $key + ); + } catch (CryptoException\HaliteAlert $ex) { + $this->assertSame( + 'Invalid message authentication code', + $ex->getMessage() + ); + } + File::decrypt( + __DIR__.'/tmp/paragon_avatar.encrypted-aad.png', + __DIR__.'/tmp/paragon_avatar.decrypted-aad.png', + $key, + $aad + ); + $this->assertSame( + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.decrypted-aad.png') + ); + + unlink(__DIR__.'/tmp/paragon_avatar.encrypted-aad.png'); + unlink(__DIR__.'/tmp/paragon_avatar.decrypted-aad.png'); + } + /** * @throws CryptoException\CannotPerformOperation * @throws CryptoException\FileAccessDenied @@ -148,21 +284,10 @@ public function testEncryptFail() 'This should have thrown an InvalidMessage exception!' ); } catch (CryptoException\InvalidMessage $e) { - $this->assertTrue($e instanceof CryptoException\InvalidMessage); + $this->assertInstanceOf(CryptoException\InvalidMessage::class, $e); unlink(__DIR__.'/tmp/paragon_avatar.encrypt_fail.png'); unlink(__DIR__.'/tmp/paragon_avatar.decrypt_fail.png'); } - - try { - File::encrypt(true, false, $key); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } - try { - File::decrypt(true, false, $key); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } } /** @@ -204,7 +329,7 @@ public function testEncryptSmallFail() file_put_contents( __DIR__.'/tmp/empty.encrypted.txt', - "\x31\x41\x03\x00\x01" + "\x31\x41\x04\x00\x01" ); try { File::decrypt( @@ -220,7 +345,7 @@ public function testEncryptSmallFail() file_put_contents( __DIR__.'/tmp/empty.encrypted.txt', - "\x31\x41\x03\x00" . \str_repeat("\x00", 87) + "\x31\x41\x04\x00" . \str_repeat("\x00", 87) ); try { File::decrypt( @@ -311,30 +436,112 @@ public function testSeal() chmod(__DIR__.'/tmp/paragon_avatar.sealed.png', 0777); touch(__DIR__.'/tmp/paragon_avatar.opened.png'); chmod(__DIR__.'/tmp/paragon_avatar.opened.png', 0777); - + $keypair = KeyFactory::generateEncryptionKeyPair(); - $secretkey = $keypair->getSecretKey(); - $publickey = $keypair->getPublicKey(); - + $secretkey = $keypair->getSecretKey(); + $publickey = $keypair->getPublicKey(); + File::seal( __DIR__.'/tmp/paragon_avatar.png', __DIR__.'/tmp/paragon_avatar.sealed.png', $publickey ); - + File::unseal( __DIR__.'/tmp/paragon_avatar.sealed.png', __DIR__.'/tmp/paragon_avatar.opened.png', $secretkey ); - + $this->assertSame( hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), hash_file('sha256', __DIR__.'/tmp/paragon_avatar.opened.png') ); - + + // New: Additional Associated Data tests + $aad = "Additional associated data"; + File::seal( + __DIR__.'/tmp/paragon_avatar.png', + __DIR__.'/tmp/paragon_avatar.sealed-aad.png', + $publickey, + $aad + ); + try { + File::unseal( + __DIR__.'/tmp/paragon_avatar.sealed-aad.png', + __DIR__.'/tmp/paragon_avatar.opened-aad.png', + $secretkey + ); + } catch (CryptoException\HaliteAlert $ex) { + $this->assertSame( + 'Invalid message authentication code', + $ex->getMessage() + ); + } + + File::unseal( + __DIR__.'/tmp/paragon_avatar.sealed-aad.png', + __DIR__.'/tmp/paragon_avatar.opened-aad.png', + $secretkey, + $aad + ); + + $this->assertSame( + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.opened-aad.png') + ); + + unlink(__DIR__.'/tmp/paragon_avatar.sealed.png'); + unlink(__DIR__.'/tmp/paragon_avatar.opened.png'); + unlink(__DIR__.'/tmp/paragon_avatar.sealed-aad.png'); + unlink(__DIR__.'/tmp/paragon_avatar.opened-aad.png'); + } + + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\FileError + * @throws CryptoException\FileModified + * @throws CryptoException\InvalidDigestLength + * @throws CryptoException\InvalidMessage + * @throws CryptoException\InvalidType + * @throws Exception + * @throws TypeError + */ + public function testSealFromStreamWrapper() + { + require_once __DIR__ . '/RemoteStream.php'; + stream_register_wrapper('haliteTest', RemoteStream::class); + touch(__DIR__.'/tmp/paragon_avatar.sealed.png'); + chmod(__DIR__.'/tmp/paragon_avatar.sealed.png', 0777); + touch(__DIR__.'/tmp/paragon_avatar.opened.png'); + chmod(__DIR__.'/tmp/paragon_avatar.opened.png', 0777); + + $keypair = KeyFactory::generateEncryptionKeyPair(); + $secretkey = $keypair->getSecretKey(); + $publickey = $keypair->getPublicKey(); + + $file = new ReadOnlyFile(fopen('haliteTest://paragon_avatar.png', 'rb')); + File::seal( + $file, + __DIR__.'/tmp/paragon_avatar.sealed.png', + $publickey + ); + + File::unseal( + __DIR__.'/tmp/paragon_avatar.sealed.png', + __DIR__.'/tmp/paragon_avatar.opened.png', + $secretkey + ); + + $this->assertSame( + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.opened.png') + ); + unlink(__DIR__.'/tmp/paragon_avatar.sealed.png'); unlink(__DIR__.'/tmp/paragon_avatar.opened.png'); + $this->assertEquals($file->getHash(), (new ReadOnlyFile(__DIR__.'/tmp/paragon_avatar.png'))->getHash()); } /** @@ -425,21 +632,10 @@ public function testSealFail() 'This should have thrown an InvalidMessage exception!' ); } catch (CryptoException\InvalidMessage $e) { - $this->assertTrue($e instanceof CryptoException\InvalidMessage); + $this->assertInstanceOf(CryptoException\InvalidMessage::class, $e); unlink(__DIR__.'/tmp/paragon_avatar.seal_fail.png'); unlink(__DIR__.'/tmp/paragon_avatar.open_fail.png'); } - - try { - File::seal(true, false, $publickey); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } - try { - File::unseal(true, false, $secretkey); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } } /** @@ -478,7 +674,7 @@ public function testSealSmallFail() file_put_contents( __DIR__.'/tmp/empty.sealed.txt', - "\x31\x41\x03\x00" . \str_repeat("\x00", 95) + "\x31\x41\x04\x00" . \str_repeat("\x00", 95) ); try { File::unseal( @@ -583,17 +779,6 @@ public function testSign() $signature ) ); - - try { - File::sign(true, $secretkey); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } - try { - File::verify(false, $publickey, ''); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } } /** @@ -683,11 +868,6 @@ public function testChecksum() File::checksum(__DIR__.'/tmp/garbage.dat', KeyFactory::generateAuthenticationKey(), true); File::checksum(__DIR__.'/tmp/garbage.dat', KeyFactory::generateSignatureKeyPair()->getPublicKey(), true); - try { - File::checksum(false); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } try { File::checksum(__DIR__.'/tmp/garbage.dat', KeyFactory::generateEncryptionKey()); $this->fail('Invalid type was accepted.'); @@ -714,4 +894,79 @@ public function testNonExistingOutputFile() ); $this->assertTrue(\file_exists(__DIR__.'/tmp/empty116.encrypted.txt')); } + + public function testOutputToOutputbuffer() + { + $stream = fopen('php://output', 'wb'); + + touch(__DIR__.'/tmp/paragon_avatar.encrypted.png'); + chmod(__DIR__.'/tmp/paragon_avatar.encrypted.png', 0777); + + $key = new EncryptionKey( + new HiddenString(\str_repeat('B', 32)) + ); + File::encrypt( + __DIR__.'/tmp/paragon_avatar.png', + __DIR__.'/tmp/paragon_avatar.encrypted.png', + $key + ); + + ob_start(); + File::decrypt( + __DIR__.'/tmp/paragon_avatar.encrypted.png', + new MutableFile($stream), + $key + ); + $contents = ob_get_clean(); + + $this->assertSame( + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), + hash('sha256', $contents) + ); + unlink(__DIR__.'/tmp/paragon_avatar.encrypted.png'); + } + + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\FileError + * @throws CryptoException\InvalidKey + * @throws CryptoException\InvalidMessage + * @throws CryptoException\InvalidType + * @throws SodiumException + */ + public function testInvalidChecksumKey(): void + { + $this->expectException(CryptoException\InvalidKey::class); + File::checksum(__DIR__.'/tmp/paragon_avatar.png', KeyFactory::generateEncryptionKey()); + } + + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\FileError + * @throws CryptoException\FileModified + * @throws CryptoException\InvalidDigestLength + * @throws CryptoException\InvalidKey + * @throws CryptoException\InvalidMessage + * @throws CryptoException\InvalidType + * @throws SodiumException + */ + public function testInvalidConfigHeader(): void + { + touch(__DIR__.'/tmp/invalid.txt'); + chmod(__DIR__.'/tmp/invalid.txt', 0777); + file_put_contents(__DIR__.'/tmp/invalid.txt', 'invalid'); + touch(__DIR__.'/tmp/invalid-out.txt'); + chmod(__DIR__.'/tmp/invalid-out.txt', 0777); + $this->expectException(CryptoException\InvalidMessage::class); + File::decrypt( + __DIR__.'/tmp/invalid.txt', + __DIR__.'/tmp/invalid-out.txt', + KeyFactory::generateEncryptionKey() + ); + unlink(__DIR__.'/tmp/invalid.txt'); + unlink(__DIR__.'/tmp/invalid-out.txt'); + } + } diff --git a/test/unit/HaliteTest.php b/test/unit/HaliteTest.php index 6539e7e5..4ed80dda 100644 --- a/test/unit/HaliteTest.php +++ b/test/unit/HaliteTest.php @@ -16,6 +16,9 @@ final class HaliteTest extends TestCase { public function testLibsodiumDetection() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $this->assertTrue( Halite::isLibsodiumSetupCorrectly() ); diff --git a/test/unit/HiddenStringTest.php b/test/unit/HiddenStringTest.php index e0268bda..78ca4fb9 100644 --- a/test/unit/HiddenStringTest.php +++ b/test/unit/HiddenStringTest.php @@ -18,4 +18,4 @@ public function testConstructor() $x = new HiddenString('test'); $this->assertInstanceOf(Outsourced::class, $x); } -} \ No newline at end of file +} diff --git a/test/unit/KeyPairTest.php b/test/unit/KeyPairTest.php index a750cd26..d625cac1 100644 --- a/test/unit/KeyPairTest.php +++ b/test/unit/KeyPairTest.php @@ -27,6 +27,9 @@ final class KeyPairTest extends TestCase */ public function testDeriveSigningKey() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $keypair = KeyFactory::deriveSignatureKeyPair( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" @@ -34,8 +37,8 @@ public function testDeriveSigningKey() $sign_secret = $keypair->getSecretKey(); $sign_public = $keypair->getPublicKey(); - $this->assertTrue($sign_secret instanceof SignatureSecretKey); - $this->assertTrue($sign_public instanceof SignaturePublicKey); + $this->assertInstanceOf(SignatureSecretKey::class, $sign_secret); + $this->assertInstanceOf(SignaturePublicKey::class, $sign_public); // Can this be used? $message = 'This is a test message'; @@ -68,6 +71,9 @@ public function testDeriveSigningKey() */ public function testDeriveSigningKeyOldArgon2i() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $keypair = KeyFactory::deriveSignatureKeyPair( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", @@ -77,8 +83,8 @@ public function testDeriveSigningKeyOldArgon2i() $sign_secret = $keypair->getSecretKey(); $sign_public = $keypair->getPublicKey(); - $this->assertTrue($sign_secret instanceof SignatureSecretKey); - $this->assertTrue($sign_public instanceof SignaturePublicKey); + $this->assertInstanceOf(SignatureSecretKey::class, $sign_secret); + $this->assertInstanceOf(SignaturePublicKey::class, $sign_public); // Can this be used? $message = 'This is a test message'; @@ -108,6 +114,9 @@ public function testDeriveSigningKeyOldArgon2i() */ public function testEncryptionKeyPair() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $boxKeypair = KeyFactory::generateEncryptionKeyPair(); $boxSecret = $boxKeypair->getSecretKey(); $boxPublic = $boxKeypair->getPublicKey(); @@ -215,6 +224,9 @@ public function testEncryptionKeyPair() */ public function testFileStorage() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $filename = tempnam(__DIR__.'/tmp/', 'key'); $key = KeyFactory::generateEncryptionKeyPair(); KeyFactory::save($key, $filename); @@ -234,6 +246,9 @@ public function testFileStorage() */ public function testMutation() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $sign_kp = KeyFactory::generateSignatureKeyPair(); $box_kp = $sign_kp->getEncryptionKeyPair(); $sign_sk = $sign_kp->getSecretKey(); @@ -258,6 +273,9 @@ public function testMutation() */ public function testSignatureKeyPair() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $signKeypair = KeyFactory::generateSignatureKeyPair(); $signSecret = $signKeypair->getSecretKey(); $signPublic = $signKeypair->getPublicKey(); @@ -364,6 +382,9 @@ public function testSignatureKeyPair() */ public function testPublicDerivation() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $enc_kp = KeyFactory::generateEncryptionKeyPair(); $enc_secret = $enc_kp->getSecretKey(); $enc_public = $enc_kp->getPublicKey(); diff --git a/test/unit/KeyTest.php b/test/unit/KeyTest.php index 28822163..8c24a3aa 100644 --- a/test/unit/KeyTest.php +++ b/test/unit/KeyTest.php @@ -25,6 +25,9 @@ class KeyTest extends TestCase */ public function testDerive() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = KeyFactory::deriveEncryptionKey( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", @@ -68,6 +71,9 @@ public function testDerive() */ public function testDeriveOldArgon2i() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = KeyFactory::deriveEncryptionKey( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", @@ -102,6 +108,9 @@ public function testDeriveOldArgon2i() */ public function testDeriveSigningKey() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $keypair = KeyFactory::deriveSignatureKeyPair( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" @@ -109,8 +118,8 @@ public function testDeriveSigningKey() $sign_secret = $keypair->getSecretKey(); $sign_public = $keypair->getPublicKey(); - $this->assertTrue($sign_secret instanceof SignatureSecretKey); - $this->assertTrue($sign_public instanceof SignaturePublicKey); + $this->assertInstanceOf(SignatureSecretKey::class, $sign_secret); + $this->assertInstanceOf(SignaturePublicKey::class, $sign_public); // Can this be used? $message = 'This is a test message'; @@ -138,6 +147,9 @@ public function testDeriveSigningKey() */ public function testDeriveSigningKeyOldArgon2i() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $keypair = KeyFactory::deriveSignatureKeyPair( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", @@ -147,8 +159,8 @@ public function testDeriveSigningKeyOldArgon2i() $sign_secret = $keypair->getSecretKey(); $sign_public = $keypair->getPublicKey(); - $this->assertTrue($sign_secret instanceof SignatureSecretKey); - $this->assertTrue($sign_public instanceof SignaturePublicKey); + $this->assertInstanceOf(SignatureSecretKey::class, $sign_secret); + $this->assertInstanceOf(SignaturePublicKey::class, $sign_public); // Can this be used? $message = 'This is a test message'; @@ -175,6 +187,9 @@ public function testDeriveSigningKeyOldArgon2i() */ public function testImport() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = KeyFactory::generateAuthenticationKey(); $export = KeyFactory::export($key); $import = KeyFactory::importAuthenticationKey($export); @@ -261,6 +276,9 @@ public function testImport() */ public function testKeyTypes() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = KeyFactory::generateAuthenticationKey(); $this->assertFalse($key->isAsymmetricKey()); $this->assertFalse($key->isEncryptionKey()); @@ -315,6 +333,9 @@ public function testKeyTypes() */ public function testEncKeyStorage() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $enc_keypair = KeyFactory::deriveEncryptionKeyPair( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" @@ -333,9 +354,7 @@ public function testEncKeyStorage() ); $load_public = KeyFactory::loadEncryptionPublicKey($file_public); - $this->assertTrue( - $load_public instanceof EncryptionPublicKey - ); + $this->assertInstanceOf(EncryptionPublicKey::class, $load_public); $this->assertTrue( \hash_equals($enc_public->getRawKeyMaterial(), $load_public->getRawKeyMaterial()) ); @@ -361,6 +380,9 @@ public function testEncKeyStorage() */ public function testSignKeyStorage() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $sign_keypair = KeyFactory::deriveSignatureKeyPair( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" @@ -379,9 +401,7 @@ public function testSignKeyStorage() ); $load_public = KeyFactory::loadSignaturePublicKey($file_public); - $this->assertTrue( - $load_public instanceof SignaturePublicKey - ); + $this->assertInstanceOf(SignaturePublicKey::class, $load_public); $this->assertTrue( \hash_equals($sign_public->getRawKeyMaterial(), $load_public->getRawKeyMaterial()) ); @@ -405,6 +425,9 @@ public function testSignKeyStorage() */ public function testInvalidKeyLevels() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } try { KeyFactory::deriveEncryptionKey( new HiddenString('apple'), @@ -428,6 +451,9 @@ public function testInvalidKeyLevels() */ public function testKeyLevels() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = KeyFactory::deriveEncryptionKey( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", @@ -457,6 +483,9 @@ public function testKeyLevels() */ public function testKeyLevelsOldArgon2i() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = KeyFactory::deriveEncryptionKey( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", @@ -489,37 +518,37 @@ public function testInvalidSizes() new \ParagonIE\Halite\Symmetric\AuthenticationKey(new HiddenString('')); $this->fail('Invalid key size accepted'); } catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) { - $this->assertSame('Authentication key must be CRYPTO_AUTH_KEYBYTES bytes long', $ex->getMessage()); + $this->assertSame('Authentication key must be CRYPTO_AUTH_KEYBYTES (32) bytes long', $ex->getMessage()); } try { new \ParagonIE\Halite\Symmetric\EncryptionKey(new HiddenString('')); $this->fail('Invalid key size accepted'); } catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) { - $this->assertSame('Encryption key must be CRYPTO_STREAM_KEYBYTES bytes long', $ex->getMessage()); + $this->assertSame('Encryption key must be CRYPTO_STREAM_KEYBYTES (32) bytes long', $ex->getMessage()); } try { new \ParagonIE\Halite\Asymmetric\EncryptionSecretKey(new HiddenString('')); $this->fail('Invalid key size accepted'); } catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) { - $this->assertSame('Encryption secret key must be CRYPTO_BOX_SECRETKEYBYTES bytes long', $ex->getMessage()); + $this->assertSame('Encryption secret key must be CRYPTO_BOX_SECRETKEYBYTES (32) bytes long', $ex->getMessage()); } try { new \ParagonIE\Halite\Asymmetric\EncryptionPublicKey(new HiddenString('')); $this->fail('Invalid key size accepted'); } catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) { - $this->assertSame('Encryption public key must be CRYPTO_BOX_PUBLICKEYBYTES bytes long', $ex->getMessage()); + $this->assertSame('Encryption public key must be CRYPTO_BOX_PUBLICKEYBYTES (32) bytes long', $ex->getMessage()); } try { new \ParagonIE\Halite\Asymmetric\SignatureSecretKey(new HiddenString('')); $this->fail('Invalid key size accepted'); } catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) { - $this->assertSame('Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES bytes long', $ex->getMessage()); + $this->assertSame('Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES (64) bytes long', $ex->getMessage()); } try { new \ParagonIE\Halite\Asymmetric\SignaturePublicKey(new HiddenString('')); $this->fail('Invalid key size accepted'); } catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) { - $this->assertSame('Signature public key must be CRYPTO_SIGN_PUBLICKEYBYTES bytes long', $ex->getMessage()); + $this->assertSame('Signature public key must be CRYPTO_SIGN_PUBLICKEYBYTES (32) bytes long', $ex->getMessage()); } } } diff --git a/test/unit/PasswordTest.php b/test/unit/PasswordTest.php index ec2ee2bb..c9edc2f1 100644 --- a/test/unit/PasswordTest.php +++ b/test/unit/PasswordTest.php @@ -22,10 +22,13 @@ final class PasswordTest extends TestCase */ public function testEncrypt() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = new EncryptionKey(new HiddenString(str_repeat('A', 32))); $hash = Password::hash(new HiddenString('test password'), $key); - $this->assertTrue(is_string($hash)); + $this->assertIsString($hash); $this->assertTrue( Password::verify( @@ -56,6 +59,9 @@ public function testEncrypt() */ public function testEncryptWithAd() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = new EncryptionKey(new HiddenString(str_repeat('A', 32))); $aad = '{"userid":12}'; @@ -65,7 +71,7 @@ public function testEncryptWithAd() KeyFactory::INTERACTIVE, $aad ); - $this->assertTrue(is_string($hash)); + $this->assertIsString($hash); $this->assertTrue( Password::verify( @@ -130,13 +136,16 @@ public function testEncryptWithAd() */ public function testKeyLevels() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = new EncryptionKey(new HiddenString(str_repeat('A', 32))); $aad = '{"userid":12}'; $passwd = new HiddenString('test password'); foreach ([KeyFactory::INTERACTIVE, KeyFactory::MODERATE, KeyFactory::SENSITIVE] as $level) { $hash = Password::hash($passwd, $key, $level, $aad); - $this->assertTrue(is_string($hash)); + $this->assertIsString($hash); $this->assertFalse(Password::needsRehash($hash, $key, $level, $aad)); $this->assertTrue(Password::verify($passwd, $hash, $key, $aad)); } @@ -153,15 +162,18 @@ public function testKeyLevels() */ public function testRehash() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = new EncryptionKey(new HiddenString(str_repeat('A', 32))); try { // Sorry version 1, you get no love from us anymore. - $legacyHash = 'MUIDAPHyUoOjV7zXTOF7nPRJP5KQTw_xOge4F9ytBnm_nqz-oKQ-yjxMRhrRLdM0X' . - '4HrEop9vppxhM6GPnwws9khtStJaQvrU2M6QDjA4VraKkVLMHRkTbLyYGppCbfNYy9iaxsKHaV4' . - 'u9j5NSo3OTiRqiz8WHKLBrQ2ETMfd8iSIaHi1u7NXgT6zTvA8mwRa3a5SrWtHw8fEfVoSt47xTy' . - 'SLnKtpUTU_YoudA4vchbPh05YqexJKmV9PAEtTORzLN3eRiucIixaEJrm4T6rLRrqjMaaOCbUu8' . - 'oPyA=='; + $legacyHash = 'MUIEAM8F9xoJSz0yBWtA8_DWq0tJM7RuTYPxehbgJ-CW0e-TnJz3-TrZI1ID8gujH' . + '5pQNzejQZEeMwaWlbIgHbpz0OUrITw5Urlv-_RxI4Ih-80uXieWfq0cOp9QqnX9uCO56OsczuPL' . + '5nDCUcTfnG-GnfvH6FkINGBLMkWfzUzaEBNS1zJVcszqle5GEAp6rm9S-BwnCmbKgdigq2rw-Lu' . + 'N_lfcC4Gijx88EwW4D7L7B3r4zyVh4eFjsaU6Djqv5XIxKvH1gJPUToE_Hukd-5dV4wOI9PKtUL' . + 'ZG0w=='; Password::needsRehash($legacyHash, $key); } catch (InvalidMessage $ex) { $this->assertSame( @@ -170,7 +182,7 @@ public function testRehash() ); } try { - $legacyHash = 'MUIDAPHyUoOjV7zXTOF7nPRJP5KQTw_xOge4F9ytBnm_nqz-oKQ-yjxMRhrRLdM0X' . + $legacyHash = 'MUIEAPHyUoOjV7zXTOF7nPRJP5KQTw_xOge4F9ytBnm_nqz-oKQ-yjxMRhrRLdM0X' . 'oPyB=='; Password::needsRehash($legacyHash, $key); } catch (InvalidMessage $ex) { @@ -180,7 +192,7 @@ public function testRehash() ); } try { - $legacyHash = 'MUIEAPH'; + $legacyHash = 'MUIFAPH'; Password::needsRehash($legacyHash, $key); } catch (InvalidMessage $ex) { $this->assertSame( @@ -190,11 +202,11 @@ public function testRehash() } try { - $legacyHash = 'MUIDAPHyUoOjV7zXTOF7nPRJP5KQTw_xOge4F9ytBnm_nqz-oKQ-yjxMRhrRLdM0X' . - '4HrEop9vppxhM6GPnwws9khtStJaQvrU2M6QDjA4VraKkVLMHRkTbLyYGppCbfNYy9iaxsKHaV4' . - 'u9j5NSo3OTiRqiz8WHKLBrQ2ETMfd8iSIaHi1u7NXgT6zTvA8mwRa3a5SrWtHw8fEfVoSt47xTy' . - 'SLnKtpUTU_YoudA4vchbPh05YqexJKmV9PAEtTORzLN3eRiucIixaEJrm4T6rLRrqjMaaOCbUu8' . - 'oPyB=='; + $legacyHash = 'MUIFAM8F9xoJSz0yBWtA8_DWq0tJM7RuTYPxehbgJ-CW0e-TnJz3-TrZI1ID8gujH' . + '5pQNzejQZEeMwaWlbIgHbpz0OUrITw5Urlv-_RxI4Ih-80uXieWfq0cOp9QqnX9uCO56OsczuPL' . + '5nDCUcTfnG-GnfvH6FkINGBLMkWfzUzaEBNS1zJVcszqle5GEAp6rm9S-BwnCmbKgdigq2rw-Lu' . + 'N_lfcC4Gijx88EwW4D7L7B3r4zyVh4eFjsaU6Djqv5XIxKvH1gJPUToE_Hukd-5dV4wOI9PKtUL' . + 'ZG0w=='; Password::needsRehash($legacyHash, $key); } catch (InvalidMessage $ex) { $this->assertSame( @@ -203,16 +215,6 @@ public function testRehash() ); } - $legacyHash = 'MUIDAPHyUoOjV7zXTOF7nPRJP5KQTw_xOge4F9ytBnm_nqz-oKQ-yjxMRhrRLdM0X' . - '4HrEop9vppxhM6GPnwws9khtStJaQvrU2M6QDjA4VraKkVLMHRkTbLyYGppCbfNYy9iaxsKHaV4' . - 'u9j5NSo3OTiRqiz8WHKLBrQ2ETMfd8iSIaHi1u7NXgT6zTvA8mwRa3a5SrWtHw8fEfVoSt47xTy' . - 'SLnKtpUTU_YoudA4vchbPh05YqexJKmV9PAEtTORzLN3eRiucIixaEJrm4T6rLRrqjMaaOCbUu8' . - 'oPyA=='; - $this->assertTrue( - Password::verify(new HiddenString('test'), $legacyHash, $key), - 'Legacy password hash calculation.' - ); - $hash = Password::hash(new HiddenString('test password'), $key); $this->assertFalse( Password::needsRehash($hash, $key), diff --git a/test/unit/RemoteStream.php b/test/unit/RemoteStream.php new file mode 100644 index 00000000..00b18278 --- /dev/null +++ b/test/unit/RemoteStream.php @@ -0,0 +1,79 @@ +contents = \file_get_contents(__DIR__ . '/tmp/' . parse_url($path, PHP_URL_HOST)); + return true; + } + + function stream_read($count) + { + $return = \substr($this->contents, $this->position, $count); + $this->position += strlen($return); + return $return; + } + + function stream_write($data) + { + return false; + } + + function stream_tell() + { + return $this->position; + } + + function stream_eof() + { + return $this->position >= \strlen($this->contents); + } + + function stream_seek($offset, $whence) + { + switch ($whence) { + case SEEK_SET: + if ($offset < strlen($this->contents) && $offset >= 0) { + $this->position = $offset; + return true; + } + return false; + + case SEEK_CUR: + if ($offset >= 0) { + $this->position += $offset; + return true; + } + return false; + + case SEEK_END: + if (strlen($this->contents) + $offset >= 0) { + $this->position = strlen($this->contents) + $offset; + return true; + } + return false; + + default: + return false; + } + } + + function stream_metadata($path, $option, $var) + { + return false; + } + + function stream_stat() + { + return false; + } + +} diff --git a/test/unit/StreamTest.php b/test/unit/StreamTest.php index dfe54c6f..499e7b31 100644 --- a/test/unit/StreamTest.php +++ b/test/unit/StreamTest.php @@ -20,7 +20,7 @@ final class StreamTest extends TestCase */ public function testFileHash() { - $filename = tempnam('/tmp', 'x'); + $filename = @tempnam('/tmp', 'x'); $buf = random_bytes(65537); file_put_contents($filename, $buf); @@ -46,13 +46,20 @@ public function testFileHash() */ public function testUnreadableFile() { - $filename = tempnam('/tmp', 'x'); + $filename = @tempnam('/tmp', 'x'); $buf = random_bytes(65537); file_put_contents($filename, $buf); chmod($filename, 0000); + $perms = fileperms($filename); + if (!is_int($perms) || ($perms & 0777) !== 0 || is_readable($filename)) { + $this->markTestSkipped('chmod failed to remove read access, so the test will fail; skipping'); + } try { new ReadOnlyFile($filename); + if (DIRECTORY_SEPARATOR === '\\') { + $this->markTestSkipped('Windows permissions are weird.'); + } $this->fail('File should not be readable'); } catch (CryptoException\FileAccessDenied $ex) { $this->assertSame('Could not open file for reading', $ex->getMessage()); @@ -90,7 +97,7 @@ public function testUnreadableFile() */ public function testResource() { - $filename = tempnam('/tmp', 'x'); + $filename = @tempnam('/tmp', 'x'); $buf = random_bytes(65537); file_put_contents($filename, $buf); @@ -142,7 +149,7 @@ public function testResource() */ public function testFileRead() { - $filename = tempnam('/tmp', 'x'); + $filename = @tempnam('/tmp', 'x'); $buf = random_bytes(65537); file_put_contents($filename, $buf); @@ -175,9 +182,7 @@ public function testFileRead() $fStream->readBytes(65537); $this->fail('File was mutated after being read'); } catch (CryptoException\FileModified $ex) { - $this->assertTrue( - $ex instanceof CryptoException\FileModified - ); + $this->assertInstanceOf(CryptoException\FileModified::class, $ex); } $fStream = new ReadOnlyFile($filename); @@ -189,7 +194,7 @@ public function testFileRead() foreach ([255, 65537] as $size) { $buffer = random_bytes($size); - $fileWrite = tempnam('/tmp', 'x'); + $fileWrite = @tempnam('/tmp', 'x'); $mStream = new MutableFile($fileWrite); $mStream->writeBytes($buffer); $mStream->reset(0); @@ -205,4 +210,51 @@ public function testFileRead() $this->assertSame(bin2hex($buffer), bin2hex($mStream->readBytes($size))); } } + + + public function testMutableFileResource() + { + $fp = fopen('php://temp', 'w+b'); + $mStream = new MutableFile($fp); + $mStream->writeBytes('test'); + $mStream->reset(); + $this->assertSame('test', $mStream->readBytes(4)); + } + + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\InvalidType + */ + public function testWriteBytesNull(): void + { + $mStream = new MutableFile(fopen('php://temp', 'w+b')); + $this->assertSame(4, $mStream->writeBytes('test', null)); + } + + /** + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\FileError + * @throws CryptoException\InvalidType + * @throws SodiumException + */ + public function testReadOnlyFileResource(): void + { + $fp = fopen('php://temp', 'rb'); + $rStream = new ReadOnlyFile($fp); + $this->assertInstanceOf(ReadOnlyFile::class, $rStream); + } + + /** + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\FileError + * @throws CryptoException\InvalidType + * @throws SodiumException + */ + public function testReadOnlyFileWriteBytes(): void + { + $this->expectException(CryptoException\FileAccessDenied::class); + $rStream = new ReadOnlyFile(fopen('php://temp', 'rb')); + $rStream->writeBytes('test'); + } } diff --git a/test/unit/SymmetricTest.php b/test/unit/SymmetricTest.php index 2939b2b4..1b33278b 100644 --- a/test/unit/SymmetricTest.php +++ b/test/unit/SymmetricTest.php @@ -229,7 +229,7 @@ public function testEncryptFail() 'This should have thrown an InvalidMessage exception!' ); } catch (CryptoException\InvalidMessage $e) { - $this->assertTrue($e instanceof CryptoException\InvalidMessage); + $this->assertInstanceOf(CryptoException\InvalidMessage::class, $e); } } @@ -262,21 +262,49 @@ public function testUnpack() $this->assertSame(Binary::safeStrlen($unpacked[0]), Halite::VERSION_TAG_LEN); $this->assertTrue($unpacked[1] instanceof Config); $config = $unpacked[1]; - if ($config instanceof Config) { - $this->assertSame(Binary::safeStrlen($unpacked[2]), $config->HKDF_SALT_LEN); - $this->assertSame(Binary::safeStrlen($unpacked[3]), SODIUM_CRYPTO_STREAM_NONCEBYTES); - $this->assertSame( - Binary::safeStrlen($unpacked[4]), - Binary::safeStrlen($message) - ( - Halite::VERSION_TAG_LEN + - $config->HKDF_SALT_LEN + - SODIUM_CRYPTO_STREAM_NONCEBYTES + - $config->MAC_SIZE - ) - ); - $this->assertSame(Binary::safeStrlen($unpacked[5]), $config->MAC_SIZE); - } else { - $this->fail('Cannot continue'); + $this->assertSame(Binary::safeStrlen($unpacked[2]), $config->HKDF_SALT_LEN); + $this->assertSame(Binary::safeStrlen($unpacked[3]), SODIUM_CRYPTO_STREAM_NONCEBYTES); + $this->assertSame( + Binary::safeStrlen($unpacked[4]), + Binary::safeStrlen($message) - ( + Halite::VERSION_TAG_LEN + + $config->HKDF_SALT_LEN + + SODIUM_CRYPTO_STREAM_NONCEBYTES + + $config->MAC_SIZE + ) + ); + $this->assertSame(Binary::safeStrlen($unpacked[5]), $config->MAC_SIZE); + } + + /** + * @throws CryptoException\InvalidKey + * @throws CryptoException\InvalidType + * @throws CryptoException\InvalidMessage + * @throws SodiumException + */ + public function testInvalidMac(): void + { + $key = new AuthenticationKey(new HiddenString(str_repeat('A', 32))); + try { + Symmetric::verify('test', $key, 'invalid'); + $this->fail('Invalid MAC was accepted'); + } catch (CryptoException\InvalidSignature $ex) { + $this->assertInstanceOf(CryptoException\InvalidSignature::class, $ex); } } + + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\InvalidDigestLength + * @throws CryptoException\InvalidKey + * @throws CryptoException\InvalidMessage + * @throws CryptoException\InvalidSignature + * @throws CryptoException\InvalidType + * @throws SodiumException + */ + public function testInvalidEncodedCiphertext(): void + { + $this->expectException(CryptoException\InvalidMessage::class); + Symmetric::decrypt('invalid', new EncryptionKey(new HiddenString(str_repeat('A', 32)))); + } } diff --git a/test/unit/UtilTest.php b/test/unit/UtilTest.php index efe94082..90bcf4f9 100644 --- a/test/unit/UtilTest.php +++ b/test/unit/UtilTest.php @@ -13,7 +13,7 @@ * @category HaliteTest * @package Halite * @author Stefanie Schmidt - * @license http://opensource.org/licenses/GPL-3.0 GPL 3 + * @license https://opensource.org/license/GPL-3.0 GPL 3 * @link https://paragonie.com/project/halite */ final class UtilTest extends TestCase @@ -34,6 +34,9 @@ public function testChrToInt() $random, Util::chrToInt(Util::intToChr($random)) ); + + $this->expectException(\RangeException::class); + Util::chrToInt("ab"); } public function testIntArrayToString() @@ -161,6 +164,9 @@ public function testBlake2bKDF() */ public function testSafeStrcpy() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $unique = random_bytes(128); $clone = Util::safeStrcpy($unique); $this->assertSame($unique, $clone);