blob: 861ed602e0c64f9d31faf17defbdd7c4c7042773 [file] [log] [blame]
Copybara botbe50d492023-11-30 00:16:42 +01001<?php
2
3
4namespace lbuchs\WebAuthn\Binary;
5use lbuchs\WebAuthn\WebAuthnException;
6
7/**
8 * Modified version of https://github.com/madwizard-thomas/webauthn-server/blob/master/src/Format/ByteBuffer.php
9 * Copyright © 2018 Thomas Bleeker - MIT licensed
10 * Modified by Lukas Buchs
11 * Thanks Thomas for your work!
12 */
13class ByteBuffer implements \JsonSerializable, \Serializable {
14 /**
15 * @var bool
16 */
17 public static $useBase64UrlEncoding = false;
18
19 /**
20 * @var string
21 */
22 private $_data;
23
24 /**
25 * @var int
26 */
27 private $_length;
28
29 public function __construct($binaryData) {
30 $this->_data = (string)$binaryData;
31 $this->_length = \strlen($binaryData);
32 }
33
34
35 // -----------------------
36 // PUBLIC STATIC
37 // -----------------------
38
39 /**
40 * create a ByteBuffer from a base64 url encoded string
41 * @param string $base64url
42 * @return ByteBuffer
43 */
44 public static function fromBase64Url($base64url): ByteBuffer {
45 $bin = self::_base64url_decode($base64url);
46 if ($bin === false) {
47 throw new WebAuthnException('ByteBuffer: Invalid base64 url string', WebAuthnException::BYTEBUFFER);
48 }
49 return new ByteBuffer($bin);
50 }
51
52 /**
53 * create a ByteBuffer from a base64 url encoded string
54 * @param string $hex
55 * @return ByteBuffer
56 */
57 public static function fromHex($hex): ByteBuffer {
58 $bin = \hex2bin($hex);
59 if ($bin === false) {
60 throw new WebAuthnException('ByteBuffer: Invalid hex string', WebAuthnException::BYTEBUFFER);
61 }
62 return new ByteBuffer($bin);
63 }
64
65 /**
66 * create a random ByteBuffer
67 * @param string $length
68 * @return ByteBuffer
69 */
70 public static function randomBuffer($length): ByteBuffer {
71 if (\function_exists('random_bytes')) { // >PHP 7.0
72 return new ByteBuffer(\random_bytes($length));
73
74 } else if (\function_exists('openssl_random_pseudo_bytes')) {
75 return new ByteBuffer(\openssl_random_pseudo_bytes($length));
76
77 } else {
78 throw new WebAuthnException('ByteBuffer: cannot generate random bytes', WebAuthnException::BYTEBUFFER);
79 }
80 }
81
82 // -----------------------
83 // PUBLIC
84 // -----------------------
85
86 public function getBytes($offset, $length): string {
87 if ($offset < 0 || $length < 0 || ($offset + $length > $this->_length)) {
88 throw new WebAuthnException('ByteBuffer: Invalid offset or length', WebAuthnException::BYTEBUFFER);
89 }
90 return \substr($this->_data, $offset, $length);
91 }
92
93 public function getByteVal($offset): int {
94 if ($offset < 0 || $offset >= $this->_length) {
95 throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
96 }
97 return \ord(\substr($this->_data, $offset, 1));
98 }
99
100 public function getJson($jsonFlags=0) {
101 $data = \json_decode($this->getBinaryString(), null, 512, $jsonFlags);
102 if (\json_last_error() !== JSON_ERROR_NONE) {
103 throw new WebAuthnException(\json_last_error_msg(), WebAuthnException::BYTEBUFFER);
104 }
105 return $data;
106 }
107
108 public function getLength(): int {
109 return $this->_length;
110 }
111
112 public function getUint16Val($offset) {
113 if ($offset < 0 || ($offset + 2) > $this->_length) {
114 throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
115 }
116 return unpack('n', $this->_data, $offset)[1];
117 }
118
119 public function getUint32Val($offset) {
120 if ($offset < 0 || ($offset + 4) > $this->_length) {
121 throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
122 }
123 $val = unpack('N', $this->_data, $offset)[1];
124
125 // Signed integer overflow causes signed negative numbers
126 if ($val < 0) {
127 throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER);
128 }
129 return $val;
130 }
131
132 public function getUint64Val($offset) {
133 if (PHP_INT_SIZE < 8) {
134 throw new WebAuthnException('ByteBuffer: 64-bit values not supported by this system', WebAuthnException::BYTEBUFFER);
135 }
136 if ($offset < 0 || ($offset + 8) > $this->_length) {
137 throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
138 }
139 $val = unpack('J', $this->_data, $offset)[1];
140
141 // Signed integer overflow causes signed negative numbers
142 if ($val < 0) {
143 throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER);
144 }
145
146 return $val;
147 }
148
149 public function getHalfFloatVal($offset) {
150 //FROM spec pseudo decode_half(unsigned char *halfp)
151 $half = $this->getUint16Val($offset);
152
153 $exp = ($half >> 10) & 0x1f;
154 $mant = $half & 0x3ff;
155
156 if ($exp === 0) {
157 $val = $mant * (2 ** -24);
158 } elseif ($exp !== 31) {
159 $val = ($mant + 1024) * (2 ** ($exp - 25));
160 } else {
161 $val = ($mant === 0) ? INF : NAN;
162 }
163
164 return ($half & 0x8000) ? -$val : $val;
165 }
166
167 public function getFloatVal($offset) {
168 if ($offset < 0 || ($offset + 4) > $this->_length) {
169 throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
170 }
171 return unpack('G', $this->_data, $offset)[1];
172 }
173
174 public function getDoubleVal($offset) {
175 if ($offset < 0 || ($offset + 8) > $this->_length) {
176 throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
177 }
178 return unpack('E', $this->_data, $offset)[1];
179 }
180
181 /**
182 * @return string
183 */
184 public function getBinaryString(): string {
185 return $this->_data;
186 }
187
188 /**
189 * @param string|ByteBuffer $buffer
190 * @return bool
191 */
192 public function equals($buffer): bool {
193 if (is_object($buffer) && $buffer instanceof ByteBuffer) {
194 return $buffer->getBinaryString() === $this->getBinaryString();
195
196 } else if (is_string($buffer)) {
197 return $buffer === $this->getBinaryString();
198 }
199
200 return false;
201 }
202
203 /**
204 * @return string
205 */
206 public function getHex(): string {
207 return \bin2hex($this->_data);
208 }
209
210 /**
211 * @return bool
212 */
213 public function isEmpty(): bool {
214 return $this->_length === 0;
215 }
216
217
218 /**
219 * jsonSerialize interface
220 * return binary data in RFC 1342-Like serialized string
221 * @return string
222 */
223 public function jsonSerialize(): string {
224 if (ByteBuffer::$useBase64UrlEncoding) {
225 return self::_base64url_encode($this->_data);
226
227 } else {
228 return '=?BINARY?B?' . \base64_encode($this->_data) . '?=';
229 }
230 }
231
232 /**
233 * Serializable-Interface
234 * @return string
235 */
236 public function serialize(): string {
237 return \serialize($this->_data);
238 }
239
240 /**
241 * Serializable-Interface
242 * @param string $serialized
243 */
244 public function unserialize($serialized) {
245 $this->_data = \unserialize($serialized);
246 $this->_length = \strlen($this->_data);
247 }
248
249 /**
250 * (PHP 8 deprecates Serializable-Interface)
251 * @return array
252 */
253 public function __serialize(): array {
254 return [
255 'data' => \serialize($this->_data)
256 ];
257 }
258
259 /**
260 * object to string
261 * @return string
262 */
263 public function __toString(): string {
264 return $this->getHex();
265 }
266
267 /**
268 * (PHP 8 deprecates Serializable-Interface)
269 * @param array $data
270 * @return void
271 */
272 public function __unserialize($data) {
273 if ($data && isset($data['data'])) {
274 $this->_data = \unserialize($data['data']);
275 $this->_length = \strlen($this->_data);
276 }
277 }
278
279 // -----------------------
280 // PROTECTED STATIC
281 // -----------------------
282
283 /**
284 * base64 url decoding
285 * @param string $data
286 * @return string
287 */
288 protected static function _base64url_decode($data): string {
289 return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4));
290 }
291
292 /**
293 * base64 url encoding
294 * @param string $data
295 * @return string
296 */
297 protected static function _base64url_encode($data): string {
298 return \rtrim(\strtr(\base64_encode($data), '+/', '-_'), '=');
299 }
300}