xref: /dokuwiki/vendor/paragonie/constant_time_encoding/src/Hex.php (revision 8e88a29b81301f78509349ab1152bb09c229123e)
1<?php
2declare(strict_types=1);
3namespace ParagonIE\ConstantTime;
4
5use Override;
6use RangeException;
7use SensitiveParameter;
8use SodiumException;
9use TypeError;
10use function extension_loaded;
11use function pack;
12use function sodium_bin2hex;
13use function sodium_hex2bin;
14use function strlen;
15use function unpack;
16
17/**
18 *  Copyright (c) 2016 - 2025 Paragon Initiative Enterprises.
19 *  Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
20 *
21 *  Permission is hereby granted, free of charge, to any person obtaining a copy
22 *  of this software and associated documentation files (the "Software"), to deal
23 *  in the Software without restriction, including without limitation the rights
24 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25 *  copies of the Software, and to permit persons to whom the Software is
26 *  furnished to do so, subject to the following conditions:
27 *
28 *  The above copyright notice and this permission notice shall be included in all
29 *  copies or substantial portions of the Software.
30 *
31 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37 *  SOFTWARE.
38 */
39
40/**
41 * Class Hex
42 * @package ParagonIE\ConstantTime
43 */
44abstract class Hex implements EncoderInterface
45{
46    /**
47     * Convert a binary string into a hexadecimal string without cache-timing
48     * leaks
49     *
50     * @param string $binString (raw binary)
51     * @return string
52     * @throws TypeError
53     */
54    #[Override]
55    public static function encode(
56        #[SensitiveParameter]
57        string $binString
58    ): string {
59        if (extension_loaded('sodium')) {
60            try {
61                return sodium_bin2hex($binString);
62            } catch (SodiumException $ex) {
63                throw new RangeException($ex->getMessage(), $ex->getCode(), $ex);
64            }
65        }
66        $hex = '';
67        $len = strlen($binString);
68        for ($i = 0; $i < $len; ++$i) {
69            /** @var array<int, int> $chunk */
70            $chunk = unpack('C', $binString[$i]);
71            $c = $chunk[1] & 0xf;
72            $b = $chunk[1] >> 4;
73
74            $hex .= pack(
75                'CC',
76                (87 + $b + ((($b - 10) >> 8) & ~38)),
77                (87 + $c + ((($c - 10) >> 8) & ~38))
78            );
79        }
80        return $hex;
81    }
82
83    /**
84     * Convert a binary string into a hexadecimal string without cache-timing
85     * leaks, returning uppercase letters (as per RFC 4648)
86     *
87     * @param string $binString (raw binary)
88     * @return string
89     * @throws TypeError
90     */
91    public static function encodeUpper(
92        #[SensitiveParameter]
93        string $binString
94    ): string {
95        $hex = '';
96        $len = strlen($binString);
97
98        for ($i = 0; $i < $len; ++$i) {
99            /** @var array<int, int> $chunk */
100            $chunk = unpack('C', $binString[$i]);
101            $c = $chunk[1] & 0xf;
102            $b = $chunk[1] >> 4;
103
104            $hex .= pack(
105                'CC',
106                (55 + $b + ((($b - 10) >> 8) & ~6)),
107                (55 + $c + ((($c - 10) >> 8) & ~6))
108            );
109        }
110        return $hex;
111    }
112
113    /**
114     * Convert a hexadecimal string into a binary string without cache-timing
115     * leaks
116     *
117     * @param string $encodedString
118     * @param bool $strictPadding
119     * @return string (raw binary)
120     * @throws RangeException
121     */
122    #[Override]
123    public static function decode(
124        #[SensitiveParameter]
125        string $encodedString,
126        bool $strictPadding = false
127    ): string {
128        if (extension_loaded('sodium') && $strictPadding) {
129            try {
130                return sodium_hex2bin($encodedString);
131            } catch (SodiumException $ex) {
132                throw new RangeException($ex->getMessage(), $ex->getCode(), $ex);
133            }
134        }
135        $hex_pos = 0;
136        $bin = '';
137        $c_acc = 0;
138        $hex_len = strlen($encodedString);
139        $state = 0;
140        if (($hex_len & 1) !== 0) {
141            if ($strictPadding) {
142                throw new RangeException(
143                    'Expected an even number of hexadecimal characters'
144                );
145            } else {
146                $encodedString = '0' . $encodedString;
147                ++$hex_len;
148            }
149        }
150
151        /** @var array<int, int> $chunk */
152        $chunk = unpack('C*', $encodedString);
153        while ($hex_pos < $hex_len) {
154            ++$hex_pos;
155            $c = $chunk[$hex_pos];
156            $c_num = $c ^ 48;
157            $c_num0 = ($c_num - 10) >> 8;
158            $c_alpha = ($c & ~32) - 55;
159            $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8;
160
161            if (($c_num0 | $c_alpha0) === 0) {
162                throw new RangeException(
163                    'Expected hexadecimal character'
164                );
165            }
166            $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0);
167            if ($state === 0) {
168                $c_acc = $c_val * 16;
169            } else {
170                $bin .= pack('C', $c_acc | $c_val);
171            }
172            $state ^= 1;
173        }
174        return $bin;
175    }
176}
177