From 3f3bf06406244a94aeffd5818ba05b41a1754ae5 Mon Sep 17 00:00:00 2001
From: Paragon Initiative Enterprises <security@paragonie.com>
Date: Fri, 10 Jun 2022 03:38:28 -0400
Subject: [PATCH] Add Base64::decodeNoPadding() and Base32::decodeNoPadding()

This is a strict decoding method that doesn't tolerate '=' padding.
---
 src/Base32.php                      | 47 +++++++++++++++++++++++++++--
 src/Base32Hex.php                   |  2 +-
 src/Base64.php                      | 41 ++++++++++++++++++++++---
 src/Base64DotSlash.php              |  2 +-
 src/Base64DotSlashOrdered.php       |  2 +-
 src/Base64UrlSafe.php               |  2 +-
 src/Binary.php                      |  2 +-
 src/EncoderInterface.php            |  2 +-
 src/Encoding.php                    |  2 +-
 src/Hex.php                         |  2 +-
 src/RFC4648.php                     |  2 +-
 tests/Base32HexTest.php             |  8 +++++
 tests/Base32Test.php                | 28 +++++++++++++++++
 tests/Base64DotSlashOrderedTest.php |  9 ++++++
 tests/Base64DotSlashTest.php        |  9 ++++++
 tests/Base64Test.php                |  8 +++++
 tests/Base64UrlSafeTest.php         |  8 +++++
 17 files changed, 161 insertions(+), 15 deletions(-)

diff --git a/src/Base32.php b/src/Base32.php
index b4f2de1..2c3ee61 100644
--- a/src/Base32.php
+++ b/src/Base32.php
@@ -2,8 +2,11 @@
 declare(strict_types=1);
 namespace ParagonIE\ConstantTime;
 
+use InvalidArgumentException;
+use RangeException;
+
 /**
- *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
+ *  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
@@ -182,6 +185,31 @@ abstract class Base32 implements EncoderInterface
         return \pack('C', $src + $diff);
     }
 
+    /**
+     * @param string $encodedString
+     * @return string
+     */
+    public static function decodeNoPadding(string $encodedString, bool $upper = false): string
+    {
+        $srcLen = Binary::safeStrlen($encodedString);
+        if ($srcLen === 0) {
+            return '';
+        }
+        if (($srcLen & 7) === 0) {
+            for ($j = 0; $j < 7; ++$j) {
+                if ($encodedString[$srcLen - 1] === '=') {
+                    throw new InvalidArgumentException(
+                        "decodeNoPadding() doesn't tolerate padding"
+                    );
+                }
+            }
+        }
+        return static::doDecode(
+            $encodedString,
+            $upper,
+            true
+        );
+    }
 
     /**
      * Base32 decoding
@@ -287,6 +315,9 @@ abstract class Base32 implements EncoderInterface
                     (($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]);
@@ -324,6 +355,9 @@ abstract class Base32 implements EncoderInterface
                     (($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]);
@@ -338,6 +372,9 @@ abstract class Base32 implements EncoderInterface
                     (($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]);
@@ -350,6 +387,9 @@ abstract class Base32 implements EncoderInterface
                     (($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]);
@@ -359,6 +399,9 @@ abstract class Base32 implements EncoderInterface
                     (($c0 << 3) | ($c1 >> 2)             ) & 0xff
                 );
                 $err |= ($c0 | $c1) >> 8;
+                if ($strictPadding) {
+                    $err |= ($c1 << 6) & 0xff;
+                }
             } else {
                 $dest .= \pack(
                     'C',
@@ -369,7 +412,7 @@ abstract class Base32 implements EncoderInterface
         }
         $check = ($err === 0);
         if (!$check) {
-            throw new \RangeException(
+            throw new RangeException(
                 'Base32::doDecode() only expects characters in the correct base32 alphabet'
             );
         }
diff --git a/src/Base32Hex.php b/src/Base32Hex.php
index 68fdad5..b868dd0 100644
--- a/src/Base32Hex.php
+++ b/src/Base32Hex.php
@@ -3,7 +3,7 @@ declare(strict_types=1);
 namespace ParagonIE\ConstantTime;
 
 /**
- *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
+ *  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
diff --git a/src/Base64.php b/src/Base64.php
index 6cf44bf..5422aa4 100644
--- a/src/Base64.php
+++ b/src/Base64.php
@@ -2,8 +2,11 @@
 declare(strict_types=1);
 namespace ParagonIE\ConstantTime;
 
+use InvalidArgumentException;
+use RangeException;
+
 /**
- *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
+ *  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
@@ -141,12 +144,12 @@ abstract class Base64 implements EncoderInterface
                 }
             }
             if (($srcLen & 3) === 1) {
-                throw new \RangeException(
+                throw new RangeException(
                     'Incorrect padding'
                 );
             }
             if ($encodedString[$srcLen - 1] === '=') {
-                throw new \RangeException(
+                throw new RangeException(
                     'Incorrect padding'
                 );
             }
@@ -208,13 +211,43 @@ abstract class Base64 implements EncoderInterface
         }
         $check = ($err === 0);
         if (!$check) {
-            throw new \RangeException(
+            throw new RangeException(
                 'Base64::decode() only expects characters in the correct base64 alphabet'
             );
         }
         return $dest;
     }
 
+    /**
+     * @param string $encodedString
+     * @return string
+     */
+    public static function decodeNoPadding(string $encodedString): string
+    {
+        $srcLen = Binary::safeStrlen($encodedString);
+        if ($srcLen === 0) {
+            return '';
+        }
+        if (($srcLen & 3) === 0) {
+            if ($encodedString[$srcLen - 1] === '=') {
+                throw new InvalidArgumentException(
+                    "decodeNoPadding() doesn't tolerate padding"
+                );
+            }
+            if (($srcLen & 3) > 1) {
+                if ($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.
diff --git a/src/Base64DotSlash.php b/src/Base64DotSlash.php
index 8ad2e2b..5e98a8f 100644
--- a/src/Base64DotSlash.php
+++ b/src/Base64DotSlash.php
@@ -3,7 +3,7 @@ declare(strict_types=1);
 namespace ParagonIE\ConstantTime;
 
 /**
- *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
+ *  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
diff --git a/src/Base64DotSlashOrdered.php b/src/Base64DotSlashOrdered.php
index dd1459e..9780b14 100644
--- a/src/Base64DotSlashOrdered.php
+++ b/src/Base64DotSlashOrdered.php
@@ -3,7 +3,7 @@ declare(strict_types=1);
 namespace ParagonIE\ConstantTime;
 
 /**
- *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
+ *  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
diff --git a/src/Base64UrlSafe.php b/src/Base64UrlSafe.php
index 1a41075..8192c63 100644
--- a/src/Base64UrlSafe.php
+++ b/src/Base64UrlSafe.php
@@ -3,7 +3,7 @@ declare(strict_types=1);
 namespace ParagonIE\ConstantTime;
 
 /**
- *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
+ *  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
diff --git a/src/Binary.php b/src/Binary.php
index add0522..4a36572 100644
--- a/src/Binary.php
+++ b/src/Binary.php
@@ -3,7 +3,7 @@ declare(strict_types=1);
 namespace ParagonIE\ConstantTime;
 
 /**
- *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
+ *  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
diff --git a/src/EncoderInterface.php b/src/EncoderInterface.php
index 7aeee55..9cafbf9 100644
--- a/src/EncoderInterface.php
+++ b/src/EncoderInterface.php
@@ -3,7 +3,7 @@ declare(strict_types=1);
 namespace ParagonIE\ConstantTime;
 
 /**
- *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
+ *  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
diff --git a/src/Encoding.php b/src/Encoding.php
index 896a668..1336935 100644
--- a/src/Encoding.php
+++ b/src/Encoding.php
@@ -3,7 +3,7 @@ declare(strict_types=1);
 namespace ParagonIE\ConstantTime;
 
 /**
- *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
+ *  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
diff --git a/src/Hex.php b/src/Hex.php
index 4c27328..a242b94 100644
--- a/src/Hex.php
+++ b/src/Hex.php
@@ -3,7 +3,7 @@ declare(strict_types=1);
 namespace ParagonIE\ConstantTime;
 
 /**
- *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
+ *  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
diff --git a/src/RFC4648.php b/src/RFC4648.php
index 492cad0..5ceda31 100644
--- a/src/RFC4648.php
+++ b/src/RFC4648.php
@@ -3,7 +3,7 @@ declare(strict_types=1);
 namespace ParagonIE\ConstantTime;
 
 /**
- *  Copyright (c) 2016 - 2018 Paragon Initiative Enterprises.
+ *  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
diff --git a/tests/Base32HexTest.php b/tests/Base32HexTest.php
index be83b7a..009b5b9 100644
--- a/tests/Base32HexTest.php
+++ b/tests/Base32HexTest.php
@@ -2,6 +2,7 @@
 declare(strict_types=1);
 namespace ParagonIE\ConstantTime\Tests;
 
+use InvalidArgumentException;
 use ParagonIE\ConstantTime\Base32Hex;
 use PHPUnit\Framework\TestCase;
 
@@ -50,4 +51,11 @@ class Base32HexTest extends TestCase
             }
         }
     }
+
+    public function testDecodeNoPadding()
+    {
+        Base32Hex::decodeNoPadding('aaaqe');
+        $this->expectException(InvalidArgumentException::class);
+        Base32Hex::decodeNoPadding('aaaqe===');
+    }
 }
diff --git a/tests/Base32Test.php b/tests/Base32Test.php
index 6eb07de..a3f053b 100644
--- a/tests/Base32Test.php
+++ b/tests/Base32Test.php
@@ -2,6 +2,7 @@
 declare(strict_types=1);
 namespace ParagonIE\ConstantTime\Tests;
 
+use InvalidArgumentException;
 use PHPUnit\Framework\TestCase;
 use ParagonIE\ConstantTime\Base32;
 
@@ -51,4 +52,31 @@ class Base32Test extends TestCase
             }
         }
     }
+
+    public function canonProvider()
+    {
+        return [
+            ['me', 'mf'],
+            ['mfra', 'mfrb'],
+            ['mfrgg', 'mfrgh'],
+            ['mfrggza', 'mfrggzb']
+        ];
+    }
+
+    /**
+     * @dataProvider canonProvider
+     */
+    public function testCanonicalBase32(string $canonical, string $munged)
+    {
+        Base32::decode($canonical);
+        $this->expectException(\RangeException::class);
+        Base32::decodeNoPadding($munged);
+    }
+
+    public function testDecodeNoPadding()
+    {
+        Base32::decodeNoPadding('aaaqe');
+        $this->expectException(InvalidArgumentException::class);
+        Base32::decodeNoPadding('aaaqe===');
+    }
 }
diff --git a/tests/Base64DotSlashOrderedTest.php b/tests/Base64DotSlashOrderedTest.php
index b0d47e8..614dabe 100644
--- a/tests/Base64DotSlashOrderedTest.php
+++ b/tests/Base64DotSlashOrderedTest.php
@@ -2,6 +2,7 @@
 declare(strict_types=1);
 namespace ParagonIE\ConstantTime\Tests;
 
+use InvalidArgumentException;
 use PHPUnit\Framework\TestCase;
 use ParagonIE\ConstantTime\Base64DotSlashOrdered;
 use RangeException;
@@ -38,6 +39,14 @@ class Base64DotSlashOrderedTest extends TestCase
             }
         }
     }
+
+    public function testDecodeNoPadding()
+    {
+        Base64DotSlashOrdered::decodeNoPadding('..');
+        $this->expectException(InvalidArgumentException::class);
+        Base64DotSlashOrdered::decodeNoPadding('..==');
+    }
+
     /**
      * @dataProvider canonicalDataProvider
      */
diff --git a/tests/Base64DotSlashTest.php b/tests/Base64DotSlashTest.php
index ca4de28..2e61f23 100644
--- a/tests/Base64DotSlashTest.php
+++ b/tests/Base64DotSlashTest.php
@@ -2,6 +2,7 @@
 declare(strict_types=1);
 namespace ParagonIE\ConstantTime\Tests;
 
+use InvalidArgumentException;
 use ParagonIE\ConstantTime\Base64DotSlash;
 use PHPUnit\Framework\TestCase;
 use RangeException;
@@ -39,6 +40,13 @@ class Base64DotSlashTest extends TestCase
         }
     }
 
+    public function testDecodeNoPadding()
+    {
+        Base64DotSlash::decodeNoPadding('..');
+        $this->expectException(InvalidArgumentException::class);
+        Base64DotSlash::decodeNoPadding('..==');
+    }
+
     /**
      * @dataProvider canonicalDataProvider
      */
@@ -56,6 +64,7 @@ class Base64DotSlashTest extends TestCase
         $this->expectException(RangeException::class);
         Base64DotSlash::decode($x, true);
     }
+
     protected function getNextChar(string $c): string
     {
         return strtr(
diff --git a/tests/Base64Test.php b/tests/Base64Test.php
index c964686..7d97fae 100644
--- a/tests/Base64Test.php
+++ b/tests/Base64Test.php
@@ -2,6 +2,7 @@
 declare(strict_types=1);
 namespace ParagonIE\ConstantTime\Tests;
 
+use InvalidArgumentException;
 use PHPUnit\Framework\TestCase;
 use ParagonIE\ConstantTime\Base64;
 use RangeException;
@@ -94,6 +95,13 @@ class Base64Test extends TestCase
         Base64::decode('00==', true);
     }
 
+    public function testDecodeNoPadding()
+    {
+        Base64::decodeNoPadding('0w');
+        $this->expectException(InvalidArgumentException::class);
+        Base64::decodeNoPadding('0w==');
+    }
+
     /**
      * @dataProvider canonicalDataProvider
      */
diff --git a/tests/Base64UrlSafeTest.php b/tests/Base64UrlSafeTest.php
index f0d4123..831f814 100644
--- a/tests/Base64UrlSafeTest.php
+++ b/tests/Base64UrlSafeTest.php
@@ -3,6 +3,7 @@ declare(strict_types=1);
 namespace ParagonIE\ConstantTime\Tests;
 
 use Exception;
+use InvalidArgumentException;
 use PHPUnit\Framework\TestCase;
 use ParagonIE\ConstantTime\Base64UrlSafe;
 use ParagonIE\ConstantTime\Binary;
@@ -64,6 +65,13 @@ class Base64UrlSafeTest extends TestCase
         );
     }
 
+    public function testDecodeNoPadding()
+    {
+        Base64UrlSafe::decodeNoPadding('0w');
+        $this->expectException(InvalidArgumentException::class);
+        Base64UrlSafe::decodeNoPadding('0w==');
+    }
+
     /**
      * @dataProvider canonicalDataProvider
      */
-- 
GitLab