diff --git a/.gitattributes b/.gitattributes index 0e4649d6eb1ff20a12f46b96fb0425a45514d09d..15b8fe602d9bda9a7bb8ea7a6664dad46ce50f81 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,11 +2,10 @@ /doc export-ignore /tests export-ignore -/.coveralls.yml export-ignore -/.scrutinizer.yml export-ignore /.gitattributes export-ignore /.github export-ignore /.gitignore export-ignore -/.travis.yml export-ignore +/ecs.php export-ignore +/rector.php export-ignore /phpunit.xml.dist export-ignore /README.md export-ignore diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 871735ad142f8e851489973c151c5ae0efeb3c01..ecc429e1b72aa2d5f45b3634b1d5b14fea056d99 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,25 +1,16 @@ -# Contributing +# Aux contributeurs -First of all, **thank you** for contributing. +Tout d’abord, **merci** pour votre contribution. -Bugs or feature requests can be posted online on the GitHub issues section of the project. +Les bogues ou les demandes de fonctionnalités peuvent être publiés en ligne dans la section des problèmes GitHub du projet. -Few rules to ease code reviews and merges: +Quelques règles pour faciliter les revues de code et les fusions : -- You MUST follow the [PSR-1](http://www.php-fig.org/psr/psr-1/), [PSR-2](http://www.php-fig.org/psr/psr-2/) and [PSR-4](http://www.php-fig.org/psr/psr-4/) coding standards. -- You MUST run the test suite. -- You MUST write (or update) unit tests when bugs are fixed or features are added. -- You SHOULD write documentation. +- Vous DEVEZ suivre le [PSR-1](http://www.php-fig.org/psr/psr-1/), [PSR-2](http://www.php-fig.org/psr /psr-2/) et [PSR-4](http://www.php-fig.org/psr/psr-4/). +- Vous DEVEZ exécuter la suite de tests. +- Vous DEVEZ écrire (ou mettre à jour) des tests lorsque des bogues sont corrigés ou que des fonctionnalités sont ajoutées. +- Vous DEVRIEZ rédiger de la documentation. -We use [Git-Flow](http://jeffkreeftmeijer.com/2010/why-arent-you-using-git-flow/) to automate our git branching workflow. +[Git-Flow](http://jeffkreeftmeijer.com/2010/why-arent-you-using-git-flow/) est vivement recommandé. -To contribute use [Pull Requests](https://help.github.com/articles/using-pull-requests), please, write commit messages that make sense, and rebase your branch before submitting your PR. - -May be asked to squash your commits too. This is used to "clean" your Pull Request before merging it, avoiding commits such as fix tests, fix 2, fix 3, etc. - -Run test suite ------------- - -* install composer: `curl -s http://getcomposer.org/installer | php` -* install dependencies: `php composer.phar install` -* run tests: `vendor/bin/phpunit` +Merci de mettre à jour votre dépôt avant de soumettre votre Pull Request. diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md new file mode 100644 index 0000000000000000000000000000000000000000..fca7e51ad4167b41c7f40a2ce9c13db4904f8d8e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.md @@ -0,0 +1,21 @@ +--- +name: 🛠Bug Report +about: âš ï¸ For all bug reports except security issues (see below) +labels: bug + +--- + +**Version(s)** : x.y.z + +**Description** +<!-- A clear and concise description of the problem. --> + +**How to reproduce?** +<!-- Code or configuration needed to reproduce the problem. If it is a complex bug, + create a "bug reproducer" --> + +**Possible solution** +<!--- Optional: only if you have any suggestions on a fix/bug reason --> + +**Additional context** +<!-- Optional: any other context regarding the issue: log messages, screenshots, etc. --> diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md new file mode 100644 index 0000000000000000000000000000000000000000..70a8ac072d049bcfc3542b33b0f75ab69a9fcb0a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_Feature_request.md @@ -0,0 +1,13 @@ +--- +name: 🎉 Feature request +about: Ideas for new features and improvements +labels: feature + +--- + +**Description** +<!-- A clear and concise description of the new feature. --> + +**Example** +<!-- A simple example of the new feature in action (include PHP code, YAML configuration, etc.) + If the new feature changes an existing feature, include a before/after comparison. --> \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/3_Documentation_issue.md b/.github/ISSUE_TEMPLATE/3_Documentation_issue.md new file mode 100644 index 0000000000000000000000000000000000000000..387d7c145263108a4dc7552cd4ddbd21d46856f6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3_Documentation_issue.md @@ -0,0 +1,15 @@ +--- +name: 📖 Documentation issue +about: If a typo or section is out of date +labels: documentation + +--- + +**Affected page** +<!-- Indicate the page (link or specific area) that needs to be corrected. --> + +**What is written** +<!-- Copy/Paste from the concerned section. --> + +**What should be written** +<!-- Your proposed correction. --> \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..dde06cf341221f7468a2cd6e426d292df2d61398 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Support Question + url: https://spomky-labs.com/contact/ + about: We use GitHub issues only to discuss about bugs and new features. For this kind of questions about using the library, please use Stackoverflow (or similar) or send a quote request at https://spomky-labs.com/contact/ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index acd0be9fc0307d0c7271ccaee52314d8d9bf29aa..68ac3b4dc0f4b0bbbf3f7a394c2defd3a1b1bbb9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,21 +1,15 @@ -| Q | A -| ------------- | --- -| Branch? | master -| Bug fix? | yes/no -| New feature? | yes/no -| BC breaks? | yes/no -| Deprecations? | yes/no -| Tests pass? | yes/no -| Fixed tickets | #... <!-- #-prefixed issue number(s), if any --> -| License | MIT -| Tests added | <!--highly recommended for new features--> -| Doc PR | <!--highly recommended for new features--> - +| Q | A +| ----------------------- | --- +| Correction de bogues? | oui/non +| Nouvelle fonctionnalité | oui/non +| Dépréciations? | oui/non +| Tickets | Corrige #... <!-- -Fill in this template according to the PR you're about to submit. -Replace this comment by a description of what your PR is solving. +Remplacez cet avis par un court README pour votre fonctionnalité/correction de bogues. Cela aidera les autres à +comprendre votre PR. Cela peut être utilisé comme point de départ pour la documentation. -Please consider the following requirement: -* Modification of existing tests should be avoided unless deemed necessary. -* You MUST never open a PR related to a security issue. Contact Spomky in private at https://gitter.im/Spomky/ +En outre: + - Ajoutez toujours des tests et assurez-vous qu’ils réussissent. + - Ne jamais rompre la rétrocompatibilité, sauf si c’est l’objet de votre PR. + - Votre dépôt doit être à jour. --> diff --git a/.github/stale.yml b/.github/stale.yml index dc90e5a1c3aad4818a813606b52fdecd2fdf6782..98284be67bc99e9f9ca7af7ac4fc2de3790f88dd 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,17 +1,8 @@ -# Number of days of inactivity before an issue becomes stale daysUntilStale: 60 -# Number of days of inactivity before a stale issue is closed daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security -# Label to use when marking an issue as stale staleLabel: wontfix -# Comment to post when marking an issue as stale. Set to `false` to disable markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable + Ce problème a été automatiquement marqué comme périmé car il n'a pas eu + d’activité récente. Il sera fermé dans 7 jours si aucune autre activité ne se produit. Merci + pour votre contribution. closeComment: false diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml new file mode 100644 index 0000000000000000000000000000000000000000..6674b28b4c3111dbfbd21d5671e658d6f0ab7f5f --- /dev/null +++ b/.github/workflows/coding-standards.yml @@ -0,0 +1,34 @@ +name: Coding Standards + +on: [push] + +jobs: + tests: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ubuntu-latest] + php-versions: ['8.0', '8.1'] + name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring + coverage: xdebug + + - name: Install the application + run: | + composer install --no-progress --prefer-dist --optimize-autoloader + yarn install --force + yarn build + + - name: Coding Standards Checks + run: make ci-cs diff --git a/.github/workflows/mutation-tests.yml b/.github/workflows/mutation-tests.yml new file mode 100644 index 0000000000000000000000000000000000000000..8189563870f57d51eeef306a072d8137eaacca73 --- /dev/null +++ b/.github/workflows/mutation-tests.yml @@ -0,0 +1,37 @@ +name: Mutation Testing + +on: [push] + +jobs: + tests: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ubuntu-latest] + php-versions: ['8.0', '8.1'] + name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring + coverage: xdebug + + - name: Install the application + run: | + composer install --no-progress --prefer-dist --optimize-autoloader + yarn install --force + yarn build + + - name: Fetch Git base reference + run: git fetch --depth=1 origin $GITHUB_BASE_REF + + - name: Infection + run: make ci-mu diff --git a/.github/workflows/rector_checkstyle.yaml b/.github/workflows/rector_checkstyle.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0b86d0dc7577a5fa3d4971ac608db774a6568336 --- /dev/null +++ b/.github/workflows/rector_checkstyle.yaml @@ -0,0 +1,32 @@ +name: Rector Checkstyle + +on: [push] + +jobs: + tests: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ ubuntu-latest ] + php-versions: ['8.0', '8.1'] + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring + coverage: none + + - name: Install the application + run: | + composer install --no-progress --prefer-dist --optimize-autoloader + yarn install --force + yarn build + + - name: Rector + run: make ci-rector diff --git a/.github/workflows/static-analyze.yml b/.github/workflows/static-analyze.yml new file mode 100644 index 0000000000000000000000000000000000000000..0233057064c6de9539728096048fd95882688fe8 --- /dev/null +++ b/.github/workflows/static-analyze.yml @@ -0,0 +1,35 @@ +name: Static Analyze + +on: [push] + +jobs: + tests: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ubuntu-latest] + php-versions: ['8.0', '8.1'] + name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring + coverage: xdebug + tools: cs2pr + + - name: Install the application + run: | + composer install --no-progress --prefer-dist --optimize-autoloader + yarn install --force + yarn build + + - name: Static Analyze Checks + run: make ci-st diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000000000000000000000000000000000000..668b92c7d5ee5e6bae66c1ff4e7a40ee7e46d894 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,34 @@ +name: Unit and Functional Tests + +on: [push] + +jobs: + tests: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ ubuntu-latest ] + php-versions: ['8.0', '8.1'] + name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring + coverage: xdebug + + - name: Install the application + run: | + composer install --no-progress --prefer-dist --optimize-autoloader + yarn install --force + yarn build + + - name: Run tests + run: make all diff --git a/.php_cs.dist b/.php_cs.dist deleted file mode 100644 index 7410dff65902e3357954ccf3abd9868467a93434..0000000000000000000000000000000000000000 --- a/.php_cs.dist +++ /dev/null @@ -1,61 +0,0 @@ -<?php - -$header = 'The MIT License (MIT) - -Copyright (c) 2014-2019 Spomky-Labs - -This software may be modified and distributed under the terms -of the MIT license. See the LICENSE file for details.'; - -$finder = PhpCsFixer\Finder::create() - ->in(__DIR__.'/src') - ->in(__DIR__.'/tests') -; - -return PhpCsFixer\Config::create() - ->setRules([ - '@PSR1' => true, - '@PSR2' => true, - '@Symfony' => true, - '@DoctrineAnnotation' => true, - '@PHP70Migration' => true, - '@PHP71Migration' => true, - 'strict_param' => true, - 'strict_comparison' => true, - 'array_syntax' => ['syntax' => 'short'], - 'array_indentation' => true, - 'ordered_imports' => true, - 'protected_to_private' => true, - 'declare_strict_types' => true, - 'native_function_invocation' => [ - 'include' => ['@compiler_optimized'], - 'scope' => 'namespaced', - ], - 'mb_str_functions' => true, - 'method_chaining_indentation' => true, - 'linebreak_after_opening_tag' => true, - 'combine_consecutive_issets' => true, - 'combine_consecutive_unsets' => true, - 'compact_nullable_typehint' => true, - 'no_superfluous_phpdoc_tags' => true, - 'no_superfluous_elseif' => true, - 'phpdoc_trim_consecutive_blank_line_separation' => true, - 'phpdoc_order' => true, - 'pow_to_exponentiation' => true, - 'simplified_null_return' => true, - 'header_comment' => [ - 'header' => $header, - ], - 'align_multiline_comment' => [ - 'comment_type' => 'all_multiline', - ], - 'php_unit_test_annotation' => [ - 'case' => 'snake', - 'style' => 'annotation', - ], - 'php_unit_test_case_static_method_calls' => true, - ]) - ->setRiskyAllowed(true) - ->setUsingCache(true) - ->setFinder($finder) - ; diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 61330d01ad1d963e2337564259a0766a50f7abd3..0000000000000000000000000000000000000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,37 +0,0 @@ -before_commands: - - "composer install --prefer-dist" - -checks: - php: - code_rating: true - duplication: false - -build: - nodes: - analysis: - tests: - override: - - php-scrutinizer-run - -tools: - php_sim: false - php_changetracking: true - sensiolabs_security_checker: true - php_mess_detector: true - php_code_sniffer: true - php_analyzer: true - php_code_coverage: false - php_cpd: true - php_pdepend: - excluded_dirs: [vendor/*, doc/*, tests/*] -filter: - excluded_paths: [vendor/*, doc/*, tests/*] -build_failure_conditions: - - 'elements.rating(<= D).exists' # No classes/methods with a rating of D or worse - - 'elements.rating(<= D).new.exists' # No new classes/methods with a rating of D or worse - - 'patches.label("Doc Comments").exists' # No doc comments patches allowed - - 'patches.label("Spacing").new.count > 1' # More than 1 new spacing patch - - 'issues.label("coding-style").exists' # No coding style issues allowed - - 'issues.label("coding-style").new.exists' # No new coding style issues allowed - - 'issues.severity(>= MAJOR).new.exists' # New issues of major or higher severity - - 'project.metric("scrutinizer.quality", < 9)' # Code Quality Rating drops below 6 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 835b621d0640ca510b1e545a75ff8841bf0d943c..0000000000000000000000000000000000000000 --- a/.travis.yml +++ /dev/null @@ -1,57 +0,0 @@ -sudo: false -language: php - -php: - - 7.2 - - 7.3 - - 7.4 - - nightly - -cache: - directories: - - $HOME/.composer/cache - - vendor - -before_script: - - mkdir -p build/logs - -before_install: - - mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{,.disabled} || echo "xdebug not available" - -install: travis_retry composer install - -jobs: - allow_failures: - - php: nightly - - include: - - stage: Metrics and quality - env: COVERAGE - before_script: - - mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,} - - if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for coverage"; exit 1; fi - script: - - vendor/bin/phpunit --coverage-clover build/logs/clover.xml - after_script: - - ./vendor/bin/php-coveralls --no-interaction - - - stage: Metrics and quality - env: STATIC_ANALYSIS - script: - - ./vendor/bin/phpstan analyse - - - stage: Metrics and quality - env: CODING_STANDARDS - before_script: - - wget https://cs.symfony.com/download/php-cs-fixer-v2.phar -O php-cs-fixer - - chmod a+x php-cs-fixer - script: - - ./php-cs-fixer fix --dry-run --stop-on-violation --using-cache=no - - - stage: Security Check - env: SECURITY_CHECK - before_script: - - wget -c https://get.sensiolabs.org/security-checker.phar - - chmod +x security-checker.phar - script: - - ./security-checker.phar security:check diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..e96bc55c1ef080a10890c5464edf77574bd03665 --- /dev/null +++ b/Makefile @@ -0,0 +1,81 @@ +######################## +# CI/CD # +######################## + +ci-cs: vendor ## Check all files using defined rules (CI/CD) + vendor/bin/ecs check + +ci-st: vendor ## Run static analyse (CI/CD) + vendor/bin/phpstan analyse --error-format=checkstyle | cs2pr + +ci-rector: vendor ## Check all files using Rector (CI/CD) + vendor/bin/rector process --ansi --dry-run + +ci-mu: vendor ## Mutation tests (CI/CD) + vendor/bin/infection --logger-github --git-diff-filter=AM -s --threads=$(nproc) --min-msi=70 --min-covered-msi=50 --test-framework-options="--exclude-group=Performance" + +######################## +# Everyday # +######################## + +all: vendor ## Run all tests + vendor/bin/phpunit --color + +tu: vendor ## Run only unit tests + vendor/bin/phpunit --color tests/Unit + +ti: vendor ## Run only integration tests + vendor/bin/phpunit --color tests/Integration + +tf: vendor ## Run only functional tests + vendor/bin/phpunit --color tests/Functional + +st: vendor ## Run static analyse + vendor/bin/phpstan analyse + + +######################## +# Every PR # +######################## + +cs: vendor ## Fix all files using defined rules + vendor/bin/ecs check --fix + +rector: vendor ## Check all files using Rector + vendor/bin/rector process + + +######################## +# Others # +######################## + +twig-lint: vendor ## All Twig template checks + bin/console lint:twig templates/ + +mu: vendor ## Mutation tests + vendor/bin/infection -s --threads=$(nproc) --min-msi=70 --min-covered-msi=50 --test-framework-options="--exclude-group=Performance" + +db: vendor ## Create the database (should only be used in local env + bin/console doctrine:database:drop --env=test --force + bin/console doctrine:database:create --env=test + bin/console doctrine:schema:create --env=test + +clean: vendor ## Cleanup the var folder + rm -rf var + +cc: vendor ## Show test coverage rates (HTML) + vendor/bin/phpunit --coverage-html ./build + +vendor: composer.json composer.lock + composer validate + composer install + + +######################## +# Default # +######################## + +.DEFAULT_GOAL := help +help: + @grep -E '(^[a-zA-Z_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/' +.PHONY: help diff --git a/README.md b/README.md index 9b9508288ac66445cb2546434dc5d04ba61f32fa..f4eb3c6dbdcbd6f5f293974dbe9b0226d5fc5c10 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ TOTP / HOTP library in PHP ========================== -[](https://scrutinizer-ci.com/g/Spomky-Labs/otphp/?branch=v10.0) -[](https://coveralls.io/github/Spomky-Labs/otphp?branch=v10.0) -[](https://travis-ci.org/Spomky-Labs/otphp) -[](https://www.guardrails.io) + + + -[](https://insight.sensiolabs.com/projects/49e5925d-0dd8-4b89-a215-5eb33b4d96d9) + + + [](https://packagist.org/packages/spomky-labs/otphp) [](https://packagist.org/packages/spomky-labs/otphp) @@ -26,7 +27,7 @@ The documentation of this project is available in the [*doc* folder](doc/index.m I bring solutions to your problems and answer your questions. -If you really love that project and the work I have done or if you want I prioritize your issues, then you can help me out for a couple of :beers: or more! +If you really love that project, and the work I have done or if you want I prioritize your issues, then you can help me out for a couple of :beers: or more! [Become a sponsor](https://github.com/sponsors/Spomky) @@ -42,10 +43,6 @@ Please report all issues in [the repository bug tracker](hhttps://github.com/Spo Also make sure to [follow these best practices](.github/CONTRIBUTING.md). -## Security Issues - -**If you think you have found a security issue, DO NOT open an issue**. [You MUST submit your issue here](https://gitter.im/Spomky/). - ## Licence This software is release under the [MIT licence](LICENSE). diff --git a/composer.json b/composer.json index 8979e8f06e6c4839032193da917a958266915c37..e3d9eb5731d8ef22a7d47ccb3bfc84e0fb634d5c 100644 --- a/composer.json +++ b/composer.json @@ -16,23 +16,23 @@ } ], "require": { - "php": "^7.2|^8.0", + "php": "^8.0", "ext-mbstring": "*", "paragonie/constant_time_encoding": "^2.0", "beberlei/assert": "^3.0", - "thecodingmachine/safe": "^0.1.14|^1.0" + "thecodingmachine/safe": "^1.0|^2.0" }, "require-dev": { - "phpunit/phpunit": "^8.0", - "php-coveralls/php-coveralls": "^2.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-beberlei-assert": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "thecodingmachine/phpstan-safe-rule": "^1.0" - }, - "suggest": { + "infection/infection": "^0.26", + "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-beberlei-assert": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "thecodingmachine/phpstan-safe-rule": "^1.0", + "rector/rector": "^0.12.11", + "symplify/easy-coding-standard": "^10.0" }, "autoload": { "psr-4": { "OTPHP\\": "src/" } @@ -40,11 +40,18 @@ "autoload-dev": { "psr-4": { "OTPHP\\Test\\": "tests/" } }, - "extra": { - "branch-alias": { - "v10.0": "10.0.x-dev", - "v9.0": "9.0.x-dev", - "v8.3": "8.3.x-dev" - } + "config": { + "allow-plugins": { + "phpstan/extension-installer": true, + "infection/extension-installer": true, + "composer/package-versions-deprecated": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "optimize-autoloader": true, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true } } diff --git a/doc/UPGRADE_v10-v11.md b/doc/UPGRADE_v10-v11.md new file mode 100644 index 0000000000000000000000000000000000000000..08b102746f88e6311f3b8dca790e34a27d489301 --- /dev/null +++ b/doc/UPGRADE_v10-v11.md @@ -0,0 +1 @@ +# Upgrade from `v10.x` to `v11.x` diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000000000000000000000000000000000000..0454311c1a3abf2b5b57852481ffc240a2ec51c0 --- /dev/null +++ b/ecs.php @@ -0,0 +1,124 @@ +<?php + +declare(strict_types=1); + +use PhpCsFixer\Fixer\Alias\MbStrFunctionsFixer; +use PhpCsFixer\Fixer\ArrayNotation\ArraySyntaxFixer; +use PhpCsFixer\Fixer\ClassNotation\ProtectedToPrivateFixer; +use PhpCsFixer\Fixer\Comment\HeaderCommentFixer; +use PhpCsFixer\Fixer\ConstantNotation\NativeConstantInvocationFixer; +use PhpCsFixer\Fixer\ControlStructure\NoSuperfluousElseifFixer; +use PhpCsFixer\Fixer\FunctionNotation\NativeFunctionInvocationFixer; +use PhpCsFixer\Fixer\Import\GlobalNamespaceImportFixer; +use PhpCsFixer\Fixer\Import\OrderedImportsFixer; +use PhpCsFixer\Fixer\LanguageConstruct\CombineConsecutiveIssetsFixer; +use PhpCsFixer\Fixer\LanguageConstruct\CombineConsecutiveUnsetsFixer; +use PhpCsFixer\Fixer\Phpdoc\AlignMultilineCommentFixer; +use PhpCsFixer\Fixer\Phpdoc\NoSuperfluousPhpdocTagsFixer; +use PhpCsFixer\Fixer\Phpdoc\PhpdocOrderFixer; +use PhpCsFixer\Fixer\Phpdoc\PhpdocTrimConsecutiveBlankLineSeparationFixer; +use PhpCsFixer\Fixer\PhpTag\LinebreakAfterOpeningTagFixer; +use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestAnnotationFixer; +use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer; +use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestClassRequiresCoversFixer; +use PhpCsFixer\Fixer\ReturnNotation\SimplifiedNullReturnFixer; +use PhpCsFixer\Fixer\Strict\DeclareStrictTypesFixer; +use PhpCsFixer\Fixer\Strict\StrictComparisonFixer; +use PhpCsFixer\Fixer\Strict\StrictParamFixer; +use PhpCsFixer\Fixer\Whitespace\ArrayIndentationFixer; +use PhpCsFixer\Fixer\Whitespace\CompactNullableTypehintFixer; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symplify\EasyCodingStandard\ValueObject\Option; +use Symplify\EasyCodingStandard\ValueObject\Set\SetList; + +return static function (ContainerConfigurator $containerConfigurator): void { + $header = ''; + + $containerConfigurator->import(SetList::PSR_12); + $containerConfigurator->import(SetList::PHP_CS_FIXER); + $containerConfigurator->import(SetList::PHP_CS_FIXER_RISKY); + $containerConfigurator->import(SetList::CLEAN_CODE); + $containerConfigurator->import(SetList::SYMFONY); + $containerConfigurator->import(SetList::DOCTRINE_ANNOTATIONS); + $containerConfigurator->import(SetList::SPACES); + $containerConfigurator->import(SetList::PHPUNIT); + $containerConfigurator->import(SetList::SYMPLIFY); + $containerConfigurator->import(SetList::ARRAY); + $containerConfigurator->import(SetList::COMMON); + $containerConfigurator->import(SetList::COMMENTS); + $containerConfigurator->import(SetList::CONTROL_STRUCTURES); + $containerConfigurator->import(SetList::DOCBLOCK); + $containerConfigurator->import(SetList::NAMESPACES); + $containerConfigurator->import(SetList::STRICT); + + $services = $containerConfigurator->services(); + $services->set(StrictParamFixer::class); + $services->set(StrictComparisonFixer::class); + $services->set(ArraySyntaxFixer::class) + ->call('configure', [[ + 'syntax' => 'short', + ]]) + ; + $services->set(ArrayIndentationFixer::class); + $services->set(OrderedImportsFixer::class); + $services->set(ProtectedToPrivateFixer::class); + $services->set(DeclareStrictTypesFixer::class); + $services->set(NativeConstantInvocationFixer::class); + $services->set(NativeFunctionInvocationFixer::class) + ->call('configure', [[ + 'include' => ['@compiler_optimized'], + 'scope' => 'namespaced', + 'strict' => true, + ]]) + ; + $services->set(MbStrFunctionsFixer::class); + $services->set(LinebreakAfterOpeningTagFixer::class); + $services->set(CombineConsecutiveIssetsFixer::class); + $services->set(CombineConsecutiveUnsetsFixer::class); + $services->set(CompactNullableTypehintFixer::class); + $services->set(NoSuperfluousElseifFixer::class); + $services->set(NoSuperfluousPhpdocTagsFixer::class); + $services->set(PhpdocTrimConsecutiveBlankLineSeparationFixer::class); + $services->set(PhpdocOrderFixer::class); + $services->set(SimplifiedNullReturnFixer::class); + $services->set(HeaderCommentFixer::class) + ->call('configure', [[ + 'header' => $header, + ]]) + ; + $services->set(AlignMultilineCommentFixer::class) + ->call('configure', [[ + 'comment_type' => 'all_multiline', + ]]) + ; + $services->set(PhpUnitTestAnnotationFixer::class) + ->call('configure', [[ + 'style' => 'annotation', + ]]) + ; + $services->set(PhpUnitTestCaseStaticMethodCallsFixer::class); + $services->set(GlobalNamespaceImportFixer::class) + ->call('configure', [[ + 'import_classes' => true, + 'import_constants' => true, + 'import_functions' => true, + ]]) + ; + + $services->remove(PhpUnitTestClassRequiresCoversFixer::class); + + $parameters = $containerConfigurator->parameters(); + $parameters + ->set(Option::PARALLEL, true) + ->set(Option::PATHS, [__DIR__]) + ->set(Option::SKIP, [ + __DIR__ . '/src/Kernel.php', + __DIR__ . '/assets', + __DIR__ . '/bin', + __DIR__ . '/config', + __DIR__ . '/heroku', + __DIR__ . '/public', + __DIR__ . '/var', + ]) + ; +}; diff --git a/infection.json.dist b/infection.json.dist new file mode 100644 index 0000000000000000000000000000000000000000..f02cbf8717b764e2f6f7a8a46b034e03c49218e8 --- /dev/null +++ b/infection.json.dist @@ -0,0 +1,16 @@ +{ + "source": { + "directories": [ + "src" + ] + }, + "logs": { + "text": "infection.log" + }, + "mutators": { + "@default": true, + "global-ignoreSourceCodeByRegex": [ + "\\$this->logger.*" + ] + } +} \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon index c9be896234126222af33e0537715d52b7f619922..5f2b3faa8f0294fa4606e64dd7edff999c070cd3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 7 + level: max paths: - src - tests @@ -11,3 +11,4 @@ includes: - vendor/phpstan/phpstan-deprecation-rules/rules.neon - vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon - vendor/phpstan/phpstan-beberlei-assert/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 327a1d1e08e4f9063c1d1fdbb26d9a3e861c1ed3..52370048ae694c783b7a7133a71d0b53924faa2a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,28 +1,18 @@ <?xml version="1.0" encoding="UTF-8"?> -<phpunit - backupGlobals="false" - backupStaticAttributes="false" - convertErrorsToExceptions="true" - convertNoticesToExceptions="true" - convertWarningsToExceptions="true" - processIsolation="false" - stopOnFailure="false" - bootstrap="vendor/autoload.php" - colors="true"> - <testsuites> - <testsuite name="OTP Test Suite"> - <directory suffix="Test.php">./tests</directory> - </testsuite> - </testsuites> - - <filter> - <whitelist> - <directory suffix=".php">./</directory> - <exclude> - <directory>./tests</directory> - <directory>./doc</directory> - <directory>./vendor</directory> - </exclude> - </whitelist> - </filter> +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" backupStaticAttributes="false" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" bootstrap="vendor/autoload.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> + <coverage> + <include> + <directory suffix=".php">./</directory> + </include> + <exclude> + <directory>./tests</directory> + <directory>./doc</directory> + <directory>./vendor</directory> + </exclude> + </coverage> + <testsuites> + <testsuite name="OTP Test Suite"> + <directory suffix="Test.php">./tests</directory> + </testsuite> + </testsuites> </phpunit> diff --git a/rector.php b/rector.php new file mode 100644 index 0000000000000000000000000000000000000000..7ccb5c0926b5895dacfd3b8a3e4d1a9e403d73d3 --- /dev/null +++ b/rector.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +use Rector\Core\Configuration\Option; +use Rector\Doctrine\Set\DoctrineSetList; +use Rector\Php74\Rector\Property\TypedPropertyRector; +use Rector\PHPUnit\Set\PHPUnitSetList; +use Rector\Set\ValueObject\SetList; +use Rector\Symfony\Set\SymfonySetList; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + +return static function (ContainerConfigurator $containerConfigurator): void { + $containerConfigurator->import(SetList::DEAD_CODE); + $containerConfigurator->import(SetList::PHP_80); + $containerConfigurator->import(SymfonySetList::SYMFONY_52); + $containerConfigurator->import(SymfonySetList::SYMFONY_CODE_QUALITY); + $containerConfigurator->import(SymfonySetList::SYMFONY_52_VALIDATOR_ATTRIBUTES); + $containerConfigurator->import(SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION); + $containerConfigurator->import(SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES); + $containerConfigurator->import(DoctrineSetList::DOCTRINE_CODE_QUALITY); + $containerConfigurator->import(DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES); + $containerConfigurator->import(PHPUnitSetList::PHPUNIT_EXCEPTION); + $containerConfigurator->import(PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD); + $containerConfigurator->import(PHPUnitSetList::PHPUNIT_91); + $containerConfigurator->import(PHPUnitSetList::PHPUNIT_YIELD_DATA_PROVIDER); + $parameters = $containerConfigurator->parameters(); + $parameters->set(Option::PATHS, [__DIR__ . '/src', __DIR__ . '/tests']); + $parameters->set(Option::AUTO_IMPORT_NAMES, true); + $parameters->set(Option::IMPORT_SHORT_CLASSES, true); + + $services = $containerConfigurator->services(); + $services->set(TypedPropertyRector::class); +}; diff --git a/src/Factory.php b/src/Factory.php index 70df63945588b71f02959064dae491ad3b1b2588..42b5fbf79c2a7d7999b3e3726e4093532d60ac81 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -2,21 +2,11 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; use Assert\Assertion; +use function count; use InvalidArgumentException; -use function Safe\parse_url; -use function Safe\sprintf; use Throwable; /** @@ -27,12 +17,10 @@ final class Factory implements FactoryInterface public static function loadFromProvisioningUri(string $uri): OTPInterface { try { - $parsed_url = parse_url($uri); + $parsed_url = Url::fromString($uri); } catch (Throwable $throwable) { throw new InvalidArgumentException('Not a valid OTP provisioning URI', $throwable->getCode(), $throwable); } - Assertion::isArray($parsed_url, 'Not a valid OTP provisioning URI'); - self::checkData($parsed_url); $otp = self::createOTP($parsed_url); @@ -41,68 +29,46 @@ final class Factory implements FactoryInterface return $otp; } - /** - * @param array<string, mixed> $data - */ - private static function populateParameters(OTPInterface &$otp, array $data): void + private static function populateParameters(OTPInterface $otp, Url $data): void { - foreach ($data['query'] as $key => $value) { + foreach ($data->getQuery() as $key => $value) { $otp->setParameter($key, $value); } } - /** - * @param array<string, mixed> $data - */ - private static function populateOTP(OTPInterface &$otp, array $data): void + private static function populateOTP(OTPInterface $otp, Url $data): void { self::populateParameters($otp, $data); - $result = explode(':', rawurldecode(mb_substr($data['path'], 1))); + $result = explode(':', rawurldecode(mb_substr($data->getPath(), 1))); - if (2 > \count($result)) { + if (count($result) < 2) { $otp->setIssuerIncludedAsParameter(false); return; } - if (null !== $otp->getIssuer()) { + if ($otp->getIssuer() !== null) { Assertion::eq($result[0], $otp->getIssuer(), 'Invalid OTP: invalid issuer in parameter'); $otp->setIssuerIncludedAsParameter(true); } $otp->setIssuer($result[0]); } - /** - * @param array<string, mixed> $data - */ - private static function checkData(array &$data): void - { - foreach (['scheme', 'host', 'path', 'query'] as $key) { - Assertion::keyExists($data, $key, 'Not a valid OTP provisioning URI'); - } - Assertion::eq('otpauth', $data['scheme'], 'Not a valid OTP provisioning URI'); - parse_str($data['query'], $data['query']); - Assertion::keyExists($data['query'], 'secret', 'Not a valid OTP provisioning URI'); - } - - /** - * @param array<string, mixed> $parsed_url - */ - private static function createOTP(array $parsed_url): OTPInterface + private static function createOTP(Url $parsed_url): OTPInterface { - switch ($parsed_url['host']) { + switch ($parsed_url->getHost()) { case 'totp': - $totp = TOTP::create($parsed_url['query']['secret']); - $totp->setLabel(self::getLabel($parsed_url['path'])); + $totp = TOTP::create($parsed_url->getSecret()); + $totp->setLabel(self::getLabel($parsed_url->getPath())); return $totp; case 'hotp': - $hotp = HOTP::create($parsed_url['query']['secret']); - $hotp->setLabel(self::getLabel($parsed_url['path'])); + $hotp = HOTP::create($parsed_url->getSecret()); + $hotp->setLabel(self::getLabel($parsed_url->getPath())); return $hotp; default: - throw new InvalidArgumentException(sprintf('Unsupported "%s" OTP type', $parsed_url['host'])); + throw new InvalidArgumentException(sprintf('Unsupported "%s" OTP type', $parsed_url->getHost())); } } @@ -110,6 +76,6 @@ final class Factory implements FactoryInterface { $result = explode(':', rawurldecode(mb_substr($data, 1))); - return 2 === \count($result) ? $result[1] : $result[0]; + return count($result) === 2 ? $result[1] : $result[0]; } } diff --git a/src/FactoryInterface.php b/src/FactoryInterface.php index 00acc2d043bae100f648995dfb1300778e90edc0..74386adebcbdd27516a152f3963b38ec35bb4a7c 100644 --- a/src/FactoryInterface.php +++ b/src/FactoryInterface.php @@ -2,22 +2,13 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; interface FactoryInterface { /** - * This method is the unique public method of the class. - * It can load a provisioning Uri and convert it into an OTP object. + * This method is the unique public method of the class. It can load a provisioning Uri and convert it into an OTP + * object. */ public static function loadFromProvisioningUri(string $uri): OTPInterface; } diff --git a/src/HOTP.php b/src/HOTP.php index a2f4a23951a1210886aa2e6460210eeb8d42f893..49d5c232614e408102f0db4ef403cb204ac92a99 100644 --- a/src/HOTP.php +++ b/src/HOTP.php @@ -2,15 +2,6 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; use Assert\Assertion; @@ -23,29 +14,28 @@ final class HOTP extends OTP implements HOTPInterface $this->setCounter($counter); } - public static function create(?string $secret = null, int $counter = 0, string $digest = 'sha1', int $digits = 6): HOTPInterface - { + public static function create( + ?string $secret = null, + int $counter = 0, + string $digest = 'sha1', + int $digits = 6 + ): HOTPInterface { return new self($secret, $counter, $digest, $digits); } - protected function setCounter(int $counter): void - { - $this->setParameter('counter', $counter); - } - public function getCounter(): int { - return $this->getParameter('counter'); - } + $value = $this->getParameter('counter'); + Assertion::integer($value, 'Invalid "counter" parameter.'); - private function updateCounter(int $counter): void - { - $this->setCounter($counter); + return $value; } public function getProvisioningUri(): string { - return $this->generateURI('hotp', ['counter' => $this->getCounter()]); + return $this->generateURI('hotp', [ + 'counter' => $this->getCounter(), + ]); } /** @@ -55,7 +45,7 @@ final class HOTP extends OTP implements HOTPInterface { Assertion::greaterOrEqualThan($counter, 0, 'The counter must be at least 0.'); - if (null === $counter) { + if ($counter === null) { $counter = $this->getCounter(); } elseif ($counter < $this->getCounter()) { return false; @@ -64,6 +54,33 @@ final class HOTP extends OTP implements HOTPInterface return $this->verifyOtpWithWindow($otp, $counter, $window); } + protected function setCounter(int $counter): void + { + $this->setParameter('counter', $counter); + } + + /** + * @return array<string, callable> + */ + protected function getParameterMap(): array + { + return array_merge( + parent::getParameterMap(), + [ + 'counter' => static function ($value): int { + Assertion::greaterOrEqualThan((int) $value, 0, 'Counter must be at least 0.'); + + return (int) $value; + }, + ] + ); + } + + private function updateCounter(int $counter): void + { + $this->setCounter($counter); + } + private function getWindow(?int $window): int { return abs($window ?? 0); @@ -83,21 +100,4 @@ final class HOTP extends OTP implements HOTPInterface return false; } - - /** - * @return array<string, mixed> - */ - protected function getParameterMap(): array - { - $v = array_merge( - parent::getParameterMap(), - ['counter' => function ($value): int { - Assertion::greaterOrEqualThan((int) $value, 0, 'Counter must be at least 0.'); - - return (int) $value; - }] - ); - - return $v; - } } diff --git a/src/HOTPInterface.php b/src/HOTPInterface.php index 336ce1055878b6e3436e7a0eaf0df238bc921798..a311cb29d85c9e99aa1bfa79cb8260bca32e65b7 100644 --- a/src/HOTPInterface.php +++ b/src/HOTPInterface.php @@ -2,15 +2,6 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; interface HOTPInterface extends OTPInterface @@ -25,5 +16,10 @@ interface HOTPInterface extends OTPInterface * * If the secret is null, a random 64 bytes secret will be generated. */ - public static function create(?string $secret = null, int $counter = 0, string $digest = 'sha1', int $digits = 6): self; + public static function create( + ?string $secret = null, + int $counter = 0, + string $digest = 'sha1', + int $digits = 6 + ): self; } diff --git a/src/OTP.php b/src/OTP.php index 932bcf97e0329ec8bb9d6f730d7b201df328cb01..2ff11dedc21d85de8cb950ce7f95330c5c6b3fc2 100644 --- a/src/OTP.php +++ b/src/OTP.php @@ -2,22 +2,16 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; use Assert\Assertion; +use function chr; +use function count; +use Exception; use ParagonIE\ConstantTime\Base32; use RuntimeException; -use function Safe\ksort; -use function Safe\sprintf; +use function Safe\unpack; +use const STR_PAD_LEFT; abstract class OTP implements OTPInterface { @@ -37,6 +31,11 @@ abstract class OTP implements OTPInterface return str_replace($placeholder, $provisioning_uri, $uri); } + public function at(int $timestamp): string + { + return $this->generateOTP($timestamp); + } + /** * The OTP at the specified input. */ @@ -46,24 +45,23 @@ abstract class OTP implements OTPInterface $hmac = array_values(unpack('C*', $hash)); - $offset = ($hmac[\count($hmac) - 1] & 0xF); + $offset = ($hmac[count($hmac) - 1] & 0xF); $code = ($hmac[$offset + 0] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF); $otp = $code % (10 ** $this->getDigits()); return str_pad((string) $otp, $this->getDigits(), '0', STR_PAD_LEFT); } - public function at(int $timestamp): string - { - return $this->generateOTP($timestamp); - } - /** * @param array<string, mixed> $options */ protected function filterOptions(array &$options): void { - foreach (['algorithm' => 'sha1', 'period' => 30, 'digits' => 6] as $key => $default) { + foreach ([ + 'algorithm' => 'sha1', + 'period' => 30, + 'digits' => 6, + ] as $key => $default) { if (isset($options[$key]) && $default === $options[$key]) { unset($options[$key]); } @@ -84,14 +82,24 @@ abstract class OTP implements OTPInterface $this->filterOptions($options); $params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options)); - return sprintf('otpauth://%s/%s?%s', $type, rawurlencode((null !== $this->getIssuer() ? $this->getIssuer().':' : '').$label), $params); + return sprintf( + 'otpauth://%s/%s?%s', + $type, + rawurlencode(($this->getIssuer() !== null ? $this->getIssuer() . ':' : '') . $label), + $params + ); + } + + protected function compareOTP(string $safe, string $user): bool + { + return hash_equals($safe, $user); } private function getDecodedSecret(): string { try { return Base32::decodeUpper($this->getSecret()); - } catch (\Exception $e) { + } catch (Exception) { throw new RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?'); } } @@ -99,16 +107,11 @@ abstract class OTP implements OTPInterface private function intToByteString(int $int): string { $result = []; - while (0 !== $int) { - $result[] = \chr($int & 0xFF); + while ($int !== 0) { + $result[] = chr($int & 0xFF); $int >>= 8; } - return str_pad(implode(array_reverse($result)), 8, "\000", STR_PAD_LEFT); - } - - protected function compareOTP(string $safe, string $user): bool - { - return hash_equals($safe, $user); + return str_pad(implode('', array_reverse($result)), 8, "\000", STR_PAD_LEFT); } } diff --git a/src/OTPInterface.php b/src/OTPInterface.php index 66e163d5d78a36dc3470ff0f355323998972048c..6c28a3f7549bfddf8cf79c4215a8bc1681df32c3 100644 --- a/src/OTPInterface.php +++ b/src/OTPInterface.php @@ -2,15 +2,6 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; interface OTPInterface @@ -21,8 +12,8 @@ interface OTPInterface public function at(int $timestamp): string; /** - * Verify that the OTP is valid with the specified input. - * If no input is provided, the input is set to a default value or false is returned. + * Verify that the OTP is valid with the specified input. If no input is provided, the input is set to a default + * value or false is returned. */ public function verify(string $otp, ?int $input = null, ?int $window = null): bool; diff --git a/src/ParameterTrait.php b/src/ParameterTrait.php index 69fa774db7c3c22d25875b07841819966a79f6f7..74c43d11b2571b65e35e4779572630e427da0688 100644 --- a/src/ParameterTrait.php +++ b/src/ParameterTrait.php @@ -2,43 +2,25 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; +use function array_key_exists; use Assert\Assertion; use InvalidArgumentException; use ParagonIE\ConstantTime\Base32; -use function Safe\sprintf; trait ParameterTrait { /** * @var array<string, mixed> */ - private $parameters = []; + private array $parameters = []; - /** - * @var string|null - */ - private $issuer; + private null|string $issuer = null; - /** - * @var string|null - */ - private $label; + private null|string $label = null; - /** - * @var bool - */ - private $issuer_included_as_parameter = true; + private bool $issuer_included_as_parameter = true; /** * @return array<string, mixed> @@ -47,7 +29,7 @@ trait ParameterTrait { $parameters = $this->parameters; - if (null !== $this->getIssuer() && true === $this->isIssuerIncludedAsParameter()) { + if ($this->getIssuer() !== null && $this->isIssuerIncludedAsParameter() === true) { $parameters['issuer'] = $this->getIssuer(); } @@ -56,12 +38,10 @@ trait ParameterTrait public function getSecret(): string { - return $this->getParameter('secret'); - } + $value = $this->getParameter('secret'); + Assertion::string($value, 'Invalid "secret" parameter.'); - private function setSecret(?string $secret): void - { - $this->setParameter('secret', $secret); + return $value; } public function getLabel(): ?string @@ -96,27 +76,23 @@ trait ParameterTrait public function getDigits(): int { - return $this->getParameter('digits'); - } + $value = $this->getParameter('digits'); + Assertion::integer($value, 'Invalid "digits" parameter.'); - private function setDigits(int $digits): void - { - $this->setParameter('digits', $digits); + return $value; } public function getDigest(): string { - return $this->getParameter('algorithm'); - } + $value = $this->getParameter('algorithm'); + Assertion::string($value, 'Invalid "algorithm" parameter.'); - private function setDigest(string $digest): void - { - $this->setParameter('algorithm', $digest); + return $value; } public function hasParameter(string $parameter): bool { - return \array_key_exists($parameter, $this->parameters); + return array_key_exists($parameter, $this->parameters); } public function getParameter(string $parameter) @@ -132,20 +108,20 @@ trait ParameterTrait { $map = $this->getParameterMap(); - if (true === \array_key_exists($parameter, $map)) { + if (array_key_exists($parameter, $map) === true) { $callback = $map[$parameter]; $value = $callback($value); } if (property_exists($this, $parameter)) { - $this->$parameter = $value; + $this->{$parameter} = $value; } else { $this->parameters[$parameter] = $value; } } /** - * @return array<string, mixed> + * @return array<string, callable> */ protected function getParameterMap(): array { @@ -155,21 +131,20 @@ trait ParameterTrait return $value; }, - 'secret' => function ($value): string { - if (null === $value) { + 'secret' => static function ($value): string { + if ($value === null) { $value = Base32::encodeUpper(random_bytes(64)); } - $value = trim(mb_strtoupper($value), '='); - return $value; + return mb_strtoupper(trim($value, '=')); }, - 'algorithm' => function ($value): string { + 'algorithm' => static function ($value): string { $value = mb_strtolower($value); Assertion::inArray($value, hash_algos(), sprintf('The "%s" digest is not supported.', $value)); return $value; }, - 'digits' => function ($value): int { + 'digits' => static function ($value): int { Assertion::greaterThan($value, 0, 'Digits must be at least 1.'); return (int) $value; @@ -182,11 +157,26 @@ trait ParameterTrait ]; } + private function setSecret(?string $secret): void + { + $this->setParameter('secret', $secret); + } + + private function setDigits(int $digits): void + { + $this->setParameter('digits', $digits); + } + + private function setDigest(string $digest): void + { + $this->setParameter('algorithm', $digest); + } + private function hasColon(string $value): bool { $colons = [':', '%3A', '%3a']; foreach ($colons as $colon) { - if (false !== mb_strpos($value, $colon)) { + if (str_contains($value, $colon)) { return true; } } diff --git a/src/TOTP.php b/src/TOTP.php index 588b37f17cb450a7f70d46d461519b12415a0b20..75805094e7fad2761194cd729105014ba1f0d972 100644 --- a/src/TOTP.php +++ b/src/TOTP.php @@ -2,19 +2,9 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; use Assert\Assertion; -use function Safe\ksort; final class TOTP extends OTP implements TOTPInterface { @@ -25,29 +15,30 @@ final class TOTP extends OTP implements TOTPInterface $this->setEpoch($epoch); } - public static function create(?string $secret = null, int $period = 30, string $digest = 'sha1', int $digits = 6, int $epoch = 0): TOTPInterface - { + public static function create( + ?string $secret = null, + int $period = 30, + string $digest = 'sha1', + int $digits = 6, + int $epoch = 0 + ): TOTPInterface { return new self($secret, $period, $digest, $digits, $epoch); } - protected function setPeriod(int $period): void - { - $this->setParameter('period', $period); - } - public function getPeriod(): int { - return $this->getParameter('period'); - } + $value = $this->getParameter('period'); + Assertion::integer($value, 'Invalid "epoch" period.'); - private function setEpoch(int $epoch): void - { - $this->setParameter('epoch', $epoch); + return $value; } public function getEpoch(): int { - return $this->getParameter('epoch'); + $value = $this->getParameter('epoch'); + Assertion::integer($value, 'Invalid "epoch" parameter.'); + + return $value; } public function at(int $timestamp): string @@ -67,80 +58,52 @@ final class TOTP extends OTP implements TOTPInterface { $timestamp = $this->getTimestamp($timestamp); - if (null === $window) { + if ($window === null) { return $this->compareOTP($this->at($timestamp), $otp); } return $this->verifyOtpWithWindow($otp, $timestamp, $window); } - private function verifyOtpWithWindow(string $otp, int $timestamp, int $window): bool - { - $window = abs($window); - - for ($i = 0; $i <= $window; ++$i) { - $next = $i * $this->getPeriod() + $timestamp; - $previous = -$i * $this->getPeriod() + $timestamp; - $valid = $this->compareOTP($this->at($next), $otp) || - $this->compareOTP($this->at($previous), $otp); - - if ($valid) { - return true; - } - } - - return false; - } - - private function getTimestamp(?int $timestamp): int - { - $timestamp = $timestamp ?? time(); - Assertion::greaterOrEqualThan($timestamp, 0, 'Timestamp must be at least 0.'); - - return $timestamp; - } - public function getProvisioningUri(): string { $params = []; - if (30 !== $this->getPeriod()) { + if ($this->getPeriod() !== 30) { $params['period'] = $this->getPeriod(); } - if (0 !== $this->getEpoch()) { + if ($this->getEpoch() !== 0) { $params['epoch'] = $this->getEpoch(); } return $this->generateURI('totp', $params); } - private function timecode(int $timestamp): int + protected function setPeriod(int $period): void { - return (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod()); + $this->setParameter('period', $period); } /** - * @return array<string, mixed> + * @return array<string, callable> */ protected function getParameterMap(): array { - $v = array_merge( + return array_merge( parent::getParameterMap(), [ - 'period' => function ($value): int { + 'period' => static function ($value): int { Assertion::greaterThan((int) $value, 0, 'Period must be at least 1.'); return (int) $value; }, - 'epoch' => function ($value): int { + 'epoch' => static function ($value): int { Assertion::greaterOrEqualThan((int) $value, 0, 'Epoch must be greater than or equal to 0.'); return (int) $value; }, ] ); - - return $v; } /** @@ -150,10 +113,46 @@ final class TOTP extends OTP implements TOTPInterface { parent::filterOptions($options); - if (isset($options['epoch']) && 0 === $options['epoch']) { + if (isset($options['epoch']) && $options['epoch'] === 0) { unset($options['epoch']); } ksort($options); } + + private function setEpoch(int $epoch): void + { + $this->setParameter('epoch', $epoch); + } + + private function verifyOtpWithWindow(string $otp, int $timestamp, int $window): bool + { + $window = abs($window); + + for ($i = 0; $i <= $window; ++$i) { + $next = $i * $this->getPeriod() + $timestamp; + $previous = -$i * $this->getPeriod() + $timestamp; + $valid = $this->compareOTP($this->at($next), $otp) || + $this->compareOTP($this->at($previous), $otp); + + if ($valid) { + return true; + } + } + + return false; + } + + private function getTimestamp(?int $timestamp): int + { + $timestamp = $timestamp ?? time(); + Assertion::greaterOrEqualThan($timestamp, 0, 'Timestamp must be at least 0.'); + + return $timestamp; + } + + private function timecode(int $timestamp): int + { + return (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod()); + } } diff --git a/src/TOTPInterface.php b/src/TOTPInterface.php index a19fe7c0b82b4fee5645f31381ad1756cca6cb78..d79fd55991f818b1254b154862a49730e8b6052f 100644 --- a/src/TOTPInterface.php +++ b/src/TOTPInterface.php @@ -2,15 +2,6 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; interface TOTPInterface extends OTPInterface @@ -20,7 +11,12 @@ interface TOTPInterface extends OTPInterface * * If the secret is null, a random 64 bytes secret will be generated. */ - public static function create(?string $secret = null, int $period = 30, string $digest = 'sha1', int $digits = 6): self; + public static function create( + ?string $secret = null, + int $period = 30, + string $digest = 'sha1', + int $digits = 6 + ): self; /** * Return the TOTP at the current time. diff --git a/src/Url.php b/src/Url.php new file mode 100644 index 0000000000000000000000000000000000000000..3fe7a8da6295804e990f78d53517061a53ffa6d6 --- /dev/null +++ b/src/Url.php @@ -0,0 +1,74 @@ +<?php + +declare(strict_types=1); + +namespace OTPHP; + +use Assert\Assertion; +use function Safe\parse_url; + +/** + * @internal + */ +final class Url +{ + public function __construct( + private string $scheme, + private string $host, + private string $path, + private string $secret, + /** @var array<string, mixed> $query */ + private array $query + ) { + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getHost(): string + { + return $this->host; + } + + public function getPath(): string + { + return $this->path; + } + + public function getSecret(): string + { + return $this->secret; + } + + /** + * @return array<string, mixed> + */ + public function getQuery(): array + { + return $this->query; + } + + public static function fromString(string $uri): self + { + $parsed_url = parse_url($uri); + Assertion::isArray($parsed_url, 'Not a valid OTP provisioning URI'); + foreach (['scheme', 'host', 'path', 'query'] as $key) { + Assertion::keyExists($parsed_url, $key, 'Not a valid OTP provisioning URI'); + Assertion::string($parsed_url[$key], 'Not a valid OTP provisioning URI'); + } + $scheme = $parsed_url['scheme']; + Assertion::eq('otpauth', $scheme, 'Not a valid OTP provisioning URI'); + $host = $parsed_url['host']; + $path = $parsed_url['path']; + $query = $parsed_url['query']; + $parsedQuery = []; + parse_str($query, $parsedQuery); + Assertion::keyExists($parsedQuery, 'secret', 'Not a valid OTP provisioning URI'); + $secret = $parsedQuery['secret']; + unset($parsedQuery['secret']); + + return new self($scheme, $host, $path, $secret, $parsedQuery); + } +} diff --git a/tests/FactoryTest.php b/tests/FactoryTest.php index bde87d656348e837bf3704710aeaa22c7d513a42..dea4e03f4e03543ca0d3f928ceb00981e68268fe 100644 --- a/tests/FactoryTest.php +++ b/tests/FactoryTest.php @@ -2,15 +2,6 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP\Test; use InvalidArgumentException; @@ -19,6 +10,9 @@ use OTPHP\HOTP; use OTPHP\TOTP; use PHPUnit\Framework\TestCase; +/** + * @internal + */ final class FactoryTest extends TestCase { /** @@ -30,16 +24,16 @@ final class FactoryTest extends TestCase $result = Factory::loadFromProvisioningUri($otp); static::assertInstanceOf(TOTP::class, $result); - static::assertEquals('My Project', $result->getIssuer()); - static::assertEquals('alice@foo.bar', $result->getLabel()); - static::assertEquals('sha512', $result->getDigest()); - static::assertEquals(8, $result->getDigits()); - static::assertEquals(20, $result->getPeriod()); - static::assertEquals('bar.baz', $result->getParameter('foo')); - static::assertEquals('JDDK4U6G3BJLEZ7Y', $result->getSecret()); + static::assertSame('My Project', $result->getIssuer()); + static::assertSame('alice@foo.bar', $result->getLabel()); + static::assertSame('sha512', $result->getDigest()); + static::assertSame(8, $result->getDigits()); + static::assertSame(20, $result->getPeriod()); + static::assertSame('bar.baz', $result->getParameter('foo')); + static::assertSame('JDDK4U6G3BJLEZ7Y', $result->getSecret()); static::assertFalse($result->hasParameter('image')); static::assertTrue($result->isIssuerIncludedAsParameter()); - static::assertEquals($otp, $result->getProvisioningUri()); + static::assertSame($otp, $result->getProvisioningUri()); } /** @@ -64,15 +58,15 @@ final class FactoryTest extends TestCase $result = Factory::loadFromProvisioningUri($otp); static::assertInstanceOf(HOTP::class, $result); - static::assertEquals('My Project', $result->getIssuer()); - static::assertEquals('alice@foo.bar', $result->getLabel()); - static::assertEquals('sha1', $result->getDigest()); - static::assertEquals(8, $result->getDigits()); - static::assertEquals(1000, $result->getCounter()); - static::assertEquals('JDDK4U6G3BJLEZ7Y', $result->getSecret()); - static::assertEquals('https://foo.bar/baz', $result->getParameter('image')); + static::assertSame('My Project', $result->getIssuer()); + static::assertSame('alice@foo.bar', $result->getLabel()); + static::assertSame('sha1', $result->getDigest()); + static::assertSame(8, $result->getDigits()); + static::assertSame(1000, $result->getCounter()); + static::assertSame('JDDK4U6G3BJLEZ7Y', $result->getSecret()); + static::assertSame('https://foo.bar/baz', $result->getParameter('image')); static::assertTrue($result->isIssuerIncludedAsParameter()); - static::assertEquals($otp, $result->getProvisioningUri()); + static::assertSame($otp, $result->getProvisioningUri()); } /** @@ -151,13 +145,13 @@ final class FactoryTest extends TestCase static::assertInstanceOf(TOTP::class, $result); static::assertNull($result->getIssuer()); - static::assertEquals('My Test - Auth', $result->getLabel()); - static::assertEquals('sha1', $result->getDigest()); - static::assertEquals(6, $result->getDigits()); - static::assertEquals(30, $result->getPeriod()); - static::assertEquals('JDDK4U6G3BJLEZ7Y', $result->getSecret()); + static::assertSame('My Test - Auth', $result->getLabel()); + static::assertSame('sha1', $result->getDigest()); + static::assertSame(6, $result->getDigits()); + static::assertSame(30, $result->getPeriod()); + static::assertSame('JDDK4U6G3BJLEZ7Y', $result->getSecret()); static::assertFalse($result->isIssuerIncludedAsParameter()); - static::assertEquals($otp, $result->getProvisioningUri()); + static::assertSame($otp, $result->getProvisioningUri()); } /** @@ -169,7 +163,7 @@ final class FactoryTest extends TestCase $totp = Factory::loadFromProvisioningUri($uri); static::assertInstanceOf(TOTP::class, $totp); - static::assertEquals('JDDK4U6G3BJLEQ', $totp->getSecret()); - static::assertEquals('otpauth://totp/My%20Test%20-%20Auth?secret=JDDK4U6G3BJLEQ', $totp->getProvisioningUri()); + static::assertSame('JDDK4U6G3BJLEQ', $totp->getSecret()); + static::assertSame('otpauth://totp/My%20Test%20-%20Auth?secret=JDDK4U6G3BJLEQ', $totp->getProvisioningUri()); } } diff --git a/tests/HOTPTest.php b/tests/HOTPTest.php index e8da1c1f3b97395c3fb4c62f71b18c198fff1244..4026de20cfc175577d5878531260fc8c9e9294d3 100644 --- a/tests/HOTPTest.php +++ b/tests/HOTPTest.php @@ -2,22 +2,17 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP\Test; use Assert\Assertion; use InvalidArgumentException; use OTPHP\HOTP; use PHPUnit\Framework\TestCase; +use RuntimeException; +/** + * @internal + */ final class HOTPTest extends TestCase { /** @@ -109,13 +104,14 @@ final class HOTPTest extends TestCase HOTP::create('JDDK4U6G3BJLEZ7Y', 0, 'foo'); } - /**xpectedExceptionMessage + /** + * xpectedExceptionMessage. * * @test */ public function secretShouldBeBase32Encoded(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Unable to decode the secret. Is it correctly base32 encoded?'); $secret = random_bytes(32); @@ -130,7 +126,7 @@ final class HOTPTest extends TestCase { $otp = HOTP::create(); - static::assertRegExp('/^[A-Z2-7]+$/', $otp->getSecret()); + static::assertMatchesRegularExpression('/^[A-Z2-7]+$/', $otp->getSecret()); } /** @@ -141,7 +137,10 @@ final class HOTPTest extends TestCase $otp = $this->createHOTP(8, 'sha1', 1000); $otp->setParameter('image', 'https://foo.bar/baz'); - static::assertEquals('otpauth://hotp/My%20Project%3Aalice%40foo.bar?counter=1000&digits=8&image=https%3A%2F%2Ffoo.bar%2Fbaz&issuer=My%20Project&secret=JDDK4U6G3BJLEZ7Y', $otp->getProvisioningUri()); + static::assertSame( + 'otpauth://hotp/My%20Project%3Aalice%40foo.bar?counter=1000&digits=8&image=https%3A%2F%2Ffoo.bar%2Fbaz&issuer=My%20Project&secret=JDDK4U6G3BJLEZ7Y', + $otp->getProvisioningUri() + ); } /** @@ -163,7 +162,7 @@ final class HOTPTest extends TestCase static::assertTrue($otp->verify('98449994')); static::assertFalse($otp->verify('11111111', 1099)); - static::assertEquals($otp->getCounter(), 1101); + static::assertSame($otp->getCounter(), 1101); } /** @@ -178,8 +177,14 @@ final class HOTPTest extends TestCase static::assertFalse($otp->verify('59647237', 2000, 50)); } - private function createHOTP(int $digits, string $digest, int $counter, string $secret = 'JDDK4U6G3BJLEZ7Y', string $label = 'alice@foo.bar', string $issuer = 'My Project'): HOTP - { + private function createHOTP( + int $digits, + string $digest, + int $counter, + string $secret = 'JDDK4U6G3BJLEZ7Y', + string $label = 'alice@foo.bar', + string $issuer = 'My Project' + ): HOTP { $otp = HOTP::create($secret, $counter, $digest, $digits); $otp->setLabel($label); $otp->setIssuer($issuer); diff --git a/tests/TOTPTest.php b/tests/TOTPTest.php index ce955ebd51d2626cf2cd5331f522fc454a821ced..59cf1160aff6002b6ec0145c903fd5172f78a534 100644 --- a/tests/TOTPTest.php +++ b/tests/TOTPTest.php @@ -2,23 +2,19 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP\Test; use Assert\Assertion; use InvalidArgumentException; use OTPHP\TOTP; +use OTPHP\TOTPInterface; use ParagonIE\ConstantTime\Base32; use PHPUnit\Framework\TestCase; +use RuntimeException; +/** + * @internal + */ final class TOTPTest extends TestCase { /** @@ -43,7 +39,10 @@ final class TOTPTest extends TestCase $otp->setIssuer('My Project'); $otp->setParameter('foo', 'bar.baz'); - static::assertEquals('otpauth://totp/My%20Project%3Aalice%40foo.bar?algorithm=sha512&digits=8&epoch=100&foo=bar.baz&issuer=My%20Project&period=20&secret=JDDK4U6G3BJLEZ7Y', $otp->getProvisioningUri()); + static::assertSame( + 'otpauth://totp/My%20Project%3Aalice%40foo.bar?algorithm=sha512&digits=8&epoch=100&foo=bar.baz&issuer=My%20Project&period=20&secret=JDDK4U6G3BJLEZ7Y', + $otp->getProvisioningUri() + ); } /** @@ -53,7 +52,7 @@ final class TOTPTest extends TestCase { $otp = TOTP::create(); - static::assertRegExp('/^[A-Z2-7]+$/', $otp->getSecret()); + static::assertMatchesRegularExpression('/^[A-Z2-7]+$/', $otp->getSecret()); } /** @@ -81,7 +80,7 @@ final class TOTPTest extends TestCase */ public function secretShouldBeBase32Encoded(): void { - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Unable to decode the secret. Is it correctly base32 encoded?'); $secret = random_bytes(32); @@ -96,7 +95,10 @@ final class TOTPTest extends TestCase { $otp = $this->createTOTP(6, 'sha1', 30); - static::assertEquals('otpauth://totp/My%20Project%3Aalice%40foo.bar?issuer=My%20Project&secret=JDDK4U6G3BJLEZ7Y', $otp->getProvisioningUri()); + static::assertSame( + 'otpauth://totp/My%20Project%3Aalice%40foo.bar?issuer=My%20Project&secret=JDDK4U6G3BJLEZ7Y', + $otp->getProvisioningUri() + ); } /** @@ -106,9 +108,9 @@ final class TOTPTest extends TestCase { $otp = $this->createTOTP(6, 'sha1', 30); - static::assertEquals('855783', $otp->at(0)); - static::assertEquals('762124', $otp->at(319690800)); - static::assertEquals('139664', $otp->at(1301012137)); + static::assertSame('855783', $otp->at(0)); + static::assertSame('762124', $otp->at(319690800)); + static::assertSame('139664', $otp->at(1301012137)); } /** @@ -118,9 +120,9 @@ final class TOTPTest extends TestCase { $otp = $this->createTOTP(6, 'sha1', 30, 'JDDK4U6G3BJLEZ7Y', 'alice@foo.bar', 'My Project', 100); - static::assertEquals('855783', $otp->at(100)); - static::assertEquals('762124', $otp->at(319690900)); - static::assertEquals('139664', $otp->at(1301012237)); + static::assertSame('855783', $otp->at(100)); + static::assertSame('762124', $otp->at(319690900)); + static::assertSame('139664', $otp->at(1301012237)); } /** @@ -144,7 +146,7 @@ final class TOTPTest extends TestCase { $otp = $this->createTOTP(6, 'sha1', 30); - static::assertEquals($otp->now(), $otp->at(time())); + static::assertSame($otp->now(), $otp->at(time())); } /** @@ -197,21 +199,24 @@ final class TOTPTest extends TestCase { $otp = $this->createTOTP(9, 'sha512', 10); - static::assertEquals('otpauth://totp/My%20Project%3Aalice%40foo.bar?algorithm=sha512&digits=9&issuer=My%20Project&period=10&secret=JDDK4U6G3BJLEZ7Y', $otp->getProvisioningUri()); + static::assertSame( + 'otpauth://totp/My%20Project%3Aalice%40foo.bar?algorithm=sha512&digits=9&issuer=My%20Project&period=10&secret=JDDK4U6G3BJLEZ7Y', + $otp->getProvisioningUri() + ); } /** * @dataProvider dataVectors * - * @param \OTPHP\TOTPInterface $totp - * @param int $timestamp - * @param string $expected_value + * @param TOTPInterface $totp + * @param int $timestamp + * @param string $expected_value * * @test */ public function vectors($totp, $timestamp, $expected_value): void { - static::assertEquals($expected_value, $totp->at($timestamp)); + static::assertSame($expected_value, $totp->at($timestamp)); static::assertTrue($totp->verify($expected_value, $timestamp)); } @@ -225,25 +230,30 @@ final class TOTPTest extends TestCase { $totp_sha1 = $this->createTOTP(8, 'sha1', 30, Base32::encodeUpper('12345678901234567890')); $totp_sha256 = $this->createTOTP(8, 'sha256', 30, Base32::encodeUpper('12345678901234567890123456789012')); - $totp_sha512 = $this->createTOTP(8, 'sha512', 30, Base32::encodeUpper('1234567890123456789012345678901234567890123456789012345678901234')); + $totp_sha512 = $this->createTOTP( + 8, + 'sha512', + 30, + Base32::encodeUpper('1234567890123456789012345678901234567890123456789012345678901234') + ); return [ - [$totp_sha1, 59, '94287082'], + [$totp_sha1, 59, '94287082'], [$totp_sha256, 59, '46119246'], [$totp_sha512, 59, '90693936'], - [$totp_sha1, 1111111109, '07081804'], + [$totp_sha1, 1111111109, '07081804'], [$totp_sha256, 1111111109, '68084774'], [$totp_sha512, 1111111109, '25091201'], - [$totp_sha1, 1111111111, '14050471'], + [$totp_sha1, 1111111111, '14050471'], [$totp_sha256, 1111111111, '67062674'], [$totp_sha512, 1111111111, '99943326'], - [$totp_sha1, 1234567890, '89005924'], + [$totp_sha1, 1234567890, '89005924'], [$totp_sha256, 1234567890, '91819424'], [$totp_sha512, 1234567890, '93441116'], - [$totp_sha1, 2000000000, '69279037'], + [$totp_sha1, 2000000000, '69279037'], [$totp_sha256, 2000000000, '90698825'], [$totp_sha512, 2000000000, '38618901'], - [$totp_sha1, 20000000000, '65353130'], + [$totp_sha1, 20000000000, '65353130'], [$totp_sha256, 20000000000, '77737706'], [$totp_sha512, 20000000000, '47863826'], ]; @@ -288,12 +298,31 @@ final class TOTPTest extends TestCase { $otp = $this->createTOTP(6, 'sha1', 30, 'DJBSWY3DPEHPK3PXP', 'alice@google.com', 'My Big Compagny'); - static::assertEquals('http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2FMy%2520Big%2520Compagny%253Aalice%2540google.com%3Fissuer%3DMy%2520Big%2520Compagny%26secret%3DDJBSWY3DPEHPK3PXP', $otp->getQrCodeUri('http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl={PROVISIONING_URI}', '{PROVISIONING_URI}')); - static::assertEquals('http://api.qrserver.com/v1/create-qr-code/?color=5330FF&bgcolor=70FF7E&data=otpauth%3A%2F%2Ftotp%2FMy%2520Big%2520Compagny%253Aalice%2540google.com%3Fissuer%3DMy%2520Big%2520Compagny%26secret%3DDJBSWY3DPEHPK3PXP&qzone=2&margin=0&size=300x300&ecc=H', $otp->getQrCodeUri('http://api.qrserver.com/v1/create-qr-code/?color=5330FF&bgcolor=70FF7E&data=[DATA HERE]&qzone=2&margin=0&size=300x300&ecc=H', '[DATA HERE]')); + static::assertSame( + 'http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2FMy%2520Big%2520Compagny%253Aalice%2540google.com%3Fissuer%3DMy%2520Big%2520Compagny%26secret%3DDJBSWY3DPEHPK3PXP', + $otp->getQrCodeUri( + 'http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl={PROVISIONING_URI}', + '{PROVISIONING_URI}' + ) + ); + static::assertSame( + 'http://api.qrserver.com/v1/create-qr-code/?color=5330FF&bgcolor=70FF7E&data=otpauth%3A%2F%2Ftotp%2FMy%2520Big%2520Compagny%253Aalice%2540google.com%3Fissuer%3DMy%2520Big%2520Compagny%26secret%3DDJBSWY3DPEHPK3PXP&qzone=2&margin=0&size=300x300&ecc=H', + $otp->getQrCodeUri( + 'http://api.qrserver.com/v1/create-qr-code/?color=5330FF&bgcolor=70FF7E&data=[DATA HERE]&qzone=2&margin=0&size=300x300&ecc=H', + '[DATA HERE]' + ) + ); } - private function createTOTP(int $digits, string $digest, int $period, string $secret = 'JDDK4U6G3BJLEZ7Y', string $label = 'alice@foo.bar', string $issuer = 'My Project', int $epoch = 0): TOTP - { + private function createTOTP( + int $digits, + string $digest, + int $period, + string $secret = 'JDDK4U6G3BJLEZ7Y', + string $label = 'alice@foo.bar', + string $issuer = 'My Project', + int $epoch = 0 + ): TOTP { $otp = TOTP::create($secret, $period, $digest, $digits, $epoch); $otp->setLabel($label); $otp->setIssuer($issuer);