From 74e3151132650bb151b51957165495364b9336e9 Mon Sep 17 00:00:00 2001
From: Paragon Initiative Enterprises <security@paragonie.com>
Date: Fri, 10 Jun 2022 02:59:59 -0400
Subject: [PATCH] Overhaul unit tests. Add test for strict padding.

---
 composer.json                       |  5 ++++
 psalm.xml                           |  4 ++-
 tests/Base32HexTest.php             |  8 ++++--
 tests/Base32Test.php                |  8 ++++--
 tests/Base64DotSlashOrderedTest.php | 37 ++++++++++++++++++++++++++--
 tests/Base64DotSlashTest.php        | 37 ++++++++++++++++++++++++++--
 tests/Base64Test.php                | 38 +++++++++++++++++++++++++++--
 tests/Base64UrlSafeTest.php         | 37 +++++++++++++++++++++++++++-
 tests/CanonicalTrait.php            | 30 +++++++++++++++++++++++
 tests/EncodingTest.php              | 19 ++++++++-------
 tests/RFC4648Test.php               | 17 +++++++------
 11 files changed, 211 insertions(+), 29 deletions(-)
 create mode 100644 tests/CanonicalTrait.php

diff --git a/composer.json b/composer.json
index 583fe36..2fe9717 100644
--- a/composer.json
+++ b/composer.json
@@ -47,5 +47,10 @@
     "psr-4": {
       "ParagonIE\\ConstantTime\\": "src/"
     }
+  },
+  "autoload-dev": {
+    "psr-4": {
+      "ParagonIE\\ConstantTime\\Tests\\": "tests/"
+    }
   }
 }
diff --git a/psalm.xml b/psalm.xml
index dade79a..af5ec45 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -1,10 +1,12 @@
 <?xml version="1.0"?>
 <psalm
     useDocblockTypes="true"
-    totallyTyped="true"
 >
     <projectFiles>
         <directory name="src" />
+        <ignoreFiles>
+            <directory name="tests" />
+        </ignoreFiles>
     </projectFiles>
     <issueHandlers>
         <UnnecessaryVarAnnotation errorLevel="info" />
diff --git a/tests/Base32HexTest.php b/tests/Base32HexTest.php
index ed19f33..be83b7a 100644
--- a/tests/Base32HexTest.php
+++ b/tests/Base32HexTest.php
@@ -1,7 +1,11 @@
 <?php
-use \ParagonIE\ConstantTime\Base32Hex;
+declare(strict_types=1);
+namespace ParagonIE\ConstantTime\Tests;
 
-class Base32HexTest extends PHPUnit\Framework\TestCase
+use ParagonIE\ConstantTime\Base32Hex;
+use PHPUnit\Framework\TestCase;
+
+class Base32HexTest extends TestCase
 {
     /**
      * @covers Base32Hex::encode()
diff --git a/tests/Base32Test.php b/tests/Base32Test.php
index 65e8e7c..6eb07de 100644
--- a/tests/Base32Test.php
+++ b/tests/Base32Test.php
@@ -1,7 +1,11 @@
 <?php
-use \ParagonIE\ConstantTime\Base32;
+declare(strict_types=1);
+namespace ParagonIE\ConstantTime\Tests;
 
-class Base32Test extends PHPUnit\Framework\TestCase
+use PHPUnit\Framework\TestCase;
+use ParagonIE\ConstantTime\Base32;
+
+class Base32Test extends TestCase
 {
     /**
      * @covers Base32::encode()
diff --git a/tests/Base64DotSlashOrderedTest.php b/tests/Base64DotSlashOrderedTest.php
index f7dc828..b0d47e8 100644
--- a/tests/Base64DotSlashOrderedTest.php
+++ b/tests/Base64DotSlashOrderedTest.php
@@ -1,8 +1,15 @@
 <?php
-use \ParagonIE\ConstantTime\Base64DotSlashOrdered;
+declare(strict_types=1);
+namespace ParagonIE\ConstantTime\Tests;
 
-class Base64DotSlashOrderedTest extends PHPUnit\Framework\TestCase
+use PHPUnit\Framework\TestCase;
+use ParagonIE\ConstantTime\Base64DotSlashOrdered;
+use RangeException;
+
+class Base64DotSlashOrderedTest extends TestCase
 {
+    use CanonicalTrait;
+
     /**
      * @covers Base64DotSlashOrdered::encode()
      * @covers Base64DotSlashOrdered::decode()
@@ -31,4 +38,30 @@ class Base64DotSlashOrderedTest extends PHPUnit\Framework\TestCase
             }
         }
     }
+    /**
+     * @dataProvider canonicalDataProvider
+     */
+    public function testNonCanonical(string $input)
+    {
+        $w = Base64DotSlashOrdered::encodeUnpadded($input);
+        Base64DotSlashOrdered::decode($w);
+        Base64DotSlashOrdered::decode($w, true);
+
+        // Mess with padding:
+        $x = $this->increment($w);
+        Base64DotSlashOrdered::decode($x);
+
+        // Should throw in strict mode:
+        $this->expectException(RangeException::class);
+        Base64DotSlashOrdered::decode($x, true);
+    }
+
+    protected function getNextChar(string $c): string
+    {
+        return strtr(
+            $c,
+            'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./',
+            'BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./A'
+        );
+    }
 }
diff --git a/tests/Base64DotSlashTest.php b/tests/Base64DotSlashTest.php
index 257a3d5..ca4de28 100644
--- a/tests/Base64DotSlashTest.php
+++ b/tests/Base64DotSlashTest.php
@@ -1,8 +1,15 @@
 <?php
-use \ParagonIE\ConstantTime\Base64DotSlash;
+declare(strict_types=1);
+namespace ParagonIE\ConstantTime\Tests;
 
-class Base64DotSlashTest extends PHPUnit\Framework\TestCase
+use ParagonIE\ConstantTime\Base64DotSlash;
+use PHPUnit\Framework\TestCase;
+use RangeException;
+
+class Base64DotSlashTest extends TestCase
 {
+    use CanonicalTrait;
+
     /**
      * @covers Base64DotSlash::encode()
      * @covers Base64DotSlash::decode()
@@ -31,4 +38,30 @@ class Base64DotSlashTest extends PHPUnit\Framework\TestCase
             }
         }
     }
+
+    /**
+     * @dataProvider canonicalDataProvider
+     */
+    public function testNonCanonical(string $input)
+    {
+        $w = Base64DotSlash::encodeUnpadded($input);
+        Base64DotSlash::decode($w);
+        Base64DotSlash::decode($w, true);
+
+        // Mess with padding:
+        $x = $this->increment($w);
+        Base64DotSlash::decode($x);
+
+        // Should throw in strict mode:
+        $this->expectException(RangeException::class);
+        Base64DotSlash::decode($x, true);
+    }
+    protected function getNextChar(string $c): string
+    {
+        return strtr(
+            $c,
+            'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./',
+            'BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./A'
+        );
+    }
 }
diff --git a/tests/Base64Test.php b/tests/Base64Test.php
index 16ab47d..339f5d2 100644
--- a/tests/Base64Test.php
+++ b/tests/Base64Test.php
@@ -1,8 +1,15 @@
 <?php
-use \ParagonIE\ConstantTime\Base64;
+declare(strict_types=1);
+namespace ParagonIE\ConstantTime\Tests;
 
-class Base64Test extends PHPUnit\Framework\TestCase
+use PHPUnit\Framework\TestCase;
+use ParagonIE\ConstantTime\Base64;
+use RangeException;
+
+class Base64Test extends TestCase
 {
+    use CanonicalTrait;
+
     /**
      * @covers Base64::encode()
      * @covers Base64::decode()
@@ -76,4 +83,31 @@ class Base64Test extends PHPUnit\Framework\TestCase
             );
         }
     }
+
+    /**
+     * @dataProvider canonicalDataProvider
+     */
+    public function testNonCanonical(string $input)
+    {
+        $w = Base64::encodeUnpadded($input);
+        Base64::decode($w);
+        Base64::decode($w, true);
+
+        // Mess with padding:
+        $x = $this->increment($w);
+        Base64::decode($x);
+
+        // Should throw in strict mode:
+        $this->expectException(RangeException::class);
+        Base64::decode($x, true);
+    }
+
+    protected function getNextChar(string $c): string
+    {
+        return strtr(
+            $c,
+            'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
+            'BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/A'
+        );
+    }
 }
diff --git a/tests/Base64UrlSafeTest.php b/tests/Base64UrlSafeTest.php
index 136ed61..f0d4123 100644
--- a/tests/Base64UrlSafeTest.php
+++ b/tests/Base64UrlSafeTest.php
@@ -1,13 +1,21 @@
 <?php
+declare(strict_types=1);
+namespace ParagonIE\ConstantTime\Tests;
 
+use Exception;
+use PHPUnit\Framework\TestCase;
 use ParagonIE\ConstantTime\Base64UrlSafe;
 use ParagonIE\ConstantTime\Binary;
+use RangeException;
+use TypeError;
 
 /**
  * Class Base64UrlSafeTest
  */
-class Base64UrlSafeTest extends PHPUnit\Framework\TestCase
+class Base64UrlSafeTest extends TestCase
 {
+    use CanonicalTrait;
+
     /**
      * @covers Base64UrlSafe::encode()
      * @covers Base64UrlSafe::decode()
@@ -55,4 +63,31 @@ class Base64UrlSafeTest extends PHPUnit\Framework\TestCase
             $enc
         );
     }
+
+    /**
+     * @dataProvider canonicalDataProvider
+     */
+    public function testNonCanonical(string $input)
+    {
+        $w = Base64UrlSafe::encodeUnpadded($input);
+        Base64UrlSafe::decode($w);
+        Base64UrlSafe::decode($w, true);
+
+        // Mess with padding:
+        $x = $this->increment($w);
+        Base64UrlSafe::decode($x);
+
+        // Should throw in strict mode:
+        $this->expectException(RangeException::class);
+        Base64UrlSafe::decode($x, true);
+    }
+
+    protected function getNextChar(string $c): string
+    {
+        return strtr(
+            $c,
+            'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
+            'BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_A'
+        );
+    }
 }
diff --git a/tests/CanonicalTrait.php b/tests/CanonicalTrait.php
new file mode 100644
index 0000000..971cb66
--- /dev/null
+++ b/tests/CanonicalTrait.php
@@ -0,0 +1,30 @@
+<?php
+declare(strict_types=1);
+namespace ParagonIE\ConstantTime\Tests;
+
+use ParagonIE\ConstantTime\Binary;
+
+/**
+ * @method getNextChar(string $c): string
+ */
+trait CanonicalTrait
+{
+    public function canonicalDataProvider(): array
+    {
+        return [
+            ['a'],
+            ['ab'],
+            ['abcd'],
+            ["\xff"],
+            ["\xff\xff"],
+            ["\xff\xff\xff\xff"]
+        ];
+    }
+
+    protected function increment(string $str): string
+    {
+        $i = Binary::safeStrlen($str) - 1;
+        $c = $this->getNextChar($str[$i]);
+        return Binary::safeSubstr($str, 0, $i) . $c;
+    }
+}
diff --git a/tests/EncodingTest.php b/tests/EncodingTest.php
index 6f774d8..0c2986e 100644
--- a/tests/EncodingTest.php
+++ b/tests/EncodingTest.php
@@ -1,14 +1,15 @@
 <?php
-use \ParagonIE\ConstantTime\Base32;
-use \ParagonIE\ConstantTime\Base32Hex;
-use \ParagonIE\ConstantTime\Base64;
-use \ParagonIE\ConstantTime\Base64DotSlash;
-use \ParagonIE\ConstantTime\Base64DotSlashOrdered;
-use \ParagonIE\ConstantTime\Base64UrlSafe;
-use \ParagonIE\ConstantTime\Encoding;
-use \ParagonIE\ConstantTime\Hex;
+declare(strict_types=1);
+namespace ParagonIE\ConstantTime\Tests;
+use PHPUnit\Framework\TestCase;
 
-class EncodingTest extends PHPUnit\Framework\TestCase
+use ParagonIE\ConstantTime\Base32;
+use ParagonIE\ConstantTime\Base32Hex;
+use ParagonIE\ConstantTime\Base64UrlSafe;
+use ParagonIE\ConstantTime\Encoding;
+use ParagonIE\ConstantTime\Hex;
+
+class EncodingTest extends TestCase
 {
     public function testBase32Encode()
     {
diff --git a/tests/RFC4648Test.php b/tests/RFC4648Test.php
index a6653de..c19c515 100644
--- a/tests/RFC4648Test.php
+++ b/tests/RFC4648Test.php
@@ -1,18 +1,19 @@
 <?php
-use \ParagonIE\ConstantTime\Base32;
-use \ParagonIE\ConstantTime\Base32Hex;
-use \ParagonIE\ConstantTime\Base64;
-use \ParagonIE\ConstantTime\Base64DotSlash;
-use \ParagonIE\ConstantTime\Base64DotSlashOrdered;
-use \ParagonIE\ConstantTime\Encoding;
-use \ParagonIE\ConstantTime\Hex;
+declare(strict_types=1);
+namespace ParagonIE\ConstantTime\Tests;
+
+use ParagonIE\ConstantTime\Base32;
+use ParagonIE\ConstantTime\Base32Hex;
+use ParagonIE\ConstantTime\Base64;
+use ParagonIE\ConstantTime\Hex;
+use PHPUnit\Framework\TestCase;
 
 /**
  * Class RFC4648Test
  *
  * @ref https://tools.ietf.org/html/rfc4648#section-10
  */
-class RFC4648Test extends PHPUnit\Framework\TestCase
+class RFC4648Test extends TestCase
 {
     public function testVectorBase64()
     {
-- 
GitLab