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