PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.4
Defender Security – Malware Scanner, Login Security & Firewall v6.2.4
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / class-webauthn.php

class-webauthn.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.4, at src/component/class-webauthn.php

310 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles WebAuthn functionalities providing methods to manage and verify user credentials.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use WP_User;
11 use WP_DEFENDER_VENDOR\Webauthn\PublicKeyCredentialSource;
12 use WP_DEFENDER_VENDOR\Webauthn\PublicKeyCredentialUserEntity;
13 use WP_DEFENDER_VENDOR\Webauthn\TrustPath\CertificateTrustPath;
14 use WP_DEFENDER_VENDOR\Webauthn\TrustPath\EcdaaKeyIdTrustPath;
15 use WP_DEFENDER_VENDOR\Webauthn\TrustPath\EmptyTrustPath;
16 use WP_Defender\Traits\Webauthn as Webauthn_Trait;
17 use WP_DEFENDER_VENDOR\Webauthn\PublicKeyCredentialSourceRepository;
18
19 /**
20 * Handles WebAuthn functionalities providing methods to manage and verify user credentials.
21 *
22 * @since 3.0.0
23 */
24 class Webauthn implements PublicKeyCredentialSourceRepository {
25
26 use Webauthn_Trait;
27
28 /**
29 * Option key for storing user credentials.
30 *
31 * @var string
32 */
33 public const CREDENTIAL_OPTION_KEY = 'user_credentials';
34
35 /**
36 * Meta key for storing credentials having userHandle mismatch.
37 *
38 * @var string
39 */
40 public const USER_HANDLE_MISMATCH_KEY = 'user_handle_match_failed';
41
42 /**
43 * User ID that findOneByCredentialId() lookups are currently scoped to.
44 * Must be set only via set_expected_user_id() from trusted server-side
45 * context (e.g. the WP_User passed down the 2FA login chain) — never
46 * derived from request input inside this class.
47 *
48 * @var int|null
49 */
50 private $expected_user_id = null;
51
52 /**
53 * Restrict findOneByCredentialId() to a specific user's credential set.
54 * Call this before delegating to Webauthn\Server so the library's internal
55 * repository lookups stay scoped to the expected 2FA target user.
56 *
57 * @param int|null $user_id Trusted user ID, or null to clear the scope.
58 *
59 * @return void
60 */
61 public function set_expected_user_id( ?int $user_id ): void {
62 $this->expected_user_id = $user_id;
63 }
64
65 /**
66 * Get user credentials.
67 *
68 * @param int $user_id The user ID.
69 *
70 * @return array
71 */
72 public function getCredentials( int $user_id ): array {
73 return $this->get_user_meta( $user_id, self::CREDENTIAL_OPTION_KEY );
74 }
75
76 /**
77 * Set user credentials.
78 *
79 * @param int $user_id The user ID.
80 * @param array $data The credentials data.
81 *
82 * @return bool
83 */
84 public function setCredentials( int $user_id, array $data ): bool {
85 return false !== $this->update_user_meta( $user_id, self::CREDENTIAL_OPTION_KEY, $data );
86 }
87
88 /**
89 * Get one credential by credential ID.
90 * {@inheritDoc}
91 * Interface-mandated single-parameter signature. Scoping to a specific
92 * user is done via $this->expected_user_id, set beforehand through
93 * set_expected_user_id() by trusted calling code — never via $_POST.
94 *
95 * @param string $public_key_credential_id The public key credential ID.
96 *
97 * @return PublicKeyCredentialSource|null
98 */
99 public function findOneByCredentialId( string $public_key_credential_id ): ?PublicKeyCredentialSource {
100 if ( null === $this->expected_user_id ) {
101 // No trusted scope set — deny rather than search globally.
102 // Never needs an unscoped, cross-account lookup.
103 return null;
104 }
105
106 $data = $this->getCredentials( $this->expected_user_id );
107 $key = base64_encode( $public_key_credential_id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
108 if ( isset( $data[ $key ]['credential_source'] ) ) {
109 $credential_source = $data[ $key ]['credential_source']; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
110
111 return PublicKeyCredentialSource::createFromArray( $this->normalize_legacy_trust_path( $credential_source ) );
112 }
113
114 return null;
115 }
116
117 /**
118 * Get all credentials of a user
119 *
120 * @param PublicKeyCredentialUserEntity $public_key_credential_user_entity The user entity.
121 *
122 * @return array
123 */
124 public function findAllForUserEntity( PublicKeyCredentialUserEntity $public_key_credential_user_entity ): array {
125 $credentials = array();
126 $username = $public_key_credential_user_entity->getName();
127 $user = get_user_by( 'login', $username );
128
129 if ( is_object( $user ) ) {
130 $credentials = $this->findAllForUserByType( $user->ID );
131 }
132
133 return $credentials;
134 }
135
136 /**
137 * Get all credentials of a user by authenticator type.
138 *
139 * @param int $user_id The user ID.
140 * @param null|string $type The type of authenticator.
141 *
142 * @return array
143 * @since 3.1.0
144 */
145 public function findAllForUserByType( int $user_id, $type = null ): array {
146 $sources = array();
147 $user_data = $this->getCredentials( $user_id );
148
149 if ( is_array( $user_data ) ) {
150 foreach ( $user_data as $data ) {
151 if (
152 ! in_array( $type, array( null, '' ), true )
153 && isset( $data['authenticator_type'] )
154 && '' !== $data['authenticator_type']
155 && $type !== $data['authenticator_type']
156 ) {
157 continue;
158 }
159
160 if ( isset( $data['credential_source'] ) ) {
161 $sources[] = PublicKeyCredentialSource::createFromArray(
162 $this->normalize_legacy_trust_path( $data['credential_source'] )
163 );
164 }
165 }
166 }
167
168 return $sources;
169 }
170
171 /**
172 * Map trust-path class names stored by Defender 6.1 to their Mozart-prefixed 6.2 classes.
173 *
174 * @param array $credential_source Stored WebAuthn credential source.
175 *
176 * @return array
177 */
178 private function normalize_legacy_trust_path( array $credential_source ): array {
179 $legacy_type = $credential_source['trustPath']['type'] ?? null;
180 $legacy_map = array(
181 'Webauthn\\TrustPath\\EmptyTrustPath' => EmptyTrustPath::class,
182 'Webauthn\\TrustPath\\EcdaaKeyIdTrustPath' => EcdaaKeyIdTrustPath::class,
183 'Webauthn\\TrustPath\\CertificateTrustPath' => CertificateTrustPath::class,
184 );
185
186 if ( is_string( $legacy_type ) && isset( $legacy_map[ $legacy_type ] ) ) {
187 $credential_source['trustPath']['type'] = $legacy_map[ $legacy_type ];
188 }
189
190 return $credential_source;
191 }
192
193 /**
194 * Store credential into database.
195 *
196 * @param PublicKeyCredentialSource $public_key_credential_source The credential source to store.
197 *
198 * @return void
199 */
200 public function saveCredentialSource( PublicKeyCredentialSource $public_key_credential_source ): void {
201 $user_id = get_current_user_id();
202 $data = $this->getCredentials( $user_id );
203 $key = base64_encode( $public_key_credential_source->getPublicKeyCredentialId() ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
204
205 if ( ! isset( $data[ $key ] ) ) {
206 $data[ $key ] = array(
207 'label' => defender_get_data_from_request( 'name', 'p' ) ?? '',
208 'added' => time(),
209 'authenticator_type' => defender_get_data_from_request( 'type', 'p' ) ?? '',
210 'user' => $public_key_credential_source->getUserHandle(),
211 'credential_source' => $public_key_credential_source,
212 );
213 } else {
214 $data[ $key ]['credential_source'] = $public_key_credential_source;
215 }
216
217 $this->setCredentials( $user_id, $data );
218 }
219
220 /**
221 * Get userHandle mismatch list.
222 *
223 * @param int $user_id The user ID.
224 *
225 * @return array
226 * @since 3.4.0
227 */
228 public function getUserHandleMatchFailed( int $user_id ): array {
229 $meta_key = $this->option_prefix . self::USER_HANDLE_MISMATCH_KEY;
230 $meta_val = get_user_meta( $user_id, $meta_key, true );
231
232 return is_array( $meta_val ) ? $meta_val : array();
233 }
234
235 /**
236 * Set userHandle mismatch list.
237 *
238 * @param int $user_id The user ID.
239 * @param array $meta_val The mismatches to set.
240 *
241 * @return void
242 * @since 3.4.0
243 */
244 public function setUserHandleMatchFailed( int $user_id, array $meta_val ): void {
245 $meta_key = $this->option_prefix . self::USER_HANDLE_MISMATCH_KEY;
246 update_user_meta( $user_id, $meta_key, $meta_val );
247 }
248
249 /**
250 * Add authenticators to userHandle mismatch list.
251 *
252 * @param WP_User $user The user object.
253 * @param array $data The data to add.
254 *
255 * @return void
256 * @since 3.4.0
257 */
258 public function addUserHandleMatchFailed( $user, $data ): void {
259 if ( isset( $user->ID ) && $user->ID > 0 && '' !== $data['rawId'] ) {
260 $meta_val = $this->getUserHandleMatchFailed( $user->ID );
261 $meta_val['show_notice'] = $meta_val['show_notice'] ?? true;
262
263 if ( ! isset( $meta_val['authenticators'] ) || array() === $meta_val['authenticators'] || ! in_array( $data['rawId'], $meta_val['authenticators'], true ) ) {
264 $meta_val['authenticators'][] = $data['rawId'];
265 }
266
267 $this->setUserHandleMatchFailed( $user->ID, $meta_val );
268 }
269 }
270
271 /**
272 * Remove authenticator from userHandle mismatch list.
273 *
274 * @param int $user_id The user ID.
275 * @param string $auth_id The authenticator ID to remove.
276 *
277 * @return void
278 * @since 3.4.0
279 */
280 public function removeUserHandleMatchFailed( int $user_id, string $auth_id ): void {
281 if ( '' !== $auth_id ) {
282 $meta_val = $this->getUserHandleMatchFailed( $user_id );
283
284 if ( isset( $meta_val['authenticators'] ) && is_array( $meta_val['authenticators'] ) && array() !== $meta_val['authenticators'] ) {
285 $pos = array_search( $auth_id, $meta_val['authenticators'], true );
286
287 if ( false !== $pos ) {
288 array_splice( $meta_val['authenticators'], $pos, 1 );
289 $this->setUserHandleMatchFailed( $user_id, $meta_val );
290 }
291 }
292 }
293 }
294
295 /**
296 * Disable userHandle mismatch notice.
297 *
298 * @param int $user_id The user ID.
299 *
300 * @return void
301 * @since 3.4.0
302 */
303 public function disableUserHandleMatchFailedNotice( int $user_id ): void {
304 $meta_val = $this->getUserHandleMatchFailed( $user_id );
305 $meta_val['show_notice'] = false;
306
307 $this->setUserHandleMatchFailed( $user_id, $meta_val );
308 }
309 }
310