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 index e2b36363..3081e765 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,67 +1,38 @@ name: CI -on: [push, pull_request] +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] jobs: - old: + 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: ['7.2', '7.3'] - phpunit-versions: ['latest'] - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - extensions: mbstring, intl, sodium - ini-values: post_max_size=256M, max_execution_time=180 - tools: psalm, phpunit:${{ matrix.phpunit-versions }} - - - name: Install dependencies - run: composer install + php-versions: ['8.1', '8.2', '8.3', '8.4', '8.5'] - - name: PHPUnit tests - uses: php-actions/phpunit@v2 - timeout-minutes: 30 - with: - memory_limit: 256M - - name: Static Analysis - run: vendor/bin/psalm - - modern: - name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} - runs-on: ${{ matrix.operating-system }} - strategy: - matrix: - operating-system: ['ubuntu-latest'] - php-versions: ['7.4', '8.0'] - phpunit-versions: ['latest'] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} extensions: mbstring, intl, sodium - ini-values: post_max_size=256M, max_execution_time=180 - tools: psalm, phpunit:${{ matrix.phpunit-versions }} + ini-values: error_reporting=-1, display_errors=On + coverage: none - - name: Install dependencies - run: composer install + - name: Install Composer dependencies + uses: "ramsey/composer-install@v3" - name: PHPUnit tests - uses: php-actions/phpunit@v2 - timeout-minutes: 30 - with: - memory_limit: 256M + run: vendor/bin/phpunit - - name: Static Analysis - run: vendor/bin/psalm + - 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..1126975b --- /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', '8.5'] + + 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 b0f83d82..00000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: php -sudo: required -dist: trusty -php: - - "7.2" - - "7.3" - - "7.4" - - "8.0" - - "master" - - "nightly" -matrix: - fast_finish: true - allow_failures: - - php: "8.0" - - 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 72579df8..132cee21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,59 @@ # 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 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 c935fc00..bd95e4c1 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # 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,11 +61,11 @@ 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 @@ -75,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 fa038512..642d846d 100644 --- a/composer.json +++ b/composer.json @@ -32,10 +32,11 @@ } ], "require": { - "php": "^7.2|^8", - "paragonie/constant_time_encoding": "^2", + "php": "^8.1", + "ext-json": "*", + "paragonie/constant_time_encoding": "^2|^3", "paragonie/hidden-string": "^1|^2", - "paragonie/sodium_compat": "^1.15" + "paragonie/sodium_compat": "^1|^2" }, "autoload": { "psr-4": { @@ -43,11 +44,12 @@ } }, "require-dev": { - "phpunit/phpunit": "^8|^9", - "vimeo/psalm": "^3|^4" + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9", + "vimeo/psalm": "^6.8" }, "scripts": { - "test": "phpunit && psalm" + "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 39724878..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. @@ -67,7 +71,7 @@ Later, you can load it like so: $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(); @@ -94,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). @@ -113,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 @@ -131,8 +186,6 @@ $send_to_bob = sodium_bin2hex($alice_public->getRawKeyMaterial()); Alice will then load Bob's public key into the appropriate object like so: ```php -use ParagonIE\HiddenString\HiddenString; - $bob_public = new \ParagonIE\Halite\Asymmetric\EncryptionPublicKey( new HiddenString( sodium_hex2bin($recv_from_bob) @@ -168,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 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/Primitives.md b/doc/Primitives.md index 5873dbd6..0fabd1db 100644 --- a/doc/Primitives.md +++ b/doc/Primitives.md @@ -1,14 +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](https://moxie.org/2011/12/13/the-cryptographic-doom-principle.html). +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 7b78565c..18f41084 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,6 @@ - + ./src @@ -13,7 +13,7 @@ ./doc - + diff --git a/psalm.xml b/psalm.xml index 69c5b316..7fed6175 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,7 +1,7 @@ +> @@ -10,8 +10,24 @@ + + + + + + + + + + + + + + + + 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 5f05d73f..2740ff0e 100644 --- a/src/Asymmetric/Crypto.php +++ b/src/Asymmetric/Crypto.php @@ -19,6 +19,22 @@ 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 @@ -28,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'); } /** @@ -64,16 +80,18 @@ final private function __construct() * @throws InvalidMessage * @throws InvalidDigestLength * @throws InvalidType - * @throws \SodiumException - * @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, @@ -90,6 +108,7 @@ public static function encrypt( * @param EncryptionPublicKey $theirPublicKey * @param string $additionalData * @param string|bool $encoding + * * @return string * * @throws CannotPerformOperation @@ -97,23 +116,28 @@ public static function encrypt( * @throws InvalidMessage * @throws InvalidDigestLength * @throws InvalidType - * @throws \SodiumException - * @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, @@ -139,16 +163,17 @@ public static function encryptWithAd( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \SodiumException - * @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, @@ -165,6 +190,7 @@ public static function decrypt( * @param EncryptionPublicKey $theirPublicKey * @param string $additionalData * @param string|bool $encoding + * * @return HiddenString * * @throws CannotPerformOperation @@ -173,23 +199,27 @@ public static function decrypt( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \SodiumException - * @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, @@ -208,33 +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 \SodiumException - * @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; } /** @@ -242,19 +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 \SodiumException - * @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() ); @@ -270,19 +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 \SodiumException - * @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() ); @@ -300,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 @@ -307,14 +361,15 @@ public static function sign( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException - * @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(); @@ -338,19 +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 \SodiumException - * @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) { @@ -358,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' ); @@ -367,8 +424,8 @@ 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 ); @@ -378,14 +435,14 @@ public static function unseal( 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 Util::memzero($key_pair); - if (!\is_string($message)) { + if (!is_string($message)) { // @codeCoverageIgnoreStart throw new InvalidKey( 'Incorrect secret key for this sealed message' @@ -403,19 +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 \SodiumException - * @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) { @@ -431,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() @@ -445,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 @@ -453,14 +512,15 @@ public static function verify( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \SodiumException - * @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(); @@ -478,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 f3a0dbce..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' @@ -70,31 +76,37 @@ public function __debugInfo() * 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,7 +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)) { + if (hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX)) { $decoded = Base64UrlSafe::decode($stored); return SymmetricConfig::getConfig( $decoded, @@ -139,20 +151,24 @@ 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) + * @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 InvalidArgument PHP version incompatibilities * @psalm-suppress MixedArgument */ public function store( + #[\SensitiveParameter] string $name, + #[\SensitiveParameter] $value, int $expire = 0, string $path = '/', @@ -163,34 +179,24 @@ public function store( ): bool { $val = Crypto::encrypt( new HiddenString( - (string) \json_encode($value) + (string) json_encode($value) ), $this->key ); - if (\version_compare(PHP_VERSION, '7.3.0') >= 0) { - $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); + $options = [ + 'expires' => (int) $expire, + 'path' => (string) $path, + 'domain' => (string) $domain, + 'secure' => (bool) $secure, + 'httponly' => (bool) $httpOnly, + ]; + if ($sameSite !== '') { + $options['samesite'] = (string) $sameSite; } - return \setcookie( + return setcookie( $name, $val, - (int) $expire, - (string) $path, - (string) $domain, - (bool) $secure, - (bool) $httpOnly + $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 9e8b5cd5..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 */ private function __construct() { - throw new \Error('Do not instantiate'); + throw new Error('Do not instantiate'); } /** @@ -61,10 +86,10 @@ 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 @@ 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 ); @@ -528,14 +636,14 @@ protected static function checksumData( // @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 @@ -543,7 +651,7 @@ protected static function checksumData( ) ); } - return (string) \sodium_crypto_generichash_final( + return (string) sodium_crypto_generichash_final( // @codeCoverageIgnoreStart $state, // @codeCoverageIgnoreEnd @@ -555,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 @@ -565,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( @@ -595,7 +707,7 @@ protected static function encryptData( ); $output->writeBytes( $firstNonce, - \SODIUM_CRYPTO_STREAM_NONCEBYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES ); $output->writeBytes( $hkdfSalt, @@ -603,11 +715,33 @@ 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'); + } Util::memzero($authKey); Util::memzero($hkdfSalt); @@ -618,7 +752,7 @@ protected static function encryptData( new EncryptionKey( new HiddenString($encKey) ), - (string) $firstNonce, + $firstNonce, (string) $mac, $config ); @@ -630,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 @@ -640,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); @@ -660,6 +796,7 @@ protected static function decryptData( $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? @@ -674,15 +811,39 @@ protected static function decryptData( $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); Util::memzero($authKey); @@ -717,6 +878,7 @@ protected static function decryptData( * @param ReadOnlyFile $input * @param MutableFile $output * @param EncryptionPublicKey $publicKey + * @param ?string $aad * @return int * * @throws CannotPerformOperation @@ -725,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(); @@ -740,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 @@ -754,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( @@ -777,7 +945,7 @@ protected static function sealData( ); $output->writeBytes( $ephPublic->getRawKeyMaterial(), - \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES + SODIUM_CRYPTO_BOX_PUBLICKEYBYTES ); $output->writeBytes( $hkdfSalt, @@ -785,14 +953,33 @@ protected static function sealData( ); // VERSION 2+ - $mac = \sodium_crypto_generichash_init($authKey); - - // We no longer need $authKey after we set up the hash context + $mac = sodium_crypto_generichash_init($authKey); Util::memzero($authKey); - - \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); - \sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + 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); Util::memzero($hkdfSalt); @@ -819,6 +1006,7 @@ protected static function sealData( * @param ReadOnlyFile $input * @param MutableFile $output * @param EncryptionSecretKey $secretKey + * @param ?string $aad * @return bool * * @throws CannotPerformOperation @@ -829,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(); @@ -862,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: @@ -876,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); @@ -889,17 +1080,41 @@ 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: @@ -932,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 @@ -941,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, @@ -966,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 * @@ -977,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( @@ -1007,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) ); } @@ -1046,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 @@ -1089,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 [ @@ -1097,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' @@ -1133,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 ]; } } @@ -1150,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 * @@ -1200,8 +1390,8 @@ protected static function splitKeys( * @throws CannotPerformOperation * @throws FileAccessDenied * @throws FileModified - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ private static function streamEncrypt( ReadOnlyFile $input, @@ -1222,21 +1412,29 @@ 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)) { + if (is_string($nonce)) { Util::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' @@ -1244,7 +1442,7 @@ 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; @@ -1259,7 +1457,7 @@ 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 * @@ -1268,8 +1466,8 @@ private static function streamEncrypt( * @throws FileAccessDenied * @throws FileModified * @throws InvalidMessage - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ private static function streamDecrypt( ReadOnlyFile $input, @@ -1301,10 +1499,12 @@ 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 @@ -1314,9 +1514,8 @@ 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( @@ -1327,15 +1526,23 @@ 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)) { + if (is_string($nonce)) { Util::memzero($nonce); } return true; @@ -1345,21 +1552,21 @@ 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 */ private static function streamVerify( ReadOnlyFile $input, - $mac, + string $mac, Config $config ): array { $start = $input->getPos(); @@ -1374,7 +1581,6 @@ 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. @@ -1391,11 +1597,13 @@ 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 $chunkMAC = Util::safeStrcpy($mac); - $chunkMACs []= \sodium_crypto_generichash_final( + $chunkMACs []= sodium_crypto_generichash_final( // @codeCoverageIgnoreStart $chunkMAC, // @codeCoverageIgnoreEnd @@ -1406,7 +1614,7 @@ 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 @@ -1416,7 +1624,7 @@ 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 9bb37afe..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,31 +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, @@ -88,7 +97,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_BASE32) { - return \implode( + return implode( '::', [ Base32::class, @@ -96,7 +105,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_BASE32HEX) { - return \implode( + return implode( '::', [ Base32Hex::class, @@ -104,7 +113,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_BASE64) { - return \implode( + return implode( '::', [ Base64::class, @@ -112,7 +121,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_BASE64URLSAFE) { - return \implode( + return implode( '::', [ Base64UrlSafe::class, @@ -120,7 +129,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_HEX) { - return \implode( + return implode( '::', [ Hex::class, @@ -143,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"; } @@ -151,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 dbe62ce3..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 @@ -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 9d3d72a6..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,23 +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 \SodiumException + * @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 Util::memzero($kp); return new EncryptionKeyPair( new EncryptionSecretKey( - new HiddenString($secretKey) + new HiddenString($secretKey), + new HiddenString($publicKey) ) ); } @@ -117,21 +154,25 @@ public static function generateEncryptionKeyPair(): EncryptionKeyPair * Generate a key pair for public key digital signatures * * @return SignatureKeyPair + * + * @throws CannotPerformOperation * @throws InvalidKey - * @throws \SodiumException - * @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 Util::memzero($kp); return new SignatureKeyPair( new SignatureSecretKey( - new HiddenString($secretKey) + new HiddenString($secretKey), + new HiddenString($publicKey) ) ); } @@ -150,8 +191,8 @@ public static function generateSignatureKeyPair(): SignatureKeyPair * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function deriveAuthenticationKey( HiddenString $password, @@ -161,15 +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 } - $secretKey = @\sodium_crypto_pwhash( - \SODIUM_CRYPTO_AUTH_KEYBYTES, + $secretKey = sodium_crypto_pwhash( + SODIUM_CRYPTO_AUTH_KEYBYTES, $password->getString(), $salt, $kdfLimits[0], @@ -192,11 +233,12 @@ public static function deriveAuthenticationKey( * (You can safely use the default) * * @return EncryptionKey + * * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function deriveEncryptionKey( HiddenString $password, @@ -206,15 +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 } - $secretKey = \sodium_crypto_pwhash( - \SODIUM_CRYPTO_STREAM_KEYBYTES, + $secretKey = sodium_crypto_pwhash( + SODIUM_CRYPTO_STREAM_KEYBYTES, $password->getString(), $salt, $kdfLimits[0], @@ -240,8 +282,8 @@ public static function deriveEncryptionKey( * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function deriveEncryptionKeyPair( HiddenString $password, @@ -251,30 +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 - $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 Util::memzero($keyPair); return new EncryptionKeyPair( new EncryptionSecretKey( - new HiddenString($secretKey) + new HiddenString($secretKey), + new HiddenString($publicKey) ) ); } @@ -290,11 +334,11 @@ public static function deriveEncryptionKeyPair( * * @return SignatureKeyPair * + * @throws CannotPerformOperation * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException */ public static function deriveSignatureKeyPair( HiddenString $password, @@ -304,30 +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 - $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 Util::memzero($keyPair); return new SignatureKeyPair( new SignatureSecretKey( - new HiddenString($secretKey) + new HiddenString($secretKey), + new HiddenString($publicKey) ) ); } @@ -337,7 +383,9 @@ public static function deriveSignatureKeyPair( * * @param string $level * @param int $alg + * * @return int[] + * * @throws InvalidType * @codeCoverageIgnore */ @@ -352,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) { @@ -361,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) { @@ -370,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( @@ -384,11 +432,12 @@ public static function getSecurityLevels( * Load a symmetric authentication key from a string * * @param HiddenString $keyData + * * @return AuthenticationKey * * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importAuthenticationKey(HiddenString $keyData): AuthenticationKey { @@ -405,11 +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 \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importEncryptionKey(HiddenString $keyData): EncryptionKey { @@ -426,11 +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 \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importEncryptionPublicKey(HiddenString $keyData): EncryptionPublicKey { @@ -447,11 +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 \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importEncryptionSecretKey(HiddenString $keyData): EncryptionSecretKey { @@ -468,11 +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 \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importSignaturePublicKey(HiddenString $keyData): SignaturePublicKey { @@ -489,11 +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 \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importSignatureSecretKey(HiddenString $keyData): SignatureSecretKey { @@ -510,11 +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 \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importEncryptionKeyPair(HiddenString $keyData): EncryptionKeyPair { @@ -535,9 +590,10 @@ public static function importEncryptionKeyPair(HiddenString $keyData): Encryptio * @param HiddenString $keyData * @return SignatureKeyPair * + * @throws CannotPerformOperation * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importSignatureKeyPair(HiddenString $keyData): SignatureKeyPair { @@ -556,17 +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 \SodiumException - * @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 ); @@ -580,17 +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 \SodiumException - * @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 ); @@ -604,17 +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 \SodiumException - * @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 ); @@ -628,17 +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 \SodiumException - * @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 ); @@ -652,17 +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 \SodiumException - * @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 ); @@ -676,17 +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 \SodiumException - * @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 ); @@ -700,17 +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 \SodiumException - * @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 ); @@ -726,17 +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 \SodiumException - * @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 ); @@ -751,33 +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 \SodiumException - * @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 + ) + ) + ); } /** @@ -785,11 +848,13 @@ public static function export($key): HiddenString * * @param Key|KeyPair $key * @param string $filename + * * @return bool - * @throws \SodiumException - * @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( @@ -804,15 +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 \SodiumException - * @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( @@ -832,10 +899,12 @@ protected static function loadKeyFile(string $filePath): HiddenString * checksum) * * @param string $data + * * @return string + * * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function getKeyDataFromString(string $data): string { @@ -843,19 +912,19 @@ 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' @@ -876,24 +945,24 @@ public static function getKeyDataFromString(string $data): string * @param string $keyData * @return bool * - * @throws \SodiumException - * @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 6c0b0b70..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,24 +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 \SodiumException - * @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] @@ -83,64 +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 \SodiumException - * @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 Argon2id - if (!\hash_equals( + 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, + }; } /** @@ -160,17 +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) ) { $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'); @@ -184,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 @@ -191,13 +204,17 @@ protected static function getConfig(string $stored): SymmetricConfig * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException - * @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); @@ -207,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 acdcb9b6..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,13 +38,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\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 { @@ -34,7 +54,7 @@ class MutableFile implements StreamInterface /** * @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' @@ -112,14 +133,16 @@ 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(); } } @@ -138,7 +161,7 @@ public function __destruct() */ public function getPos(): int { - return \ftell($this->fp); + return ftell($this->fp); } /** @@ -148,7 +171,7 @@ public function getPos(): int */ public function getSize(): int { - $stat = \fstat($this->fp); + $stat = fstat($this->fp); return (int) $stat['size']; } @@ -159,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); } /** @@ -167,7 +190,9 @@ public function getStreamMetadata(): array * * @param int $num * @param bool $skipTests + * * @return string + * * @throws CannotPerformOperation * @throws FileAccessDenied */ @@ -191,10 +216,9 @@ public function readBytes(int $num, bool $skipTests = false): string break; // @codeCoverageIgnoreEnd } - $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' @@ -217,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 @@ -230,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( @@ -250,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 @@ -275,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( @@ -285,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 40e48f89..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' ); @@ -96,16 +99,16 @@ public function __construct($file, Key $key = null) $this->closeAfter = true; $this->pos = 0; $this->stat = $this->fstat(); - } elseif (\is_resource($file)) { + } 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->pos = ftell($this->fp); $this->stat = $this->fstat(); } else { throw new InvalidType( @@ -138,8 +141,8 @@ public function close(): void { if ($this->closeAfter) { $this->closeAfter = false; - \fclose($this->fp); - \clearstatcache(); + fclose($this->fp); + clearstatcache(); } } @@ -147,7 +150,8 @@ public function close(): void * Calculate a BLAKE2b hash of a file * * @return string - * @throws \SodiumException + * + * @throws SodiumException * @throws FileModified * @throws FileError */ @@ -158,29 +162,29 @@ public function getHash(): string 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); } /** @@ -210,7 +214,7 @@ public function getSize(): int */ public function getStreamMetadata(): array { - return \stream_get_meta_data($this->fp); + return stream_get_meta_data($this->fp); } /** @@ -220,10 +224,12 @@ public function getStreamMetadata(): array * 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 @@ -251,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' @@ -286,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 @@ -307,12 +314,13 @@ 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' @@ -331,11 +339,13 @@ public function toctouTest() * 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); @@ -350,7 +360,7 @@ public function writeBytes(string $buf, int $num = null): int * @return array */ private function fstat() : array { - $stat = \fstat($this->fp); + $stat = fstat($this->fp); if ($stat) { return $stat; } @@ -358,11 +368,11 @@ private function fstat() : array { $stat = [ 'size' => 0, ]; - \fseek($this->fp, 0); + fseek($this->fp, 0); while (!feof($this->fp)) { - $stat['size'] += \strlen(\fread($this->fp, 8192)); + $stat['size'] += Binary::safeStrlen(fread($this->fp, self::CHUNK)); } - \fseek($this->fp, $this->pos); + fseek($this->fp, $this->pos); return $stat; } } diff --git a/src/Structure/MerkleTree.php b/src/Structure/MerkleTree.php index 65b3e7a3..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,9 +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 - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ public function getRoot(bool $raw = false): string { @@ -89,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 @@ -108,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 ) ); } @@ -140,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 @@ -155,9 +155,10 @@ public function setPersonalizationString(string $str = ''): self * Explicitly recalculate the Merkle root * * @return self + * * @throws CannotPerformOperation - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException * @codeCoverageIgnore */ public function triggerRootCalculation(): self @@ -172,13 +173,14 @@ public function triggerRootCalculation(): self * to protect against second-preimage attacks * * @return string + * * @throws CannotPerformOperation - * @throws \TypeError - * @throws \SodiumException + * @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 @@ -239,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 a3944505..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 ''; } @@ -84,19 +89,19 @@ protected function calculateRoot(): string // 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 241f3cb0..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,18 +70,19 @@ final private function __construct() * * @param string $message * @param AuthenticationKey $secretKey - * @param mixed $encoding + * @param string|bool $encoding + * * @return string * * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException - * @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, @@ -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,15 +114,15 @@ public static function authenticate( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \SodiumException - * @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, '', @@ -113,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 @@ -124,43 +153,45 @@ public static function decrypt( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \SodiumException - * @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 @@ -168,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' ); } - CryptoUtil::memzero($salt); - CryptoUtil::memzero($authKey); + Util::memzero($salt); + Util::memzero($authKey); // crypto_stream_xor() can be used to encrypt and decrypt - $plaintext = \sodium_crypto_stream_xor( - (string) $encrypted, - (string) $nonce, - (string) $encKey - ); - CryptoUtil::memzero($encrypted); - CryptoUtil::memzero($nonce); - CryptoUtil::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 \SodiumException - * @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, '', @@ -241,33 +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 \SodiumException - * @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 @@ -278,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: - $encrypted = \sodium_crypto_stream_xor( - $plaintext->getString(), - $nonce, - $encKey - ); - CryptoUtil::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 - ); - CryptoUtil::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 - CryptoUtil::memzero($nonce); - CryptoUtil::memzero($salt); - CryptoUtil::memzero($encrypted); - CryptoUtil::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 \SodiumException - * @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; } /** @@ -354,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 @@ -398,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 @@ -407,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 ) ); @@ -424,7 +457,7 @@ public static function unpackMessageForDecryption(string $ciphertext): array ); // We don't need this anymore. - CryptoUtil::memzero($ciphertext); + Util::memzero($ciphertext); // Now we return the pieces in a specific order: return [$version, $config, $salt, $nonce, $encrypted, $auth]; @@ -436,22 +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 \SodiumException - * @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) { @@ -459,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, @@ -486,9 +520,11 @@ public static function verify( * @param string $message * @param string $authKey * @param SymmetricConfig $config + * * @return string + * * @throws InvalidMessage - * @throws \SodiumException + * @throws SodiumException */ protected static function calculateMAC( string $message, @@ -496,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 @@ -513,15 +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 + * @throws SodiumException */ protected static function verifyMAC( string $mac, @@ -537,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); - CryptoUtil::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 16edcb91..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,57 +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 \SodiumException - * @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) @@ -78,26 +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 + * @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. @@ -109,11 +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 \SodiumException + * @throws TypeError + * @throws SodiumException */ public static function hkdfBlake2b( string $ikm, @@ -122,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' ); @@ -130,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' ); @@ -154,21 +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 - $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 @@ -177,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 ); } @@ -191,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); } /** @@ -203,21 +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 \SodiumException + * @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() * @@ -227,32 +278,34 @@ public static function keyed_hash( * @param string $input * @param string $key * @param int $length + * * @return string + * * @throws CannotPerformOperation - * @throws \SodiumException + * @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); } /** @@ -260,8 +313,10 @@ 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 { @@ -277,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 @@ -322,6 +438,9 @@ public static function xorStrings(string $left, string $right): string * Wrap memzero() without breaking on sodium_compat * * @param string &$var + * + * @return void + * * @psalm-param-out null $var * @psalm-suppress UnnecessaryVarAnnotation * @psalm-suppress InvalidOperand @@ -329,11 +448,11 @@ public static function xorStrings(string $left, string $right): string public static function memzero(string &$var): void { try { - \sodium_memzero($var); - } catch (\Throwable $ex) { + sodium_memzero($var); + } catch (Throwable $ex) { // Best-effort: $var ^= $var; } - $var = null; + unset($var); } } diff --git a/test/random_audit.php b/test/random_audit.php index 09ce70ad..d1ae3716 100644 --- a/test/random_audit.php +++ b/test/random_audit.php @@ -38,8 +38,8 @@ function list_all_files(string $folder, string $extension = '*'): array return $fileList; } -if ($argc > 1) { - $extensions = array_slice($argv, 1); +if ($_SERVER['argc'] > 1) { + $extensions = array_slice($_SERVER['argv'], 1); } else { $extensions = ['php', 'twig']; } diff --git a/test/unit/AsymmetricTest.php b/test/unit/AsymmetricTest.php index 76c8489a..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; @@ -186,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); } } @@ -285,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); } } @@ -331,7 +333,7 @@ public function testSignEncrypt() $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.' @@ -374,7 +376,7 @@ public function testSignEncryptFail() $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) . @@ -394,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.' @@ -489,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 2a199088..91fdb36b 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -25,6 +25,82 @@ public function setUp(): void } } + /** + * @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'); + } + /** * @throws CryptoException\CannotPerformOperation * @throws CryptoException\FileAccessDenied @@ -66,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 @@ -152,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) { - } } /** @@ -208,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( @@ -224,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( @@ -315,30 +436,65 @@ 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'); } /** @@ -476,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) { - } } /** @@ -529,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( @@ -634,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) { - } } /** @@ -734,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.'); @@ -785,7 +914,7 @@ public function testOutputToOutputbuffer() ob_start(); File::decrypt( __DIR__.'/tmp/paragon_avatar.encrypted.png', - $stream, + new MutableFile($stream), $key ); $contents = ob_get_clean(); @@ -796,4 +925,48 @@ public function testOutputToOutputbuffer() ); 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/KeyPairTest.php b/test/unit/KeyPairTest.php index 31da70d3..d625cac1 100644 --- a/test/unit/KeyPairTest.php +++ b/test/unit/KeyPairTest.php @@ -37,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'; @@ -83,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'; diff --git a/test/unit/KeyTest.php b/test/unit/KeyTest.php index 3cfeea70..8c24a3aa 100644 --- a/test/unit/KeyTest.php +++ b/test/unit/KeyTest.php @@ -118,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'; @@ -159,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'; @@ -354,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()) ); @@ -403,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()) ); @@ -522,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 dfc1eb75..c9edc2f1 100644 --- a/test/unit/PasswordTest.php +++ b/test/unit/PasswordTest.php @@ -28,7 +28,7 @@ public function testEncrypt() $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( @@ -71,7 +71,7 @@ public function testEncryptWithAd() KeyFactory::INTERACTIVE, $aad ); - $this->assertTrue(is_string($hash)); + $this->assertIsString($hash); $this->assertTrue( Password::verify( @@ -145,7 +145,7 @@ public function testKeyLevels() $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)); } @@ -169,11 +169,11 @@ public function testRehash() 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( @@ -182,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) { @@ -192,7 +192,7 @@ public function testRehash() ); } try { - $legacyHash = 'MUIEAPH'; + $legacyHash = 'MUIFAPH'; Password::needsRehash($legacyHash, $key); } catch (InvalidMessage $ex) { $this->assertSame( @@ -202,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( @@ -215,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/StreamTest.php b/test/unit/StreamTest.php index c433fa72..499e7b31 100644 --- a/test/unit/StreamTest.php +++ b/test/unit/StreamTest.php @@ -53,14 +53,12 @@ public function testUnreadableFile() $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'); - return; } try { new ReadOnlyFile($filename); if (DIRECTORY_SEPARATOR === '\\') { $this->markTestSkipped('Windows permissions are weird.'); - return; } $this->fail('File should not be readable'); } catch (CryptoException\FileAccessDenied $ex) { @@ -184,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); @@ -214,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 685fe4f0..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()