ChatGPT's update to harden software and run db updates.

This commit is contained in:
2026-10-21 06:53:45 -04:00
parent 1e63076123
commit acc8cb70f5
171 changed files with 9325 additions and 404 deletions
+10
View File
@@ -0,0 +1,10 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
);
+3
View File
@@ -8,8 +8,11 @@ $baseDir = dirname($vendorDir);
return array(
'ZipStream\\' => array($vendorDir . '/maennchen/zipstream-php/src'),
'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'),
'PhpOffice\\PhpSpreadsheet\\' => array($vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet'),
'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'),
'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
'OTPHP\\' => array($vendorDir . '/spomky-labs/otphp/src'),
'Matrix\\' => array($vendorDir . '/markbaker/matrix/classes/src'),
'Composer\\Pcre\\' => array($vendorDir . '/composer/pcre/src'),
'Complex\\' => array($vendorDir . '/markbaker/complex/classes/src'),
+12
View File
@@ -33,6 +33,18 @@ class ComposerAutoloaderInit6621356a0da1644f04f6f40bd0e0b948
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit6621356a0da1644f04f6f40bd0e0b948::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}
+22
View File
@@ -6,6 +6,10 @@ namespace Composer\Autoload;
class ComposerStaticInit6621356a0da1644f04f6f40bd0e0b948
{
public static $files = array (
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
);
public static $prefixLengthsPsr4 = array (
'Z' =>
array (
@@ -14,9 +18,15 @@ class ComposerStaticInit6621356a0da1644f04f6f40bd0e0b948
'P' =>
array (
'Psr\\SimpleCache\\' => 16,
'Psr\\Clock\\' => 10,
'PhpOffice\\PhpSpreadsheet\\' => 25,
'ParagonIE\\ConstantTime\\' => 23,
'PHPMailer\\PHPMailer\\' => 20,
),
'O' =>
array (
'OTPHP\\' => 6,
),
'M' =>
array (
'Matrix\\' => 7,
@@ -37,14 +47,26 @@ class ComposerStaticInit6621356a0da1644f04f6f40bd0e0b948
array (
0 => __DIR__ . '/..' . '/psr/simple-cache/src',
),
'Psr\\Clock\\' =>
array (
0 => __DIR__ . '/..' . '/psr/clock/src',
),
'PhpOffice\\PhpSpreadsheet\\' =>
array (
0 => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet',
),
'ParagonIE\\ConstantTime\\' =>
array (
0 => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src',
),
'PHPMailer\\PHPMailer\\' =>
array (
0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src',
),
'OTPHP\\' =>
array (
0 => __DIR__ . '/..' . '/spomky-labs/otphp/src',
),
'Matrix\\' =>
array (
0 => __DIR__ . '/..' . '/markbaker/matrix/classes/src',
+270
View File
@@ -273,6 +273,78 @@
},
"install-path": "../markbaker/matrix"
},
{
"name": "paragonie/constant_time_encoding",
"version": "v3.1.3",
"version_normalized": "3.1.3.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77",
"reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77",
"shasum": ""
},
"require": {
"php": "^8"
},
"require-dev": {
"infection/infection": "^0",
"nikic/php-fuzzer": "^0",
"phpunit/phpunit": "^9|^10|^11",
"vimeo/psalm": "^4|^5|^6"
},
"time": "2025-09-24T15:06:41+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"ParagonIE\\ConstantTime\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com",
"role": "Maintainer"
},
{
"name": "Steve 'Sc00bz' Thomas",
"email": "steve@tobtu.com",
"homepage": "https://www.tobtu.com",
"role": "Original Developer"
}
],
"description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
"keywords": [
"base16",
"base32",
"base32_decode",
"base32_encode",
"base64",
"base64_decode",
"base64_encode",
"bin2hex",
"encoding",
"hex",
"hex2bin",
"rfc4648"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
"source": "https://github.com/paragonie/constant_time_encoding"
},
"install-path": "../paragonie/constant_time_encoding"
},
{
"name": "phpmailer/phpmailer",
"version": "v7.0.0",
@@ -472,6 +544,57 @@
},
"install-path": "../phpoffice/phpspreadsheet"
},
{
"name": "psr/clock",
"version": "1.0.0",
"version_normalized": "1.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/clock.git",
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0"
},
"time": "2022-11-25T14:36:26+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Clock\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for reading the clock.",
"homepage": "https://github.com/php-fig/clock",
"keywords": [
"clock",
"now",
"psr",
"psr-20",
"time"
],
"support": {
"issues": "https://github.com/php-fig/clock/issues",
"source": "https://github.com/php-fig/clock/tree/1.0.0"
},
"install-path": "../psr/clock"
},
{
"name": "psr/simple-cache",
"version": "3.0.0",
@@ -526,6 +649,153 @@
},
"install-path": "../psr/simple-cache"
},
{
"name": "spomky-labs/otphp",
"version": "11.5.0",
"version_normalized": "11.5.0.0",
"source": {
"type": "git",
"url": "https://github.com/Spomky-Labs/otphp.git",
"reference": "877683d6352b80cdc7020fd43a725629c2524435"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/877683d6352b80cdc7020fd43a725629c2524435",
"reference": "877683d6352b80cdc7020fd43a725629c2524435",
"shasum": ""
},
"require": {
"paragonie/constant_time_encoding": "^2.0 || ^3.0",
"php": ">=8.1",
"psr/clock": "^1.0",
"symfony/deprecation-contracts": "^3.2"
},
"require-dev": {
"symfony/error-handler": "^6.4|^7.0|^8.0"
},
"time": "2026-06-06T23:41:24+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"OTPHP\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Florent Morselli",
"homepage": "https://github.com/Spomky"
},
{
"name": "All contributors",
"homepage": "https://github.com/Spomky-Labs/otphp/contributors"
}
],
"description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator",
"homepage": "https://github.com/Spomky-Labs/otphp",
"keywords": [
"FreeOTP",
"RFC 4226",
"RFC 6238",
"google authenticator",
"hotp",
"otp",
"totp"
],
"support": {
"issues": "https://github.com/Spomky-Labs/otphp/issues",
"source": "https://github.com/Spomky-Labs/otphp/tree/11.5.0"
},
"funding": [
{
"url": "https://github.com/Spomky",
"type": "github"
},
{
"url": "https://www.patreon.com/FlorentMorselli",
"type": "patreon"
}
],
"install-path": "../spomky-labs/otphp"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.7.1",
"version_normalized": "3.7.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d",
"reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"time": "2026-06-05T06:23:12+00:00",
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.7-dev"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/deprecation-contracts"
},
{
"name": "tecnickcom/tcpdf",
"version": "6.10.0",
+38 -2
View File
@@ -3,7 +3,7 @@
'name' => '__root__',
'pretty_version' => 'dev-development',
'version' => 'dev-development',
'reference' => 'a1643eb1b74d1dae0fda79a68b6f0672d4603a81',
'reference' => '1e63076123398a86c77f1c3a53bd64563fe29543',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -13,7 +13,7 @@
'__root__' => array(
'pretty_version' => 'dev-development',
'version' => 'dev-development',
'reference' => 'a1643eb1b74d1dae0fda79a68b6f0672d4603a81',
'reference' => '1e63076123398a86c77f1c3a53bd64563fe29543',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -55,6 +55,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'paragonie/constant_time_encoding' => array(
'pretty_version' => 'v3.1.3',
'version' => '3.1.3.0',
'reference' => 'd5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77',
'type' => 'library',
'install_path' => __DIR__ . '/../paragonie/constant_time_encoding',
'aliases' => array(),
'dev_requirement' => false,
),
'phpmailer/phpmailer' => array(
'pretty_version' => 'v7.0.0',
'version' => '7.0.0.0',
@@ -73,6 +82,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'psr/clock' => array(
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'reference' => 'e41a24703d4560fd0acb709162f73b8adfc3aa0d',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/clock',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/simple-cache' => array(
'pretty_version' => '3.0.0',
'version' => '3.0.0.0',
@@ -82,6 +100,24 @@
'aliases' => array(),
'dev_requirement' => false,
),
'spomky-labs/otphp' => array(
'pretty_version' => '11.5.0',
'version' => '11.5.0.0',
'reference' => '877683d6352b80cdc7020fd43a725629c2524435',
'type' => 'library',
'install_path' => __DIR__ . '/../spomky-labs/otphp',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v3.7.1',
'version' => '3.7.1.0',
'reference' => 'f3202fa1b5097b0af062dc978b32ecf63404e31d',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(),
'dev_requirement' => false,
),
'tecnickcom/tcpdf' => array(
'pretty_version' => '6.10.0',
'version' => '6.10.0.0',
+48
View File
@@ -0,0 +1,48 @@
The MIT License (MIT)
Copyright (c) 2016 - 2022 Paragon Initiative Enterprises
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
This library was based on the work of Steve "Sc00bz" Thomas.
------------------------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2014 Steve Thomas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+88
View File
@@ -0,0 +1,88 @@
# Constant-Time Encoding
[![Build Status](https://github.com/paragonie/constant_time_encoding/actions/workflows/ci.yml/badge.svg)](https://github.com/paragonie/constant_time_encoding/actions)
[![Static Analysis](https://github.com/paragonie/constant_time_encoding/actions/workflows/psalm.yml/badge.svg)](https://github.com/paragonie/constant_time_encoding/actions)
[![Latest Stable Version](https://poser.pugx.org/paragonie/constant_time_encoding/v/stable)](https://packagist.org/packages/paragonie/constant_time_encoding)
[![Latest Unstable Version](https://poser.pugx.org/paragonie/constant_time_encoding/v/unstable)](https://packagist.org/packages/paragonie/constant_time_encoding)
[![License](https://poser.pugx.org/paragonie/constant_time_encoding/license)](https://packagist.org/packages/paragonie/constant_time_encoding)
[![Downloads](https://img.shields.io/packagist/dt/paragonie/constant_time_encoding.svg)](https://packagist.org/packages/paragonie/constant_time_encoding)
Based on the [constant-time base64 implementation made by Steve "Sc00bz" Thomas](https://github.com/Sc00bz/ConstTimeEncoding),
this library aims to offer character encoding functions that do not leak
information about what you are encoding/decoding via processor cache
misses. Further reading on [cache-timing attacks](http://blog.ircmaxell.com/2014/11/its-all-about-time.html).
Our fork offers the following enhancements:
* `mbstring.func_overload` resistance
* Unit tests
* Composer- and Packagist-ready
* Base16 encoding
* Base32 encoding
* Uses `pack()` and `unpack()` instead of `chr()` and `ord()`
## PHP Version Requirements
Version 3 of this library should work on **PHP 8** or newer.
Version 2 of this library should work on **PHP 7** or newer. See [the v2.x branch](https://github.com/paragonie/constant_time_encoding/tree/v2.x).
For PHP 5 support, see [the v1.x branch](https://github.com/paragonie/constant_time_encoding/tree/v1.x).
If you are adding this as a dependency to a project intended to work on PHP 5 through 8.4, please set the required version to `^1|^2|^3`.
## How to Install
```sh
composer require paragonie/constant_time_encoding
```
## How to Use
```php
use ParagonIE\ConstantTime\Encoding;
// possibly (if applicable):
// require 'vendor/autoload.php';
$data = random_bytes(32);
echo Encoding::base64Encode($data), "\n";
echo Encoding::base32EncodeUpper($data), "\n";
echo Encoding::base32Encode($data), "\n";
echo Encoding::hexEncode($data), "\n";
echo Encoding::hexEncodeUpper($data), "\n";
```
Example output:
```
1VilPkeVqirlPifk5scbzcTTbMT2clp+Zkyv9VFFasE=
2VMKKPSHSWVCVZJ6E7SONRY3ZXCNG3GE6ZZFU7TGJSX7KUKFNLAQ====
2vmkkpshswvcvzj6e7sonry3zxcng3ge6zzfu7tgjsx7kukfnlaq====
d558a53e4795aa2ae53e27e4e6c71bcdc4d36cc4f6725a7e664caff551456ac1
D558A53E4795AA2AE53E27E4E6C71BDCC4D36CC4F6725A7E664CAFF551456AC1
```
If you only need a particular variant, you can just reference the
required class like so:
```php
use ParagonIE\ConstantTime\Base64;
use ParagonIE\ConstantTime\Base32;
$data = random_bytes(32);
echo Base64::encode($data), "\n";
echo Base32::encode($data), "\n";
```
Example output:
```
1VilPkeVqirlPifk5scbzcTTbMT2clp+Zkyv9VFFasE=
2vmkkpshswvcvzj6e7sonry3zxcng3ge6zzfu7tgjsx7kukfnlaq====
```
## Support Contracts
If your company uses this library in their products or services, you may be
interested in [purchasing a support contract from Paragon Initiative Enterprises](https://paragonie.com/enterprise).
+67
View File
@@ -0,0 +1,67 @@
{
"name": "paragonie/constant_time_encoding",
"description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
"keywords": [
"base64",
"encoding",
"rfc4648",
"base32",
"base16",
"hex",
"bin2hex",
"hex2bin",
"base64_encode",
"base64_decode",
"base32_encode",
"base32_decode"
],
"license": "MIT",
"type": "library",
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com",
"role": "Maintainer"
},
{
"name": "Steve 'Sc00bz' Thomas",
"email": "steve@tobtu.com",
"homepage": "https://www.tobtu.com",
"role": "Original Developer"
}
],
"support": {
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
"email": "info@paragonie.com",
"source": "https://github.com/paragonie/constant_time_encoding"
},
"require": {
"php": "^8"
},
"require-dev": {
"infection/infection": "^0",
"nikic/php-fuzzer": "^0",
"phpunit/phpunit": "^9|^10|^11",
"vimeo/psalm": "^4|^5|^6"
},
"autoload": {
"psr-4": {
"ParagonIE\\ConstantTime\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"ParagonIE\\ConstantTime\\Tests\\": "tests/"
}
},
"scripts": {
"mutation-test": "infection"
},
"config": {
"process-timeout": 0,
"allow-plugins": {
"infection/extension-installer": true
}
}
}
+558
View File
@@ -0,0 +1,558 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use InvalidArgumentException;
use Override;
use RangeException;
use SensitiveParameter;
use TypeError;
use function pack;
use function rtrim;
use function strlen;
use function substr;
use function unpack;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Base32
* [A-Z][2-7]
*
* @package ParagonIE\ConstantTime
*/
abstract class Base32 implements EncoderInterface
{
/**
* Decode a Base32-encoded string into raw binary
*
* @param string $encodedString
* @param bool $strictPadding
* @return string
*/
#[Override]
public static function decode(
#[SensitiveParameter]
string $encodedString,
bool $strictPadding = false
): string {
return static::doDecode($encodedString, false, $strictPadding);
}
/**
* Decode an uppercase Base32-encoded string into raw binary
*
* @param string $src
* @param bool $strictPadding
* @return string
*/
public static function decodeUpper(
#[SensitiveParameter]
string $src,
bool $strictPadding = false
): string {
return static::doDecode($src, true, $strictPadding);
}
/**
* Encode into Base32 (RFC 4648)
*
* @param string $binString
* @return string
* @throws TypeError
*/
#[Override]
public static function encode(
#[SensitiveParameter]
string $binString
): string {
return static::doEncode($binString, false, true);
}
/**
* Encode into Base32 (RFC 4648)
*
* @param string $src
* @return string
* @throws TypeError
* @api
*/
public static function encodeUnpadded(
#[SensitiveParameter]
string $src
): string {
return static::doEncode($src, false, false);
}
/**
* Encode into uppercase Base32 (RFC 4648)
*
* @param string $src
* @return string
* @throws TypeError
* @api
*/
public static function encodeUpper(
#[SensitiveParameter]
string $src
): string {
return static::doEncode($src, true, true);
}
/**
* Encode into uppercase Base32 (RFC 4648)
*
* @param string $src
* @return string
* @throws TypeError
* @api
*/
public static function encodeUpperUnpadded(
#[SensitiveParameter]
string $src
): string {
return static::doEncode($src, true, false);
}
/**
* Uses bitwise operators instead of table-lookups to turn 5-bit integers
* into 8-bit integers.
*
* @param int $src
* @return int
* @api
*/
protected static function decode5Bits(int $src): int
{
$ret = -1;
// if ($src > 96 && $src < 123) $ret += $src - 97 + 1; // -64
$ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 96);
// if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23
$ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 5-bit integers
* into 8-bit integers.
*
* Uppercase variant.
*
* @param int $src
* @return int
* @api
*/
protected static function decode5BitsUpper(int $src): int
{
$ret = -1;
// if ($src > 64 && $src < 91) $ret += $src - 65 + 1; // -64
$ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
// if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23
$ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 5-bit integers.
*
* @param int $src
* @return string
* @api
*/
protected static function encode5Bits(int $src): string
{
$diff = 0x61;
// if ($src > 25) $ret -= 72;
$diff -= ((25 - $src) >> 8) & 73;
return pack('C', $src + $diff);
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 5-bit integers.
*
* Uppercase variant.
*
* @param int $src
* @return string
* @api
*/
protected static function encode5BitsUpper(int $src): string
{
$diff = 0x41;
// if ($src > 25) $ret -= 40;
$diff -= ((25 - $src) >> 8) & 41;
return pack('C', $src + $diff);
}
/**
* @param string $encodedString
* @param bool $upper
* @return string
* @api
*/
public static function decodeNoPadding(
#[SensitiveParameter]
string $encodedString,
bool $upper = false
): string {
$srcLen = strlen($encodedString);
if ($srcLen === 0) {
return '';
}
if (($srcLen & 7) === 0) {
for ($j = 0; $j < 7 && $j < $srcLen; ++$j) {
if ($encodedString[$srcLen - $j - 1] === '=') {
throw new InvalidArgumentException(
"decodeNoPadding() doesn't tolerate padding"
);
}
}
}
return static::doDecode(
$encodedString,
$upper,
true
);
}
/**
* Base32 decoding
*
* @param string $src
* @param bool $upper
* @param bool $strictPadding
* @return string
*
* @throws TypeError
*/
protected static function doDecode(
#[SensitiveParameter]
string $src,
bool $upper = false,
bool $strictPadding = false
): string {
// We do this to reduce code duplication:
$method = $upper
? 'decode5BitsUpper'
: 'decode5Bits';
// Remove padding
$srcLen = strlen($src);
if ($srcLen === 0) {
return '';
}
if ($strictPadding) {
if (($srcLen & 7) === 0) {
for ($j = 0; $j < 7; ++$j) {
if ($src[$srcLen - 1] === '=') {
$srcLen--;
} else {
break;
}
}
}
if (($srcLen & 7) === 1) {
throw new RangeException(
'Incorrect padding'
);
}
} else {
$src = rtrim($src, '=');
$srcLen = strlen($src);
}
$err = 0;
$dest = '';
// Main loop (no padding):
for ($i = 0; $i + 8 <= $srcLen; $i += 8) {
/** @var array<int, int> $chunk */
$chunk = unpack('C*', substr($src, $i, 8));
/** @var int $c0 */
$c0 = static::$method($chunk[1]);
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
/** @var int $c2 */
$c2 = static::$method($chunk[3]);
/** @var int $c3 */
$c3 = static::$method($chunk[4]);
/** @var int $c4 */
$c4 = static::$method($chunk[5]);
/** @var int $c5 */
$c5 = static::$method($chunk[6]);
/** @var int $c6 */
$c6 = static::$method($chunk[7]);
/** @var int $c7 */
$c7 = static::$method($chunk[8]);
$dest .= pack(
'CCCCC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
(($c3 << 4) | ($c4 >> 1) ) & 0xff,
(($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff,
(($c6 << 5) | ($c7 ) ) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6 | $c7) >> 8;
}
// The last chunk, which may have padding:
if ($i < $srcLen) {
/** @var array<int, int> $chunk */
$chunk = unpack('C*', substr($src, $i, $srcLen - $i));
/** @var int $c0 */
$c0 = static::$method($chunk[1]);
if ($i + 6 < $srcLen) {
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
/** @var int $c2 */
$c2 = static::$method($chunk[3]);
/** @var int $c3 */
$c3 = static::$method($chunk[4]);
/** @var int $c4 */
$c4 = static::$method($chunk[5]);
/** @var int $c5 */
$c5 = static::$method($chunk[6]);
/** @var int $c6 */
$c6 = static::$method($chunk[7]);
$dest .= pack(
'CCCC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
(($c3 << 4) | ($c4 >> 1) ) & 0xff,
(($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6) >> 8;
if ($strictPadding) {
$err |= ($c6 << 5) & 0xff;
}
} elseif ($i + 5 < $srcLen) {
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
/** @var int $c2 */
$c2 = static::$method($chunk[3]);
/** @var int $c3 */
$c3 = static::$method($chunk[4]);
/** @var int $c4 */
$c4 = static::$method($chunk[5]);
/** @var int $c5 */
$c5 = static::$method($chunk[6]);
$dest .= pack(
'CCCC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
(($c3 << 4) | ($c4 >> 1) ) & 0xff,
(($c4 << 7) | ($c5 << 2) ) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5) >> 8;
} elseif ($i + 4 < $srcLen) {
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
/** @var int $c2 */
$c2 = static::$method($chunk[3]);
/** @var int $c3 */
$c3 = static::$method($chunk[4]);
/** @var int $c4 */
$c4 = static::$method($chunk[5]);
$dest .= pack(
'CCC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
(($c3 << 4) | ($c4 >> 1) ) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3 | $c4) >> 8;
if ($strictPadding) {
$err |= ($c4 << 7) & 0xff;
}
} elseif ($i + 3 < $srcLen) {
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
/** @var int $c2 */
$c2 = static::$method($chunk[3]);
/** @var int $c3 */
$c3 = static::$method($chunk[4]);
$dest .= pack(
'CC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3) >> 8;
if ($strictPadding) {
$err |= ($c3 << 4) & 0xff;
}
} elseif ($i + 2 < $srcLen) {
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
/** @var int $c2 */
$c2 = static::$method($chunk[3]);
$dest .= pack(
'CC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) ) & 0xff
);
$err |= ($c0 | $c1 | $c2) >> 8;
if ($strictPadding) {
$err |= ($c2 << 6) & 0xff;
}
} elseif ($i + 1 < $srcLen) {
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
$dest .= pack(
'C',
(($c0 << 3) | ($c1 >> 2) ) & 0xff
);
$err |= ($c0 | $c1) >> 8;
if ($strictPadding) {
$err |= ($c1 << 6) & 0xff;
}
} else {
$dest .= pack(
'C',
(($c0 << 3) ) & 0xff
);
$err |= ($c0) >> 8;
}
}
$check = ($err === 0);
if (!$check) {
throw new RangeException(
'Base32::doDecode() only expects characters in the correct base32 alphabet'
);
}
return $dest;
}
/**
* Base32 Encoding
*
* @param string $src
* @param bool $upper
* @param bool $pad
* @return string
* @throws TypeError
*/
protected static function doEncode(
#[SensitiveParameter]
string $src,
bool $upper = false,
bool $pad = true
): string {
// We do this to reduce code duplication:
$method = $upper
? 'encode5BitsUpper'
: 'encode5Bits';
$dest = '';
$srcLen = strlen($src);
// Main loop (no padding):
for ($i = 0; $i + 5 <= $srcLen; $i += 5) {
/** @var array<int, int> $chunk */
$chunk = unpack('C*', substr($src, $i, 5));
$b0 = $chunk[1];
$b1 = $chunk[2];
$b2 = $chunk[3];
$b3 = $chunk[4];
$b4 = $chunk[5];
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
static::$method((($b1 >> 1) ) & 31) .
static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
static::$method((($b2 << 1) | ($b3 >> 7)) & 31) .
static::$method((($b3 >> 2) ) & 31) .
static::$method((($b3 << 3) | ($b4 >> 5)) & 31) .
static::$method( $b4 & 31);
}
// The last chunk, which may have padding:
if ($i < $srcLen) {
/** @var array<int, int> $chunk */
$chunk = unpack('C*', substr($src, $i, $srcLen - $i));
$b0 = $chunk[1];
if ($i + 3 < $srcLen) {
$b1 = $chunk[2];
$b2 = $chunk[3];
$b3 = $chunk[4];
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
static::$method((($b1 >> 1) ) & 31) .
static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
static::$method((($b2 << 1) | ($b3 >> 7)) & 31) .
static::$method((($b3 >> 2) ) & 31) .
static::$method((($b3 << 3) ) & 31);
if ($pad) {
$dest .= '=';
}
} elseif ($i + 2 < $srcLen) {
$b1 = $chunk[2];
$b2 = $chunk[3];
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
static::$method((($b1 >> 1) ) & 31) .
static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
static::$method((($b2 << 1) ) & 31);
if ($pad) {
$dest .= '===';
}
} elseif ($i + 1 < $srcLen) {
$b1 = $chunk[2];
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
static::$method((($b1 >> 1) ) & 31) .
static::$method((($b1 << 4) ) & 31);
if ($pad) {
$dest .= '====';
}
} else {
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method( ($b0 << 2) & 31);
if ($pad) {
$dest .= '======';
}
}
}
return $dest;
}
}
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use Override;
use function pack;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Base32Hex
* [0-9][A-V]
*
* @package ParagonIE\ConstantTime
*/
abstract class Base32Hex extends Base32
{
/**
* Uses bitwise operators instead of table-lookups to turn 5-bit integers
* into 8-bit integers.
*
* @param int $src
* @return int
*/
#[Override]
protected static function decode5Bits(int $src): int
{
$ret = -1;
// if ($src > 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47
$ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47);
// if ($src > 0x60 && $src < 0x77) ret += $src - 0x61 + 10 + 1; // -86
$ret += (((0x60 - $src) & ($src - 0x77)) >> 8) & ($src - 86);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 5-bit integers
* into 8-bit integers.
*
* @param int $src
* @return int
*/
#[Override]
protected static function decode5BitsUpper(int $src): int
{
$ret = -1;
// if ($src > 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47
$ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47);
// if ($src > 0x40 && $src < 0x57) ret += $src - 0x41 + 10 + 1; // -54
$ret += (((0x40 - $src) & ($src - 0x57)) >> 8) & ($src - 54);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 5-bit integers.
*
* @param int $src
* @return string
*/
#[Override]
protected static function encode5Bits(int $src): string
{
$src += 0x30;
// if ($src > 0x39) $src += 0x61 - 0x3a; // 39
$src += ((0x39 - $src) >> 8) & 39;
return pack('C', $src);
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 5-bit integers.
*
* Uppercase variant.
*
* @param int $src
* @return string
*/
#[Override]
protected static function encode5BitsUpper(int $src): string
{
$src += 0x30;
// if ($src > 0x39) $src += 0x41 - 0x3a; // 7
$src += ((0x39 - $src) >> 8) & 7;
return pack('C', $src);
}
}
+381
View File
@@ -0,0 +1,381 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use InvalidArgumentException;
use Override;
use RangeException;
use SensitiveParameter;
use SodiumException;
use TypeError;
use function extension_loaded;
use function pack;
use function rtrim;
use function sodium_base642bin;
use function sodium_bin2base64;
use function strlen;
use function substr;
use function unpack;
use const SODIUM_BASE64_VARIANT_ORIGINAL;
use const SODIUM_BASE64_VARIANT_ORIGINAL_NO_PADDING;
use const SODIUM_BASE64_VARIANT_URLSAFE;
use const SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Base64
* [A-Z][a-z][0-9]+/
*
* @package ParagonIE\ConstantTime
*/
abstract class Base64 implements EncoderInterface
{
/**
* Encode into Base64
*
* Base64 character set "[A-Z][a-z][0-9]+/"
*
* @param string $binString
* @return string
*
* @throws TypeError
*/
#[Override]
public static function encode(
#[SensitiveParameter]
string $binString
): string {
if (extension_loaded('sodium')) {
$variant = match(static::class) {
Base64::class => SODIUM_BASE64_VARIANT_ORIGINAL,
Base64UrlSafe::class => SODIUM_BASE64_VARIANT_URLSAFE,
default => 0,
};
if ($variant > 0) {
try {
return sodium_bin2base64($binString, $variant);
} catch (SodiumException $ex) {
throw new RangeException($ex->getMessage(), $ex->getCode(), $ex);
}
}
}
return static::doEncode($binString, true);
}
/**
* Encode into Base64, no = padding
*
* Base64 character set "[A-Z][a-z][0-9]+/"
*
* @param string $src
* @return string
*
* @throws TypeError
* @api
*/
public static function encodeUnpadded(
#[SensitiveParameter]
string $src
): string {
if (extension_loaded('sodium')) {
$variant = match(static::class) {
Base64::class => SODIUM_BASE64_VARIANT_ORIGINAL_NO_PADDING,
Base64UrlSafe::class => SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING,
default => 0,
};
if ($variant > 0) {
try {
return sodium_bin2base64($src, $variant);
} catch (SodiumException $ex) {
throw new RangeException($ex->getMessage(), $ex->getCode(), $ex);
}
}
}
return static::doEncode($src, false);
}
/**
* @param string $src
* @param bool $pad Include = padding?
* @return string
*
* @throws TypeError
*/
protected static function doEncode(
#[SensitiveParameter]
string $src,
bool $pad = true
): string {
$dest = '';
$srcLen = strlen($src);
// Main loop (no padding):
for ($i = 0; $i + 3 <= $srcLen; $i += 3) {
/** @var array<int, int> $chunk */
$chunk = unpack('C*', substr($src, $i, 3));
$b0 = $chunk[1];
$b1 = $chunk[2];
$b2 = $chunk[3];
$dest .=
static::encode6Bits( $b0 >> 2 ) .
static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
static::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) .
static::encode6Bits( $b2 & 63);
}
// The last chunk, which may have padding:
if ($i < $srcLen) {
/** @var array<int, int> $chunk */
$chunk = unpack('C*', substr($src, $i, $srcLen - $i));
$b0 = $chunk[1];
if ($i + 1 < $srcLen) {
$b1 = $chunk[2];
$dest .=
static::encode6Bits($b0 >> 2) .
static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
static::encode6Bits(($b1 << 2) & 63);
if ($pad) {
$dest .= '=';
}
} else {
$dest .=
static::encode6Bits( $b0 >> 2) .
static::encode6Bits(($b0 << 4) & 63);
if ($pad) {
$dest .= '==';
}
}
}
return $dest;
}
/**
* decode from base64 into binary
*
* Base64 character set "./[A-Z][a-z][0-9]"
*
* @param string $encodedString
* @param bool $strictPadding
* @return string
*
* @throws RangeException
* @throws TypeError
*/
#[Override]
public static function decode(
#[SensitiveParameter]
string $encodedString,
bool $strictPadding = false
): string {
// Remove padding
$srcLen = strlen($encodedString);
if ($srcLen === 0) {
return '';
}
if ($strictPadding) {
if (($srcLen & 3) === 0) {
if ($encodedString[$srcLen - 1] === '=') {
$srcLen--;
if ($encodedString[$srcLen - 1] === '=') {
$srcLen--;
}
}
}
if (($srcLen & 3) === 1) {
throw new RangeException(
'Incorrect padding'
);
}
if ($encodedString[$srcLen - 1] === '=') {
throw new RangeException(
'Incorrect padding'
);
}
if (extension_loaded('sodium')) {
$variant = match(static::class) {
Base64::class => SODIUM_BASE64_VARIANT_ORIGINAL_NO_PADDING,
Base64UrlSafe::class => SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING,
default => 0,
};
if ($variant > 0) {
try {
return sodium_base642bin(substr($encodedString, 0, $srcLen), $variant);
} catch (SodiumException $ex) {
throw new RangeException($ex->getMessage(), $ex->getCode(), $ex);
}
}
}
} else {
// Just remove all padding.
$encodedString = rtrim($encodedString, '=');
$srcLen = strlen($encodedString);
}
$err = 0;
$dest = '';
// Main loop (no padding):
for ($i = 0; $i + 4 <= $srcLen; $i += 4) {
/** @var array<int, int> $chunk */
$chunk = unpack('C*', substr($encodedString, $i, 4));
$c0 = static::decode6Bits($chunk[1]);
$c1 = static::decode6Bits($chunk[2]);
$c2 = static::decode6Bits($chunk[3]);
$c3 = static::decode6Bits($chunk[4]);
$dest .= pack(
'CCC',
((($c0 << 2) | ($c1 >> 4)) & 0xff),
((($c1 << 4) | ($c2 >> 2)) & 0xff),
((($c2 << 6) | $c3 ) & 0xff)
);
$err |= ($c0 | $c1 | $c2 | $c3) >> 8;
}
// The last chunk, which may have padding:
if ($i < $srcLen) {
/** @var array<int, int> $chunk */
$chunk = unpack('C*', substr($encodedString, $i, $srcLen - $i));
$c0 = static::decode6Bits($chunk[1]);
if ($i + 2 < $srcLen) {
$c1 = static::decode6Bits($chunk[2]);
$c2 = static::decode6Bits($chunk[3]);
$dest .= pack(
'CC',
((($c0 << 2) | ($c1 >> 4)) & 0xff),
((($c1 << 4) | ($c2 >> 2)) & 0xff)
);
$err |= ($c0 | $c1 | $c2) >> 8;
if ($strictPadding) {
$err |= ($c2 << 6) & 0xff;
}
} elseif ($i + 1 < $srcLen) {
$c1 = static::decode6Bits($chunk[2]);
$dest .= pack(
'C',
((($c0 << 2) | ($c1 >> 4)) & 0xff)
);
$err |= ($c0 | $c1) >> 8;
if ($strictPadding) {
$err |= ($c1 << 4) & 0xff;
}
} elseif ($strictPadding) {
$err |= 1;
}
}
$check = ($err === 0);
if (!$check) {
throw new RangeException(
'Base64::decode() only expects characters in the correct base64 alphabet'
);
}
return $dest;
}
/**
* @param string $encodedString
* @return string
* @api
*/
public static function decodeNoPadding(
#[SensitiveParameter]
string $encodedString
): string {
$srcLen = strlen($encodedString);
if ($srcLen === 0) {
return '';
}
if (($srcLen & 3) === 0) {
// If $strLen is not zero, and it is divisible by 4, then it's at least 4.
if ($encodedString[$srcLen - 1] === '=' || $encodedString[$srcLen - 2] === '=') {
throw new InvalidArgumentException(
"decodeNoPadding() doesn't tolerate padding"
);
}
}
return static::decode(
$encodedString,
true
);
}
/**
* Uses bitwise operators instead of table-lookups to turn 6-bit integers
* into 8-bit integers.
*
* Base64 character set:
* [A-Z] [a-z] [0-9] + /
* 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f
*
* @param int $src
* @return int
*/
protected static function decode6Bits(int $src): int
{
$ret = -1;
// if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64
$ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
// if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70
$ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);
// if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5
$ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);
// if ($src == 0x2b) $ret += 62 + 1;
$ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63;
// if ($src == 0x2f) ret += 63 + 1;
$ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64;
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 6-bit integers.
*
* @param int $src
* @return string
*/
protected static function encode6Bits(int $src): string
{
$diff = 0x41;
// if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6
$diff += ((25 - $src) >> 8) & 6;
// if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75
$diff -= ((51 - $src) >> 8) & 75;
// if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15
$diff -= ((61 - $src) >> 8) & 15;
// if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3
$diff += ((62 - $src) >> 8) & 3;
return pack('C', $src + $diff);
}
}
@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use Override;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Base64DotSlash
* ./[A-Z][a-z][0-9]
*
* @package ParagonIE\ConstantTime
*/
abstract class Base64DotSlash extends Base64
{
/**
* Uses bitwise operators instead of table-lookups to turn 6-bit integers
* into 8-bit integers.
*
* Base64 character set:
* ./ [A-Z] [a-z] [0-9]
* 0x2e-0x2f, 0x41-0x5a, 0x61-0x7a, 0x30-0x39
*
* @param int $src
* @return int
*/
#[Override]
protected static function decode6Bits(int $src): int
{
$ret = -1;
// if ($src > 0x2d && $src < 0x30) ret += $src - 0x2e + 1; // -45
$ret += (((0x2d - $src) & ($src - 0x30)) >> 8) & ($src - 45);
// if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 2 + 1; // -62
$ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 62);
// if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 28 + 1; // -68
$ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 68);
// if ($src > 0x2f && $src < 0x3a) ret += $src - 0x30 + 54 + 1; // 7
$ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 7);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 6-bit integers.
*
* @param int $src
* @return string
*/
#[Override]
protected static function encode6Bits(int $src): string
{
$src += 0x2e;
// if ($src > 0x2f) $src += 0x41 - 0x30; // 17
$src += ((0x2f - $src) >> 8) & 17;
// if ($src > 0x5a) $src += 0x61 - 0x5b; // 6
$src += ((0x5a - $src) >> 8) & 6;
// if ($src > 0x7a) $src += 0x30 - 0x7b; // -75
$src -= ((0x7a - $src) >> 8) & 75;
return \pack('C', $src);
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use Override;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Base64DotSlashOrdered
* ./[0-9][A-Z][a-z]
*
* @package ParagonIE\ConstantTime
*/
abstract class Base64DotSlashOrdered extends Base64
{
/**
* Uses bitwise operators instead of table-lookups to turn 6-bit integers
* into 8-bit integers.
*
* Base64 character set:
* [.-9] [A-Z] [a-z]
* 0x2e-0x39, 0x41-0x5a, 0x61-0x7a
*
* @param int $src
* @return int
*/
#[Override]
protected static function decode6Bits(int $src): int
{
$ret = -1;
// if ($src > 0x2d && $src < 0x3a) ret += $src - 0x2e + 1; // -45
$ret += (((0x2d - $src) & ($src - 0x3a)) >> 8) & ($src - 45);
// if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 12 + 1; // -52
$ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 52);
// if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 38 + 1; // -58
$ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 58);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 6-bit integers.
*
* @param int $src
* @return string
*/
#[Override]
protected static function encode6Bits(int $src): string
{
$src += 0x2e;
// if ($src > 0x39) $src += 0x41 - 0x3a; // 7
$src += ((0x39 - $src) >> 8) & 7;
// if ($src > 0x5a) $src += 0x61 - 0x5b; // 6
$src += ((0x5a - $src) >> 8) & 6;
return \pack('C', $src);
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use Override;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Base64UrlSafe
* [A-Z][a-z][0-9]\-_
*
* @package ParagonIE\ConstantTime
*/
abstract class Base64UrlSafe extends Base64
{
/**
* Uses bitwise operators instead of table-lookups to turn 6-bit integers
* into 8-bit integers.
*
* Base64 character set:
* [A-Z] [a-z] [0-9] - _
* 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2d, 0x5f
*
* @param int $src
* @return int
*/
#[Override]
protected static function decode6Bits(int $src): int
{
$ret = -1;
// if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64
$ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
// if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70
$ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);
// if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5
$ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);
// if ($src == 0x2c) $ret += 62 + 1;
$ret += (((0x2c - $src) & ($src - 0x2e)) >> 8) & 63;
// if ($src == 0x5f) ret += 63 + 1;
$ret += (((0x5e - $src) & ($src - 0x60)) >> 8) & 64;
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 6-bit integers.
*
* @param int $src
* @return string
*/
#[Override]
protected static function encode6Bits(int $src): string
{
$diff = 0x41;
// if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6
$diff += ((25 - $src) >> 8) & 6;
// if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75
$diff -= ((51 - $src) >> 8) & 75;
// if ($src > 61) $diff += 0x2d - 0x30 - 10; // -13
$diff -= ((61 - $src) >> 8) & 13;
// if ($src > 62) $diff += 0x5f - 0x2b - 1; // 3
$diff += ((62 - $src) >> 8) & 49;
return \pack('C', $src + $diff);
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use SensitiveParameter;
use TypeError;
use function strlen;
use function substr;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Binary
*
* Binary string operators that don't choke on
* mbstring.func_overload
*
* @package ParagonIE\ConstantTime
*/
abstract class Binary
{
/**
* Safe string length
*
* @ref mbstring.func_overload
*
* @param string $str
* @return int
*/
public static function safeStrlen(
#[SensitiveParameter]
string $str
): int {
return strlen($str);
}
/**
* Safe substring
*
* @ref mbstring.func_overload
*
* @staticvar boolean $exists
* @param string $str
* @param int $start
* @param ?int $length
* @return string
*
* @throws TypeError
*/
public static function safeSubstr(
#[SensitiveParameter]
string $str,
int $start = 0,
?int $length = null
): string {
if ($length === 0) {
return '';
}
// Unlike mb_substr(), substr() doesn't accept NULL for length
if ($length !== null) {
return substr($str, $start, $length);
} else {
return substr($str, $start);
}
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use SensitiveParameter;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Interface EncoderInterface
* @package ParagonIE\ConstantTime
*/
interface EncoderInterface
{
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $binString (raw binary)
* @return string
*/
public static function encode(
#[SensitiveParameter]
string $binString
): string;
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $encodedString
* @param bool $strictPadding Error on invalid padding
* @return string (raw binary)
*/
public static function decode(
#[SensitiveParameter]
string $encodedString,
bool $strictPadding = false
): string;
}
+301
View File
@@ -0,0 +1,301 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use RangeException;
use SensitiveParameter;
use TypeError;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Encoding
* @package ParagonIE\ConstantTime
* @api
*/
abstract class Encoding
{
/**
* RFC 4648 Base32 encoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32Encode(
#[SensitiveParameter]
string $str
): string {
return Base32::encode($str);
}
/**
* RFC 4648 Base32 encoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32EncodeUpper(
#[SensitiveParameter]
string $str
): string {
return Base32::encodeUpper($str);
}
/**
* RFC 4648 Base32 decoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32Decode(
#[SensitiveParameter]
string $str
): string {
return Base32::decode($str);
}
/**
* RFC 4648 Base32 decoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32DecodeUpper(
#[SensitiveParameter]
string $str
): string {
return Base32::decodeUpper($str);
}
/**
* RFC 4648 Base32 encoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32HexEncode(
#[SensitiveParameter]
string $str
): string {
return Base32Hex::encode($str);
}
/**
* RFC 4648 Base32Hex encoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32HexEncodeUpper(
#[SensitiveParameter]
string $str
): string {
return Base32Hex::encodeUpper($str);
}
/**
* RFC 4648 Base32Hex decoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32HexDecode(
#[SensitiveParameter]
string $str
): string {
return Base32Hex::decode($str);
}
/**
* RFC 4648 Base32Hex decoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32HexDecodeUpper(
#[SensitiveParameter]
string $str
): string {
return Base32Hex::decodeUpper($str);
}
/**
* RFC 4648 Base64 encoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base64Encode(
#[SensitiveParameter]
string $str
): string {
return Base64::encode($str);
}
/**
* RFC 4648 Base64 decoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base64Decode(
#[SensitiveParameter]
string $str
): string {
return Base64::decode($str);
}
/**
* Encode into Base64
*
* Base64 character set "./[A-Z][a-z][0-9]"
* @param string $str
* @return string
* @throws TypeError
*/
public static function base64EncodeDotSlash(
#[SensitiveParameter]
string $str
): string {
return Base64DotSlash::encode($str);
}
/**
* Decode from base64 to raw binary
*
* Base64 character set "./[A-Z][a-z][0-9]"
*
* @param string $str
* @return string
* @throws RangeException
* @throws TypeError
*/
public static function base64DecodeDotSlash(
#[SensitiveParameter]
string $str
): string {
return Base64DotSlash::decode($str);
}
/**
* Encode into Base64
*
* Base64 character set "[.-9][A-Z][a-z]" or "./[0-9][A-Z][a-z]"
* @param string $str
* @return string
* @throws TypeError
*/
public static function base64EncodeDotSlashOrdered(
#[SensitiveParameter]
string $str
): string {
return Base64DotSlashOrdered::encode($str);
}
/**
* Decode from base64 to raw binary
*
* Base64 character set "[.-9][A-Z][a-z]" or "./[0-9][A-Z][a-z]"
*
* @param string $str
* @return string
* @throws RangeException
* @throws TypeError
*/
public static function base64DecodeDotSlashOrdered(
#[SensitiveParameter]
string $str
): string {
return Base64DotSlashOrdered::decode($str);
}
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $bin_string (raw binary)
* @return string
* @throws TypeError
*/
public static function hexEncode(
#[SensitiveParameter]
string $bin_string
): string {
return Hex::encode($bin_string);
}
/**
* Convert a hexadecimal string into a binary string without cache-timing
* leaks
*
* @param string $hex_string
* @return string (raw binary)
* @throws RangeException
*/
public static function hexDecode(
#[SensitiveParameter]
string $hex_string
): string {
return Hex::decode($hex_string);
}
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $bin_string (raw binary)
* @return string
* @throws TypeError
*/
public static function hexEncodeUpper(
#[SensitiveParameter]
string $bin_string
): string {
return Hex::encodeUpper($bin_string);
}
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $bin_string (raw binary)
* @return string
*/
public static function hexDecodeUpper(
#[SensitiveParameter]
string $bin_string
): string {
return Hex::decode($bin_string);
}
}
+176
View File
@@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use Override;
use RangeException;
use SensitiveParameter;
use SodiumException;
use TypeError;
use function extension_loaded;
use function pack;
use function sodium_bin2hex;
use function sodium_hex2bin;
use function strlen;
use function unpack;
/**
* Copyright (c) 2016 - 2025 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Hex
* @package ParagonIE\ConstantTime
*/
abstract class Hex implements EncoderInterface
{
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $binString (raw binary)
* @return string
* @throws TypeError
*/
#[Override]
public static function encode(
#[SensitiveParameter]
string $binString
): string {
if (extension_loaded('sodium')) {
try {
return sodium_bin2hex($binString);
} catch (SodiumException $ex) {
throw new RangeException($ex->getMessage(), $ex->getCode(), $ex);
}
}
$hex = '';
$len = strlen($binString);
for ($i = 0; $i < $len; ++$i) {
/** @var array<int, int> $chunk */
$chunk = unpack('C', $binString[$i]);
$c = $chunk[1] & 0xf;
$b = $chunk[1] >> 4;
$hex .= pack(
'CC',
(87 + $b + ((($b - 10) >> 8) & ~38)),
(87 + $c + ((($c - 10) >> 8) & ~38))
);
}
return $hex;
}
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks, returning uppercase letters (as per RFC 4648)
*
* @param string $binString (raw binary)
* @return string
* @throws TypeError
*/
public static function encodeUpper(
#[SensitiveParameter]
string $binString
): string {
$hex = '';
$len = strlen($binString);
for ($i = 0; $i < $len; ++$i) {
/** @var array<int, int> $chunk */
$chunk = unpack('C', $binString[$i]);
$c = $chunk[1] & 0xf;
$b = $chunk[1] >> 4;
$hex .= pack(
'CC',
(55 + $b + ((($b - 10) >> 8) & ~6)),
(55 + $c + ((($c - 10) >> 8) & ~6))
);
}
return $hex;
}
/**
* Convert a hexadecimal string into a binary string without cache-timing
* leaks
*
* @param string $encodedString
* @param bool $strictPadding
* @return string (raw binary)
* @throws RangeException
*/
#[Override]
public static function decode(
#[SensitiveParameter]
string $encodedString,
bool $strictPadding = false
): string {
if (extension_loaded('sodium') && $strictPadding) {
try {
return sodium_hex2bin($encodedString);
} catch (SodiumException $ex) {
throw new RangeException($ex->getMessage(), $ex->getCode(), $ex);
}
}
$hex_pos = 0;
$bin = '';
$c_acc = 0;
$hex_len = strlen($encodedString);
$state = 0;
if (($hex_len & 1) !== 0) {
if ($strictPadding) {
throw new RangeException(
'Expected an even number of hexadecimal characters'
);
} else {
$encodedString = '0' . $encodedString;
++$hex_len;
}
}
/** @var array<int, int> $chunk */
$chunk = unpack('C*', $encodedString);
while ($hex_pos < $hex_len) {
++$hex_pos;
$c = $chunk[$hex_pos];
$c_num = $c ^ 48;
$c_num0 = ($c_num - 10) >> 8;
$c_alpha = ($c & ~32) - 55;
$c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8;
if (($c_num0 | $c_alpha0) === 0) {
throw new RangeException(
'Expected hexadecimal character'
);
}
$c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0);
if ($state === 0) {
$c_acc = $c_val * 16;
} else {
$bin .= pack('C', $c_acc | $c_val);
}
$state ^= 1;
}
return $bin;
}
}
+208
View File
@@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use SensitiveParameter;
use TypeError;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class RFC4648
*
* This class conforms strictly to the RFC
*
* @package ParagonIE\ConstantTime
* @api
*/
abstract class RFC4648
{
/**
* RFC 4648 Base64 encoding
*
* "foo" -> "Zm9v"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base64Encode(
#[SensitiveParameter]
string $str
): string {
return Base64::encode($str);
}
/**
* RFC 4648 Base64 decoding
*
* "Zm9v" -> "foo"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base64Decode(
#[SensitiveParameter]
string $str
): string {
return Base64::decode($str, true);
}
/**
* RFC 4648 Base64 (URL Safe) encoding
*
* "foo" -> "Zm9v"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base64UrlSafeEncode(
#[SensitiveParameter]
string $str
): string {
return Base64UrlSafe::encode($str);
}
/**
* RFC 4648 Base64 (URL Safe) decoding
*
* "Zm9v" -> "foo"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base64UrlSafeDecode(
#[SensitiveParameter]
string $str
): string {
return Base64UrlSafe::decode($str, true);
}
/**
* RFC 4648 Base32 encoding
*
* "foo" -> "MZXW6==="
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base32Encode(
#[SensitiveParameter]
string $str
): string {
return Base32::encodeUpper($str);
}
/**
* RFC 4648 Base32 encoding
*
* "MZXW6===" -> "foo"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base32Decode(
#[SensitiveParameter]
string $str
): string {
return Base32::decodeUpper($str, true);
}
/**
* RFC 4648 Base32-Hex encoding
*
* "foo" -> "CPNMU==="
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base32HexEncode(
#[SensitiveParameter]
string $str
): string {
return Base32::encodeUpper($str);
}
/**
* RFC 4648 Base32-Hex decoding
*
* "CPNMU===" -> "foo"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base32HexDecode(
#[SensitiveParameter]
string $str
): string {
return Base32::decodeUpper($str, true);
}
/**
* RFC 4648 Base16 decoding
*
* "foo" -> "666F6F"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base16Encode(
#[SensitiveParameter]
string $str
): string {
return Hex::encodeUpper($str);
}
/**
* RFC 4648 Base16 decoding
*
* "666F6F" -> "foo"
*
* @param string $str
* @return string
*/
public static function base16Decode(
#[SensitiveParameter]
string $str
): string {
return Hex::decode($str, true);
}
}
+11
View File
@@ -0,0 +1,11 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 1.0.0
First stable release after PSR-20 acceptance
## 0.1.0
First release
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2017 PHP Framework Interoperability Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+61
View File
@@ -0,0 +1,61 @@
# PSR Clock
This repository holds the interface for [PSR-20][psr-url].
Note that this is not a clock of its own. It is merely an interface that
describes a clock. See the specification for more details.
## Installation
```bash
composer require psr/clock
```
## Usage
If you need a clock, you can use the interface like this:
```php
<?php
use Psr\Clock\ClockInterface;
class Foo
{
private ClockInterface $clock;
public function __construct(ClockInterface $clock)
{
$this->clock = $clock;
}
public function doSomething()
{
/** @var DateTimeImmutable $currentDateAndTime */
$currentDateAndTime = $this->clock->now();
// do something useful with that information
}
}
```
You can then pick one of the [implementations][implementation-url] of the interface to get a clock.
If you want to implement the interface, you can require this package and
implement `Psr\Clock\ClockInterface` in your code.
Don't forget to add `psr/clock-implementation` to your `composer.json`s `provides`-section like this:
```json
{
"provides": {
"psr/clock-implementation": "1.0"
}
}
```
And please read the [specification text][specification-url] for details on the interface.
[psr-url]: https://www.php-fig.org/psr/psr-20
[package-url]: https://packagist.org/packages/psr/clock
[implementation-url]: https://packagist.org/providers/psr/clock-implementation
[specification-url]: https://github.com/php-fig/fig-standards/blob/master/proposed/clock.md
+21
View File
@@ -0,0 +1,21 @@
{
"name": "psr/clock",
"description": "Common interface for reading the clock.",
"keywords": ["psr", "psr-20", "time", "clock", "now"],
"homepage": "https://github.com/php-fig/clock",
"license": "MIT",
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"require": {
"php": "^7.0 || ^8.0"
},
"autoload": {
"psr-4": {
"Psr\\Clock\\": "src/"
}
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace Psr\Clock;
use DateTimeImmutable;
interface ClockInterface
{
/**
* Returns the current time as a DateTimeImmutable Object
*/
public function now(): DateTimeImmutable;
}
+46
View File
@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@spomky-labs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014-2016 Florent Morselli
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+42
View File
@@ -0,0 +1,42 @@
TOTP / HOTP library in PHP
==========================
[![Build Status](https://github.com/spomky-labs/otphp/actions/workflows/ci.yml/badge.svg)](https://github.com/spomky-labs/otphp/actions/workflows/ci.yml)
[![Latest Stable Version](https://poser.pugx.org/spomky-labs/otphp/v/stable.svg)](https://packagist.org/packages/spomky-labs/otphp)
[![Total Downloads](https://poser.pugx.org/spomky-labs/otphp/downloads.svg)](https://packagist.org/packages/spomky-labs/otphp)
[![Latest Unstable Version](https://poser.pugx.org/spomky-labs/otphp/v/unstable.svg)](https://packagist.org/packages/spomky-labs/otphp)
[![License](https://poser.pugx.org/spomky-labs/otphp/license.svg)](https://packagist.org/packages/spomky-labs/otphp)
A php library for generating one-time passwords according to [RFC 4226](https://datatracker.ietf.org/doc/html/rfc4226) (HOTP Algorithm) and [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238) (TOTP Algorithm)
This library is compatible with Google Authenticator apps available for Android and iPhone.
It is also compatible with other applications such as [FreeOTP](https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp) for example.
# Documentation
The documentation of this project is available in the [*doc* folder](doc/index.md).
# Support
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!
[Become a sponsor](https://github.com/sponsors/Spomky)
Or
[![Become a Patreon](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/FlorentMorselli)
## Contributing
Requests for new features, bug fixes and all other ideas to make this project useful are welcome.
Please report all issues in [the repository bug tracker](https://github.com/Spomky-Labs/otphp/issues).
Also make sure to [follow these best practices](.github/CONTRIBUTING.md).
## Licence
This software is release under the [MIT licence](LICENSE).
+18
View File
@@ -0,0 +1,18 @@
# Versioning and Release
This document describes the versioning and release process of OTPHP.
This document is a living document, contents will be updated according to each release.
## Releases
OTPHP releases will be versioned using dotted triples, similar to [Semantic Version](http://semver.org/).
For this specific document, we will refer to the respective components of this triple as `<major>.<minor>.<patch>`.
The version number may have additional information, such as "-rc1,-rc2,-rc3" to mark release candidate builds for earlier access.
Such releases will be considered as "pre-releases".
## Minor Release Support Matrix
| Version | Supported |
|----------|--------------------|
| 11.4.x | :white_check_mark: |
| < 11.4.x | :x: |
+74
View File
@@ -0,0 +1,74 @@
# Security Release Process
OTPHP is devoted to providing the best experience for all developers who want to implement OTP authentication in their applications.
Spomky-Labs has adopted this security disclosure and response policy to ensure we responsibly handle critical issues.
## Supported Versions
The OTPHP project maintains release branches for the three most recent minor releases.
Applicable fixes, including security fixes, may be backported to those three release branches, depending on severity and feasibility. Please refer to [RELEASES.md](RELEASES.md) for details.
## Reporting a Vulnerability - Private Disclosure Process
Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to OTPHP privately, to minimize attacks against current users of OTPHP before they are fixed.
Vulnerabilities will be investigated and patched on the next patch (or minor) release as soon as possible.
This information could be kept entirely internal to the project.
If you know of a publicly disclosed security vulnerability for OTPHP, please **IMMEDIATELY** contact security@spomky-labs.com to inform the OTPHP Security Team.
**IMPORTANT: Do not file public issues on GitHub for security vulnerabilities**
To report a vulnerability or a security-related issue, please email the private address security@spomky-labs.com with the details of the vulnerability.
The email will be fielded by the OTPHP Security Team, which is made up of the OTPHP maintainers who have committer and release permissions.
Do not report non-security-impacting bugs through this channel. Use [GitHub issues](https://github.com/Spomky-Labs/otphp/issues/new/choose) instead.
Emails can be encrypted if you wish to share the vulnerability details securely.
The OTPHP Security Team's PGP key is available on the [PGP keyservers](https://keys.openpgp.org/search?q=security%40spomky-labs.com).
### Proposed Email Content
Provide a descriptive subject line and in the body of the email include the following information:
- Basic identity information, such as your name and your affiliation or company.
- Detailed steps to reproduce the vulnerability (POC scripts, screenshots, and compressed packet captures are all helpful to us).
- Description of the effects of the vulnerability on OTPHP and the related hardware and software configurations, so that the OTPHP Security Team can reproduce it.
- How the vulnerability affects OTPHP usage and an estimation of the attack surface, if there is one.
- List other projects or dependencies that were used in conjunction with OTPHP to produce the vulnerability.
## When to report a vulnerability
- When you think OTPHP has a potential security vulnerability.
- When you suspect a potential vulnerability, but you are unsure that it impacts OTPHP.
- When you know of or suspect a potential vulnerability on another project that is used by OTPHP.
## Patch, Release, and Disclosure
The OTPHP Security Team will respond to vulnerability reports as follows:
1. The Security Team will investigate the vulnerability and determine its effects and criticality.
2. If the issue is not deemed to be a vulnerability, the Security Team will follow up with a detailed reason for rejection.
3. The Security Team will initiate a conversation with the reporter as soon as possible.
4. If a vulnerability is acknowledged and the timeline for a fix is determined, the Security Team will work on a plan to communicate with the appropriate community, including identifying mitigating steps that affected users can take to protect themselves until the fix is rolled out.
5. The Security Team will work on fixing the vulnerability and perform internal testing before preparing to roll out the fix.
6. A public disclosure date is negotiated by the OTPHP Security Team and the bug submitter. We prefer to fully disclose the bug as soon as possible once a user mitigation or patch is available. It is reasonable to delay disclosure when the bug or the fix is not yet fully understood, the solution is not well-tested, or for distributor coordination. The timeframe for disclosure is from immediate (especially if it's already publicly known) to a few weeks. For a critical vulnerability with a straightforward mitigation, we expect report date to public disclosure date to be on the order of 14 business days. The OTPHP Security Team holds the final say when setting a public disclosure date.
7. Once the fix is confirmed, the Security Team will patch the vulnerability in the next patch or minor release, and backport a patch release into all earlier supported releases. Upon release of the patched version of OTPHP, we will follow the **Public Disclosure Process**.
### Public Disclosure Process
The Security Team publishes a public [advisory](https://github.com/Spomky-Labs/otphp/security/advisories) to the OTPHP community via GitHub. In most cases, additional communication via Twitter, blog and other channels will assist in educating OTPHP users and rolling out the patched release to affected users.
The Security Team will also publish any mitigating steps users can take until the fix can be applied to their OTPHP instances. OTPHP distributors will handle creating and publishing their own security advisories.
## Mailing lists
- Use security@spomky-labs.com to report security concerns to the OTPHP Security Team, who uses the list to privately discuss security issues and fixes prior to disclosure.
## Early Disclosure to OTPHP Distributors List
This private list is intended to be used primarily to provide actionable information to multiple distributor projects at once. This list is not intended to inform individuals about security issues.
## Confidentiality, integrity and availability
We consider vulnerabilities leading to the compromise of data confidentiality, elevation of privilege, or integrity to be our highest priority concerns.
Availability, in particular in areas relating to DoS and resource exhaustion, is also a serious security concern.
The OTPHP Security Team takes all vulnerabilities, potential vulnerabilities, and suspected vulnerabilities seriously and will investigate them in an urgent and expeditious manner.
+47
View File
@@ -0,0 +1,47 @@
{
"name": "spomky-labs/otphp",
"type": "library",
"description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator",
"license": "MIT",
"keywords": ["otp", "hotp", "totp", "RFC 4226", "RFC 6238", "Google Authenticator", "FreeOTP"],
"homepage": "https://github.com/Spomky-Labs/otphp",
"authors": [
{
"name": "Florent Morselli",
"homepage": "https://github.com/Spomky"
},
{
"name": "All contributors",
"homepage": "https://github.com/Spomky-Labs/otphp/contributors"
}
],
"require": {
"php": ">=8.1",
"paragonie/constant_time_encoding": "^2.0 || ^3.0",
"psr/clock": "^1.0",
"symfony/deprecation-contracts": "^3.2"
},
"require-dev": {
"symfony/error-handler": "^6.4|^7.0|^8.0"
},
"autoload": {
"psr-4": { "OTPHP\\": "src/" }
},
"autoload-dev": {
"psr-4": { "OTPHP\\Test\\": "tests/" }
},
"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
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace OTPHP\Exception;
use InvalidArgumentException;
use Throwable;
/**
* Exception thrown when a label or issuer format is invalid.
* This includes violations of the Google Authenticator label specification.
*/
final class InvalidLabelException extends InvalidArgumentException implements OTPExceptionInterface
{
public function __construct(
string $message,
public readonly string $labelName = '',
public readonly mixed $labelValue = null,
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace OTPHP\Exception;
use InvalidArgumentException;
use Throwable;
/**
* Exception thrown when an OTP parameter has an invalid value.
* This includes: secret, digits, algorithm, period, epoch, counter, secret size.
*/
final class InvalidParameterException extends InvalidArgumentException implements OTPExceptionInterface
{
public function __construct(
string $message,
public readonly string $parameterName = '',
public readonly mixed $parameterValue = null,
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace OTPHP\Exception;
use InvalidArgumentException;
/**
* Exception thrown when a provisioning URI cannot be parsed or is invalid.
* This includes: invalid format, wrong scheme, missing required parameters, unsupported OTP type.
*/
final class InvalidProvisioningUriException extends InvalidArgumentException implements OTPExceptionInterface
{
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace OTPHP\Exception;
use Throwable;
/**
* Marker interface for all OTPHP exceptions.
* This allows catching all OTPHP-specific exceptions while maintaining
* backward compatibility with PHP's built-in exception types.
*/
interface OTPExceptionInterface extends Throwable
{
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace OTPHP\Exception;
use InvalidArgumentException;
use Throwable;
/**
* Exception thrown when attempting to access a parameter that doesn't exist.
*/
final class ParameterNotFoundException extends InvalidArgumentException implements OTPExceptionInterface
{
public function __construct(
string $message,
public readonly string $parameterName = '',
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace OTPHP\Exception;
use RuntimeException;
/**
* Exception thrown when a secret cannot be decoded from Base32.
*/
final class SecretDecodingException extends RuntimeException implements OTPExceptionInterface
{
}
+124
View File
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use function count;
use OTPHP\Exception\InvalidProvisioningUriException;
use Psr\Clock\ClockInterface;
use function sprintf;
use Throwable;
/**
* This class is used to load OTP object from a provisioning Uri.
*
* @readonly
*
* @see \OTPHP\Test\FactoryTest
*/
final class Factory implements FactoryInterface
{
public static function loadFromProvisioningUri(string $uri, ?ClockInterface $clock = null): OTPInterface
{
try {
$parsed_url = Url::fromString($uri);
$parsed_url->getScheme() === 'otpauth' || throw new InvalidProvisioningUriException('Invalid scheme.');
} catch (Throwable $throwable) {
throw new InvalidProvisioningUriException(
'Not a valid OTP provisioning URI',
$throwable->getCode(),
$throwable
);
}
if ($clock === null) {
trigger_deprecation(
'spomky-labs/otphp',
'11.3.0',
'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".'
);
$clock = new InternalClock();
}
try {
$otp = self::createOTP($parsed_url, $clock);
self::populateOTP($otp, $parsed_url);
} catch (InvalidProvisioningUriException $exception) {
throw $exception;
} catch (Throwable $throwable) {
throw new InvalidProvisioningUriException(
'Not a valid OTP provisioning URI',
$throwable->getCode(),
$throwable
);
}
return $otp;
}
private static function populateParameters(OTPInterface $otp, Url $data): void
{
foreach ($data->getQuery() as $key => $value) {
$otp->setParameter($key, $value);
}
}
private static function populateOTP(OTPInterface $otp, Url $data): void
{
self::populateParameters($otp, $data);
$result = explode(':', rawurldecode(substr($data->getPath(), 1)));
if (count($result) < 2) {
$otp->setIssuerIncludedAsParameter(false);
return;
}
$issuerFromLabel = $result[0];
$issuerFromParameter = $otp->getIssuer();
if ($issuerFromParameter !== null) {
// Issuer parameter takes precedence over issuer in label
// According to Google Authenticator spec: "they should be equal" but not required to be
$otp->setIssuerIncludedAsParameter(true);
} else {
// No issuer parameter, use the issuer from label
$issuerFromLabel !== '' || throw new InvalidProvisioningUriException(
'Issuer from label must not be empty.'
);
$otp->setIssuer($issuerFromLabel);
}
}
private static function createOTP(Url $parsed_url, ClockInterface $clock): OTPInterface
{
switch ($parsed_url->getHost()) {
case 'totp':
$totp = TOTP::createFromSecret($parsed_url->getSecret(), $clock);
$totp->setLabel(self::getLabel($parsed_url->getPath()));
return $totp;
case 'hotp':
$hotp = HOTP::createFromSecret($parsed_url->getSecret());
$hotp->setLabel(self::getLabel($parsed_url->getPath()));
return $hotp;
default:
throw new InvalidProvisioningUriException(sprintf('Unsupported "%s" OTP type', $parsed_url->getHost()));
}
}
/**
* @param non-empty-string $data
* @return non-empty-string
*/
private static function getLabel(string $data): string
{
$result = explode(':', rawurldecode(substr($data, 1)));
$label = count($result) === 2 ? $result[1] : $result[0];
$label !== '' || throw new InvalidProvisioningUriException('Label must not be empty.');
return $label;
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use Psr\Clock\ClockInterface;
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.
*
* @param non-empty-string $uri
*/
public static function loadFromProvisioningUri(string $uri, ?ClockInterface $clock = null): OTPInterface;
}
+165
View File
@@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use function is_int;
use OTPHP\Exception\InvalidParameterException;
/**
* Note: This class is not marked as readonly because the verify() method
* updates the counter state on successful verification, which is an intentional
* side-effect to prevent OTP reuse.
*
* @see \OTPHP\Test\HOTPTest
*/
final class HOTP extends OTP implements HOTPInterface
{
private const DEFAULT_WINDOW = 0;
public static function create(
null|string $secret = null,
int $counter = self::DEFAULT_COUNTER,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS,
?int $secretSize = null
): self {
$htop = $secret !== null
? self::createFromSecret($secret)
: self::generate($secretSize)
;
$htop->setCounter($counter);
$htop->setDigest($digest);
$htop->setDigits($digits);
return $htop;
}
public static function createFromSecret(string $secret): self
{
$htop = new self($secret);
$htop->setCounter(self::DEFAULT_COUNTER);
$htop->setDigest(self::DEFAULT_DIGEST);
$htop->setDigits(self::DEFAULT_DIGITS);
return $htop;
}
/**
* @param positive-int|null $secretSize
*/
public static function generate(?int $secretSize = null): self
{
return self::createFromSecret(self::generateSecret($secretSize));
}
/**
* @return 0|positive-int
*/
public function getCounter(): int
{
$value = $this->getParameter('counter');
(is_int($value) && $value >= 0) || throw new InvalidParameterException(
'Invalid "counter" parameter.',
'counter',
$value
);
return $value;
}
public function getProvisioningUri(): string
{
return $this->generateURI('hotp', [
'counter' => $this->getCounter(),
]);
}
/**
* If the counter is not provided, the OTP is verified at the actual counter.
*
* @param null|0|positive-int $counter
*/
public function verify(string $otp, null|int $counter = null, null|int $window = null): bool
{
$counter >= 0 || throw new InvalidParameterException(
'The counter must be at least 0.',
'counter',
$counter
);
if ($counter === null) {
$counter = $this->getCounter();
} elseif ($counter < $this->getCounter()) {
return false;
}
return $this->verifyOtpWithWindow($otp, $counter, $window);
}
public function setCounter(int $counter): void
{
$this->setParameter('counter', $counter);
}
public function withCounter(int $counter): self
{
$otp = clone $this;
$otp->setParameter('counter', $counter);
return $otp;
}
/**
* @return array<non-empty-string, callable>
*/
protected function getParameterMap(): array
{
return [...parent::getParameterMap(), ...[
'counter' => static function (mixed $value): int {
$value = (int) $value;
$value >= 0 || throw new InvalidParameterException(
'Counter must be at least 0.',
'counter',
$value
);
return $value;
},
]];
}
private function updateCounter(int $counter): void
{
$this->setCounter($counter);
}
/**
* @param null|0|positive-int $window
*/
private function getWindow(null|int $window): int
{
return abs($window ?? self::DEFAULT_WINDOW);
}
/**
* @param non-empty-string $otp
* @param 0|positive-int $counter
* @param null|0|positive-int $window
*/
private function verifyOtpWithWindow(string $otp, int $counter, null|int $window): bool
{
$window = $this->getWindow($window);
for ($i = $counter; $i <= $counter + $window; ++$i) {
if ($this->compareOTP($this->at($i), $otp)) {
$this->updateCounter($i + 1);
return true;
}
}
return false;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace OTPHP;
interface HOTPInterface extends OTPInterface
{
public const DEFAULT_COUNTER = 0;
/**
* The initial counter (a positive integer).
*/
public function getCounter(): int;
/**
* Create a new HOTP object.
*
* If the secret is null, a random 64 bytes secret will be generated.
*
* @param null|non-empty-string $secret
* @param 0|positive-int $counter
* @param non-empty-string $digest
* @param positive-int $digits
*
* @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead
*/
public static function create(
null|string $secret = null,
int $counter = 0,
string $digest = 'sha1',
int $digits = 6
): self;
/**
* @deprecated Deprecated since v11.4, use {@see self::withCounter()} instead
*/
public function setCounter(int $counter): void;
public function withCounter(int $counter): self;
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use DateTimeImmutable;
use Psr\Clock\ClockInterface;
/**
* @readonly
*
* @internal
*/
final class InternalClock implements ClockInterface
{
public function now(): DateTimeImmutable
{
return new DateTimeImmutable();
}
}
+515
View File
@@ -0,0 +1,515 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use function array_key_exists;
use function chr;
use function count;
use Exception;
use function in_array;
use function is_int;
use function is_string;
use OTPHP\Exception\InvalidLabelException;
use OTPHP\Exception\InvalidParameterException;
use OTPHP\Exception\ParameterNotFoundException;
use OTPHP\Exception\SecretDecodingException;
use ParagonIE\ConstantTime\Base32;
use function sprintf;
use const STR_PAD_LEFT;
use function strlen;
/**
* @readonly
*/
abstract class OTP implements OTPInterface
{
private const DEFAULT_SECRET_SIZE = 64;
/**
* Minimum digest size, in bytes, required by the RFC 4226 dynamic truncation.
*
* The truncation reads four bytes starting at an offset taken from the low
* nibble of the last digest byte, i.e. an offset in the range [0, 15]. It
* therefore reads up to index "offset + 3" = 18 and needs at least 19 bytes.
* A shorter digest (e.g. MD5, 16 bytes) makes the truncation read past the
* end of the hash, collapsing the output to a small, secret-independent set
* of values. Such algorithms are also outside RFC 4226/6238 and are not
* interoperable with authenticator apps. See {@see self::generateOTP()}.
*/
private const MINIMUM_DIGEST_SIZE = 19;
/**
* @var array<non-empty-string, mixed>
*/
private array $parameters = [];
/**
* @var non-empty-string|null
*/
private null|string $issuer = null;
/**
* @var non-empty-string|null
*/
private null|string $label = null;
private bool $issuer_included_as_parameter = true;
/**
* @param non-empty-string $secret
*/
protected function __construct(string $secret)
{
$this->setSecret($secret);
}
public function getQrCodeUri(string $uri, string $placeholder): string
{
$provisioning_uri = urlencode($this->getProvisioningUri());
return str_replace($placeholder, $provisioning_uri, $uri);
}
/**
* @param 0|positive-int $input
*/
public function at(int $input): string
{
return $this->generateOTP($input);
}
/**
* @return array<non-empty-string, mixed>
*/
public function getParameters(): array
{
$parameters = $this->parameters;
if ($this->getIssuer() !== null && $this->isIssuerIncludedAsParameter() === true) {
$parameters['issuer'] = $this->getIssuer();
}
return $parameters;
}
public function getSecret(): string
{
$value = $this->getParameter('secret');
(is_string($value) && $value !== '') || throw new InvalidParameterException(
'Invalid "secret" parameter.',
'secret',
$value
);
return $value;
}
public function getLabel(): null|string
{
return $this->label;
}
public function setLabel(string $label): void
{
$this->setParameter('label', $label);
}
public function withLabel(string $label): static
{
$otp = clone $this;
$otp->setParameter('label', $label);
return $otp;
}
public function getIssuer(): null|string
{
return $this->issuer;
}
public function setIssuer(string $issuer): void
{
$this->setParameter('issuer', $issuer);
}
public function withIssuer(string $issuer): static
{
$otp = clone $this;
$otp->setParameter('issuer', $issuer);
return $otp;
}
public function isIssuerIncludedAsParameter(): bool
{
return $this->issuer_included_as_parameter;
}
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void
{
$this->issuer_included_as_parameter = $issuer_included_as_parameter;
}
public function withIssuerIncludedAsParameter(bool $issuer_included_as_parameter): static
{
$otp = clone $this;
$otp->issuer_included_as_parameter = $issuer_included_as_parameter;
return $otp;
}
public function getDigits(): int
{
$value = $this->getParameter('digits');
(is_int($value) && $value >= 1 && $value <= self::MAX_DIGITS) || throw new InvalidParameterException(
'Invalid "digits" parameter.',
'digits',
$value
);
return $value;
}
public function getDigest(): string
{
$value = $this->getParameter('algorithm');
(is_string($value) && $value !== '') || throw new InvalidParameterException(
'Invalid "algorithm" parameter.',
'algorithm',
$value
);
return $value;
}
public function hasParameter(string $parameter): bool
{
return array_key_exists($parameter, $this->parameters);
}
public function getParameter(string $parameter): mixed
{
if ($this->hasParameter($parameter)) {
return $this->getParameters()[$parameter];
}
throw new ParameterNotFoundException(sprintf('Parameter "%s" does not exist', $parameter), $parameter);
}
public function setParameter(string $parameter, mixed $value): void
{
$map = $this->getParameterMap();
if (array_key_exists($parameter, $map) === true) {
$callback = $map[$parameter];
$value = $callback($value);
}
if (in_array($parameter, ['label', 'issuer'], true)) {
$this->{$parameter} = $value;
} else {
$this->parameters[$parameter] = $value;
}
}
public function withParameter(string $parameter, mixed $value): static
{
$otp = clone $this;
$otp->setParameter($parameter, $value);
return $otp;
}
public function setSecret(string $secret): void
{
$this->setParameter('secret', $secret);
}
public function withSecret(string $secret): static
{
$otp = clone $this;
$otp->setParameter('secret', $secret);
return $otp;
}
public function setDigits(int $digits): void
{
$this->setParameter('digits', $digits);
}
public function withDigits(int $digits): static
{
$otp = clone $this;
$otp->setParameter('digits', $digits);
return $otp;
}
public function setDigest(string $digest): void
{
$this->setParameter('algorithm', $digest);
}
public function withDigest(string $digest): static
{
$otp = clone $this;
$otp->setParameter('algorithm', $digest);
return $otp;
}
/**
* @param positive-int|null $secretSize
*
* @return non-empty-string
*/
final protected static function generateSecret(?int $secretSize = null): string
{
$secretSize ??= self::DEFAULT_SECRET_SIZE;
$secretSize > 0 || throw new InvalidParameterException(
'Secret size must be at least 1.',
'secretSize',
$secretSize
);
return Base32::encodeUpper(random_bytes($secretSize));
}
/**
* The OTP at the specified input.
*
* @param 0|positive-int $input
*
* @return non-empty-string
*/
protected function generateOTP(int $input): string
{
$hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret(), true);
$unpacked = unpack('C*', $hash);
$unpacked !== false || throw new InvalidParameterException('Invalid data.', 'hash', $hash);
/** @var list<int> $hmac */
$hmac = array_values($unpacked);
$offset = ($hmac[count($hmac) - 1] & 0xF);
$code = ($hmac[$offset] & 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);
}
/**
* @param array<non-empty-string, mixed> $options
*/
protected function filterOptions(array &$options): void
{
foreach ([
'algorithm' => 'sha1',
'period' => 30,
'digits' => 6,
] as $key => $default) {
if (isset($options[$key]) && $default === $options[$key]) {
unset($options[$key]);
}
}
ksort($options);
}
/**
* @param non-empty-string $type
* @param array<non-empty-string, mixed> $options
*
* @return non-empty-string
*/
protected function generateURI(string $type, array $options): string
{
$options = [...$options, ...$this->getParameters()];
$this->filterOptions($options);
$params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options, '', '&'));
return sprintf('otpauth://%s/%s?%s', $type, rawurlencode($this->buildProvisioningUriLabel()), $params);
}
/**
* @param non-empty-string $safe
* @param non-empty-string $user
*/
protected function compareOTP(string $safe, string $user): bool
{
return hash_equals($safe, $user);
}
/**
* @return array<non-empty-string, callable>
*/
protected function getParameterMap(): array
{
return [
'label' => function (string $value): string {
$value !== '' || throw new InvalidLabelException('Label must not be empty.', 'label', $value);
$this->validateLabel($value);
return $value;
},
'secret' => static fn (string $value): string => strtoupper(trim($value, '=')),
'algorithm' => static function (string $value): string {
$value = strtolower($value);
in_array($value, hash_hmac_algos(), true) || throw new InvalidParameterException(
sprintf('The "%s" digest is not supported.', $value),
'algorithm',
$value
);
$size = strlen(hash($value, '', true));
$size >= self::MINIMUM_DIGEST_SIZE || throw new InvalidParameterException(
sprintf(
'The "%s" digest produces a %d-byte hash which is too short for the RFC 4226 dynamic truncation; at least %d bytes are required.',
$value,
$size,
self::MINIMUM_DIGEST_SIZE
),
'algorithm',
$value
);
return $value;
},
'digits' => static function ($value): int {
$value = (int) $value;
($value >= 1 && $value <= self::MAX_DIGITS) || throw new InvalidParameterException(
sprintf('Digits must be between 1 and %d.', self::MAX_DIGITS),
'digits',
$value
);
return $value;
},
'issuer' => function (string $value): string {
$value !== '' || throw new InvalidLabelException('Issuer must not be empty.', 'issuer', $value);
$this->hasColon($value) === false || throw new InvalidLabelException(
'Issuer must not contain a colon.',
'issuer',
$value
);
return $value;
},
];
}
/**
* @return non-empty-string
*/
private function getDecodedSecret(): string
{
try {
$decoded = Base32::decodeUpper($this->getSecret());
} catch (Exception) {
throw new SecretDecodingException('Unable to decode the secret. Is it correctly base32 encoded?');
}
$decoded !== '' || throw new SecretDecodingException('The decoded secret must not be empty.');
return $decoded;
}
private function intToByteString(int $int): string
{
$result = [];
while ($int !== 0) {
$result[] = chr($int & 0xFF);
$int >>= 8;
}
return str_pad(implode('', array_reverse($result)), 8, "\000", STR_PAD_LEFT);
}
/**
* @return non-empty-string
*/
private function buildProvisioningUriLabel(): string
{
$issuer = $this->getIssuer();
$label = $this->getLabel();
return match (true) {
$issuer === null && $label === null => throw new InvalidLabelException(
'The label is not set. Either label or issuer must be set.',
'label'
),
$label !== null && $this->hasColon($label) => throw new InvalidLabelException(
'Label must not contain a colon.',
'label',
$label
),
$issuer !== null && $label !== null => $issuer . ':' . $label,
$issuer !== null => $issuer,
default => $label,
};
}
/**
* Validates a label according to Google Authenticator spec:
* label = accountname / issuer (":" / "%3A") *"%20" accountname
* Neither issuer nor account name may themselves contain a colon.
*
* Valid examples:
* - alice@gmail.com
* - Provider1:Alice%20Smith
* - Big%20Corporation%3A%20alice%40bigco.com
*
* @param non-empty-string $value
*/
private function validateLabel(string $value): void
{
// Check for colon separators (literal or URL-encoded)
$hasLiteralColon = str_contains($value, ':');
$hasEncodedColon = str_contains($value, '%3A') || str_contains($value, '%3a');
if (! $hasLiteralColon && ! $hasEncodedColon) {
// Simple label (account name only) - no colons allowed anywhere
return;
}
// Label contains a separator - validate issuer:account format
// Split by literal or encoded colon
$parts = match (true) {
$hasLiteralColon => explode(':', $value, 2),
default => preg_split('/%3[Aa]/', $value, 2),
};
if ($parts === false || count($parts) !== 2) {
throw new InvalidLabelException('Label must not contain a colon.', 'label', $value);
}
[$issuerPart, $accountPart] = $parts;
// Remove leading %20 (spaces) from account part per spec: *"%20" accountname
$accountPart = ltrim($accountPart, '%20');
// Validate that neither part contains additional colons
if ($this->hasColon($issuerPart) || $this->hasColon($accountPart)) {
throw new InvalidLabelException(
'Neither issuer nor account name in label may contain a colon.',
'label',
$value
);
}
}
/**
* @param non-empty-string $value
*/
private function hasColon(string $value): bool
{
$colons = [':', '%3A', '%3a'];
foreach ($colons as $colon) {
if (str_contains($value, $colon)) {
return true;
}
}
return false;
}
}
+185
View File
@@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace OTPHP;
interface OTPInterface
{
public const DEFAULT_DIGITS = 6;
/**
* Upper bound for the "digits" parameter. The RFC 4226 dynamic truncation
* yields a 31-bit value (max 2 147 483 647, i.e. 10 digits); above this
* bound the extra digits carry no entropy and "10 ** digits" overflows
* PHP's integer range, leading to a DivisionByZeroError during generation.
*/
public const MAX_DIGITS = 10;
public const DEFAULT_DIGEST = 'sha1';
/**
* Create a OTP object from an existing secret.
*
* @param non-empty-string $secret
*/
public static function createFromSecret(string $secret): self;
/**
* Create a new OTP object. A random 64 bytes secret will be generated.
*/
public static function generate(): self;
/**
* @param non-empty-string $secret
*
* @deprecated Deprecated since v11.4, use {@see self::withSecret()} instead
*/
public function setSecret(string $secret): void;
/**
* @param non-empty-string $secret
*/
public function withSecret(string $secret): self;
/**
* @deprecated Deprecated since v11.4, use {@see self::withDigits()} instead
*/
public function setDigits(int $digits): void;
public function withDigits(int $digits): self;
/**
* @param non-empty-string $digest
*
* @deprecated Deprecated since v11.4, use {@see self::withDigest()} instead
*/
public function setDigest(string $digest): void;
/**
* @param non-empty-string $digest
*/
public function withDigest(string $digest): self;
/**
* Generate the OTP at the specified input.
*
* @param 0|positive-int $input
*
* @return non-empty-string Return the OTP at the specified timestamp
*/
public function at(int $input): 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.
*
* @param non-empty-string $otp
* @param null|0|positive-int $input
* @param null|0|positive-int $window
*/
public function verify(string $otp, null|int $input = null, null|int $window = null): bool;
/**
* @return non-empty-string The secret of the OTP
*/
public function getSecret(): string;
/**
* @param non-empty-string $label The label of the OTP
*
* @deprecated Deprecated since v11.4, use {@see self::withLabel()} instead
*/
public function setLabel(string $label): void;
/**
* @param non-empty-string $label The label of the OTP
*/
public function withLabel(string $label): self;
/**
* @return non-empty-string|null The label of the OTP
*/
public function getLabel(): null|string;
/**
* @return non-empty-string|null The issuer
*/
public function getIssuer(): ?string;
/**
* @param non-empty-string $issuer
*
* @deprecated Deprecated since v11.4, use {@see self::withIssuer()} instead
*/
public function setIssuer(string $issuer): void;
/**
* @param non-empty-string $issuer
*/
public function withIssuer(string $issuer): self;
/**
* @return bool If true, the issuer will be added as a parameter in the provisioning URI
*/
public function isIssuerIncludedAsParameter(): bool;
/**
* @deprecated Deprecated since v11.4, use {@see self::withIssuerIncludedAsParameter()} instead
*/
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void;
public function withIssuerIncludedAsParameter(bool $issuer_included_as_parameter): self;
/**
* @return positive-int Number of digits in the OTP
*/
public function getDigits(): int;
/**
* @return non-empty-string Digest algorithm used to calculate the OTP. Spec-compliant, interoperable values are 'sha1', 'sha256' and 'sha512'. Any digest shorter than 19 bytes (e.g. 'md5') is rejected as it cannot satisfy the RFC 4226 dynamic truncation.
*/
public function getDigest(): string;
/**
* @param non-empty-string $parameter
*/
public function getParameter(string $parameter): mixed;
/**
* @param non-empty-string $parameter
*/
public function hasParameter(string $parameter): bool;
/**
* @return array<non-empty-string, mixed>
*/
public function getParameters(): array;
/**
* @param non-empty-string $parameter
*
* @deprecated Deprecated since v11.4, use {@see self::withParameter()} instead
*/
public function setParameter(string $parameter, mixed $value): void;
/**
* @param non-empty-string $parameter
*/
public function withParameter(string $parameter, mixed $value): self;
/**
* Get the provisioning URI.
*
* @return non-empty-string
*/
public function getProvisioningUri(): string;
/**
* Get the provisioning URI.
*
* @param non-empty-string $uri The Uri of the QRCode generator with all parameters. This Uri MUST contain a placeholder that will be replaced by the method.
* @param non-empty-string $placeholder the placeholder to be replaced in the QR Code generator URI
*/
public function getQrCodeUri(string $uri, string $placeholder): string;
}
+262
View File
@@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use function is_int;
use OTPHP\Exception\InvalidParameterException;
use Psr\Clock\ClockInterface;
/**
* @readonly
*
* @see \OTPHP\Test\TOTPTest
*/
final class TOTP extends OTP implements TOTPInterface
{
private readonly ClockInterface $clock;
public function __construct(string $secret, ?ClockInterface $clock = null)
{
parent::__construct($secret);
if ($clock === null) {
trigger_deprecation(
'spomky-labs/otphp',
'11.3.0',
'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".'
);
$clock = new InternalClock();
}
$this->clock = $clock;
}
public static function create(
null|string $secret = null,
int $period = self::DEFAULT_PERIOD,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS,
int $epoch = self::DEFAULT_EPOCH,
?ClockInterface $clock = null,
?int $secretSize = null
): self {
$totp = $secret !== null
? self::createFromSecret($secret, $clock)
: self::generate($clock, $secretSize)
;
$totp->setPeriod($period);
$totp->setDigest($digest);
$totp->setDigits($digits);
$totp->setEpoch($epoch);
return $totp;
}
public static function createFromSecret(string $secret, ?ClockInterface $clock = null): self
{
$totp = new self($secret, $clock);
$totp->setPeriod(self::DEFAULT_PERIOD);
$totp->setDigest(self::DEFAULT_DIGEST);
$totp->setDigits(self::DEFAULT_DIGITS);
$totp->setEpoch(self::DEFAULT_EPOCH);
return $totp;
}
/**
* @param positive-int|null $secretSize
*/
public static function generate(?ClockInterface $clock = null, ?int $secretSize = null): self
{
return self::createFromSecret(self::generateSecret($secretSize), $clock);
}
public function getPeriod(): int
{
$value = $this->getParameter('period');
(is_int($value) && $value > 0) || throw new InvalidParameterException(
'Invalid "period" parameter.',
'period',
$value
);
return $value;
}
public function getEpoch(): int
{
$value = $this->getParameter('epoch');
(is_int($value) && $value >= 0) || throw new InvalidParameterException(
'Invalid "epoch" parameter.',
'epoch',
$value
);
return $value;
}
public function expiresIn(): int
{
$period = $this->getPeriod();
return $period - (($this->clock->now()->getTimestamp() - $this->getEpoch()) % $period);
}
/**
* The OTP at the specified input.
*
* @param 0|positive-int $input
*/
public function at(int $input): string
{
return $this->generateOTP($this->timecode($input));
}
public function now(): string
{
$timestamp = $this->clock->now()
->getTimestamp();
$timestamp >= 0 || throw new InvalidParameterException(
'The timestamp must return a positive integer.',
'timestamp',
$timestamp
);
return $this->at($timestamp);
}
/**
* If no timestamp is provided, the OTP is verified at the actual timestamp. When used, the leeway parameter will
* allow time drift. The passed value is in seconds.
*
* @param 0|positive-int $timestamp
* @param null|0|positive-int $leeway
*/
public function verify(string $otp, null|int $timestamp = null, null|int $leeway = null): bool
{
$timestamp ??= $this->clock->now()
->getTimestamp();
$timestamp >= 0 || throw new InvalidParameterException(
'Timestamp must be at least 0.',
'timestamp',
$timestamp
);
if ($leeway === null) {
return $this->compareOTP($this->at($timestamp), $otp);
}
$leeway = abs($leeway);
$leeway < $this->getPeriod() || throw new InvalidParameterException(
'The leeway must be lower than the TOTP period',
'leeway',
$leeway
);
$timestampMinusLeeway = $timestamp - $leeway;
$timestampMinusLeeway >= 0 || throw new InvalidParameterException(
'The timestamp must be greater than or equal to the leeway.',
'timestamp',
$timestamp
);
return $this->compareOTP($this->at($timestampMinusLeeway), $otp)
|| $this->compareOTP($this->at($timestamp), $otp)
|| $this->compareOTP($this->at($timestamp + $leeway), $otp);
}
public function getProvisioningUri(): string
{
$params = [];
if ($this->getPeriod() !== 30) {
$params['period'] = $this->getPeriod();
}
if ($this->getEpoch() !== 0) {
$params['epoch'] = $this->getEpoch();
}
return $this->generateURI('totp', $params);
}
public function setPeriod(int $period): void
{
$this->setParameter('period', $period);
}
public function withPeriod(int $period): self
{
$otp = clone $this;
$otp->setParameter('period', $period);
return $otp;
}
public function setEpoch(int $epoch): void
{
$this->setParameter('epoch', $epoch);
}
public function withEpoch(int $epoch): self
{
$otp = clone $this;
$otp->setParameter('epoch', $epoch);
return $otp;
}
/**
* @return array<non-empty-string, callable>
*/
protected function getParameterMap(): array
{
return [
...parent::getParameterMap(),
'period' => static function ($value): int {
(int) $value > 0 || throw new InvalidParameterException('Period must be at least 1.', 'period', $value);
return (int) $value;
},
'epoch' => static function ($value): int {
(int) $value >= 0 || throw new InvalidParameterException(
'Epoch must be greater than or equal to 0.',
'epoch',
$value
);
return (int) $value;
},
];
}
/**
* @param array<non-empty-string, mixed> $options
*/
protected function filterOptions(array &$options): void
{
parent::filterOptions($options);
if (isset($options['epoch']) && $options['epoch'] === 0) {
unset($options['epoch']);
}
ksort($options);
}
/**
* @param 0|positive-int $timestamp
*
* @return 0|positive-int
*/
private function timecode(int $timestamp): int
{
$timecode = (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
$timecode >= 0 || throw new InvalidParameterException(
'Timecode must be at least 0. The timestamp must be greater than or equal to the epoch.',
'timecode',
$timecode
);
return $timecode;
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace OTPHP;
interface TOTPInterface extends OTPInterface
{
public const DEFAULT_PERIOD = 30;
public const DEFAULT_EPOCH = 0;
/**
* Create a new TOTP object.
*
* If the secret is null, a random 64 bytes secret will be generated.
*
* @param null|non-empty-string $secret
* @param positive-int $period
* @param non-empty-string $digest
* @param positive-int $digits
*
* @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead
*/
public static function create(
null|string $secret = null,
int $period = self::DEFAULT_PERIOD,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS
): self;
/**
* @deprecated Deprecated since v11.4, use {@see self::withPeriod()} instead
*/
public function setPeriod(int $period): void;
public function withPeriod(int $period): self;
/**
* @deprecated Deprecated since v11.4, use {@see self::withEpoch()} instead
*/
public function setEpoch(int $epoch): void;
public function withEpoch(int $epoch): self;
/**
* Return the TOTP at the current time.
*
* @return non-empty-string
*/
public function now(): string;
/**
* Get the period of time for OTP generation (a non-null positive integer, in second).
*/
public function getPeriod(): int;
public function expiresIn(): int;
public function getEpoch(): int;
}
+105
View File
@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use function array_key_exists;
use function is_string;
use OTPHP\Exception\InvalidProvisioningUriException;
/**
* @readonly
*
* @internal
*/
final class Url
{
/**
* @param non-empty-string $scheme
* @param non-empty-string $host
* @param non-empty-string $path
* @param non-empty-string $secret
* @param array<non-empty-string, mixed> $query
*/
public function __construct(
private readonly string $scheme,
private readonly string $host,
private readonly string $path,
private readonly string $secret,
private readonly array $query
) {
}
/**
* @return non-empty-string
*/
public function getScheme(): string
{
return $this->scheme;
}
/**
* @return non-empty-string
*/
public function getHost(): string
{
return $this->host;
}
/**
* @return non-empty-string
*/
public function getPath(): string
{
return $this->path;
}
/**
* @return non-empty-string
*/
public function getSecret(): string
{
return $this->secret;
}
/**
* @return array<non-empty-string, mixed>
*/
public function getQuery(): array
{
return $this->query;
}
/**
* @param non-empty-string $uri
*/
public static function fromString(string $uri): self
{
$parsed_url = parse_url($uri);
$parsed_url !== false || throw new InvalidProvisioningUriException('Invalid URI.');
foreach (['scheme', 'host', 'path', 'query'] as $key) {
array_key_exists($key, $parsed_url) || throw new InvalidProvisioningUriException(
'Not a valid OTP provisioning URI'
);
}
$scheme = $parsed_url['scheme'] ?? null;
$host = $parsed_url['host'] ?? null;
$path = $parsed_url['path'] ?? null;
$query = $parsed_url['query'] ?? null;
$scheme === 'otpauth' || throw new InvalidProvisioningUriException('Not a valid OTP provisioning URI');
(is_string($host) && $host !== '') || throw new InvalidProvisioningUriException('Invalid URI.');
(is_string($path) && $path !== '') || throw new InvalidProvisioningUriException('Invalid URI.');
is_string($query) || throw new InvalidProvisioningUriException('Invalid URI.');
$parsedQuery = [];
parse_str($query, $parsedQuery);
array_key_exists('secret', $parsedQuery) || throw new InvalidProvisioningUriException(
'Not a valid OTP provisioning URI'
);
$secret = $parsedQuery['secret'];
(is_string($secret) && $secret !== '') || throw new InvalidProvisioningUriException('Invalid URI.');
unset($parsedQuery['secret']);
return new self($scheme, $host, $path, $secret, $parsedQuery);
}
}
+5
View File
@@ -0,0 +1,5 @@
CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2020-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+26
View File
@@ -0,0 +1,26 @@
Symfony Deprecation Contracts
=============================
A generic function and convention to trigger deprecation notices.
This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
The function requires at least 3 arguments:
- the name of the Composer package that is triggering the deprecation
- the version of the package that introduced the deprecation
- the message of the deprecation
- more arguments can be provided: they will be inserted in the message using `printf()` formatting
Example:
```php
trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
```
This will generate the following message:
`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
While not recommended, the deprecation notices can be completely ignored by declaring an empty
`function trigger_deprecation() {}` in your application.
+35
View File
@@ -0,0 +1,35 @@
{
"name": "symfony/deprecation-contracts",
"type": "library",
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.1"
},
"autoload": {
"files": [
"function.php"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "3.7-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (!function_exists('trigger_deprecation')) {
/**
* Triggers a silenced deprecation notice.
*
* @param string $package The name of the Composer package that is triggering the deprecation
* @param string $version The version of the package that introduced the deprecation
* @param string $message The message of the deprecation
* @param mixed ...$args Values to insert in the message using printf() formatting
*
* @author Nicolas Grekas <p@tchwork.com>
*/
function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void
{
@trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
}
}