1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2024-2025
6: *
7: * This program is free software; you can redistribute it and/or
8: * modify it under the terms of the GNU General Public
9: * License as published by the Free Software Foundation; either
10: * version 2 of the License, or (at your option) any later version.
11: *
12: * This program is distributed in the hope that it will be useful,
13: * but WITHOUT ANY WARRANTY; without even the implied warranty of
14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15: * General Public License for more details.
16: *
17: * You should have received a copy of the GNU General Public
18: * License along with this program; if not, write to the Free
19: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20: *
21: */
22:
23: namespace SimpleID\Auth;
24:
25: use \InvalidArgumentException;
26: use SimpleJWT\Util\Util as SimpleJWTUtil;
27:
28: /**
29: * Class that parses and represents the authenticator data structure
30: * in WebAuthn.
31: *
32: * @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/Authenticator_data
33: */
34: class WebAuthnAuthenticatorData {
35: /**
36: * The hash of the RpID, in base64url encoding
37: *
38: * @var string
39: */
40: protected $rpIdHash;
41:
42: /**
43: * User present flag.
44: *
45: * @var bool
46: */
47: protected $userPresentFlag;
48:
49: /**
50: * User verified flag.
51: *
52: * @var bool
53: */
54: protected $userVerifiedFlag;
55:
56: /**
57: * Backup eligibility flag.
58: *
59: * @var bool
60: */
61: protected $backupEligibleFlag;
62:
63: /**
64: * Backup state flag.
65: *
66: * @var bool
67: */
68: protected $backupStateFlag;
69:
70: /**
71: * Flag to indicate whether attestation data exists.
72: *
73: * @var bool
74: */
75: protected $attestedDataIncludedFlag;
76:
77: /**
78: * Flag to indicate whether extension data exists.
79: *
80: * @var bool
81: */
82: protected $extensionDataIncludedFlag;
83:
84: /**
85: * Value of signature counter.
86: *
87: * @var int $signCount
88: */
89: protected $signCount;
90:
91: /**
92: * The AAGUID in hex format (e.g. 00000000-0000-0000-0000-000000000000)
93: *
94: * @var ?string $aaguid
95: */
96: protected $aaguid;
97:
98: function __construct(string $binary) {
99: if (strlen($binary) < 37)
100: throw new InvalidArgumentException('Unexpected length of authenticatorData');
101:
102: // https://www.w3.org/TR/webauthn/#sec-authenticator-data
103: // rpIdHash
104: $this->rpIdHash = SimpleJWTUtil::base64url_encode(substr($binary, 0, 32));
105:
106: // flags
107: $flags = ord($binary[32]);
108: $this->userPresentFlag = (($flags & 1) > 0);
109: $this->userVerifiedFlag = (($flags & 4) > 0);
110: $this->backupEligibleFlag = (($flags & 8) > 0);
111: $this->backupStateFlag = (($flags & 16) > 0);
112: $this->attestedDataIncludedFlag = (($flags & 64) > 0);
113: $this->extensionDataIncludedFlag = (($flags & 128) > 0);
114:
115: // signCount
116: $signCount = unpack('N', substr($binary, 33, 4));
117: if ($signCount === false) throw new InvalidArgumentException('Invalid signCount');
118: $this->signCount = $signCount[1];
119:
120: // attestationData
121: $pos = 37;
122: if ($this->attestedDataIncludedFlag) {
123: if (strlen($binary) < 56)
124: throw new InvalidArgumentException('Unexpected length of authenticatorData with attestationData');
125:
126: $hex = bin2hex(substr($binary, 37, 16));
127: $this->aaguid = sprintf('%08s-%04s-%04s-%04s-%012s', substr($hex, 0, 8), substr($hex, 8, 4), substr($hex, 12, 4), substr($hex, 16, 4), substr($hex, 20));
128: }
129: }
130:
131: /**
132: * Returns the value of the RP ID hash as a base64url encoded string
133: *
134: * @return string the RP ID hash
135: */
136: public function getRpIdHash(): string {
137: return $this->rpIdHash;
138: }
139:
140: /**
141: * Returns the value of the signature counter.
142: *
143: * @return int the value of the signature counter
144: */
145: public function getSignCount(): int {
146: return $this->signCount;
147: }
148:
149: /**
150: * Returns the AAGUID of the authenticator.
151: *
152: * The value is formatted in lowercase hex format
153: * (e.g. 00000000-0000-0000-0000-000000000000)
154: *
155: * @return ?string the AAGUID, or null if the AAGUID is not
156: * included
157: */
158: public function getAAGUID(): ?string {
159: return $this->aaguid;
160: }
161:
162: /**
163: * Returns whether the user was present. If true, the authenticator has
164: * performed a Test of User Presence (TUP), such as touching a button on
165: * the authenticator.
166: *
167: * @return bool true if the user was present
168: */
169: public function isUserPresent(): bool {
170: return $this->userPresentFlag;
171: }
172:
173: /**
174: * Returns whether the user was verified. If true, authenticator has
175: * performed verification using e.g. PIN or biometrics
176: *
177: * @return bool true if the user was verified
178: */
179: public function isUserVerified(): bool {
180: return $this->userVerifiedFlag;
181: }
182:
183: /**
184: * Returns whether the credentials stored in the authenticator can
185: * be backed up, e.g. to the cloud. This allows the credentials
186: * to be used across multiple devices.
187: *
188: * @return bool true if the authenticator can be backed up
189: */
190: public function isBackupEligible(): bool {
191: return $this->backupEligibleFlag;
192: }
193:
194: /**
195: * Returns whether the credentials stored in the authenticator has
196: * be backed up.
197: *
198: * This is only meaningful if `isBackupEligible()` returns true
199: *
200: * @return bool true if the authenticator can be backed up
201: */
202: public function isBackedUp(): bool {
203: return $this->backupStateFlag;
204: }
205: }
206: ?>
207: