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