Passed
Push — master ( b39fb5...590849 )
by Roeland
11:59 queued 10s
created

Manager::finishRegister()   A

Complexity

Conditions 3
Paths 6

Size

Total Lines 43
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 24
c 1
b 0
f 0
nc 6
nop 3
dl 0
loc 43
rs 9.536
1
<?php
2
declare(strict_types=1);
3
/**
4
 * @copyright Copyright (c) 2020, Roeland Jago Douma <[email protected]>
5
 *
6
 * @author Roeland Jago Douma <[email protected]>
7
 *
8
 * @license GNU AGPL version 3 or any later version
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License as
12
 * published by the Free Software Foundation, either version 3 of the
13
 * License, or (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 *
23
 */
24
25
namespace OC\Authentication\WebAuthn;
26
27
use Cose\Algorithm\Signature\ECDSA\ES256;
28
use Cose\Algorithm\Signature\RSA\RS256;
29
use Cose\Algorithms;
30
use GuzzleHttp\Psr7\ServerRequest;
31
use OC\Authentication\WebAuthn\Db\PublicKeyCredentialEntity;
32
use OC\Authentication\WebAuthn\Db\PublicKeyCredentialMapper;
33
use OCP\AppFramework\Db\DoesNotExistException;
34
use OCP\IConfig;
35
use OCP\ILogger;
36
use OCP\IUser;
37
use Webauthn\AttestationStatement\AttestationObjectLoader;
38
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
39
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
40
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
41
use Webauthn\AuthenticatorAssertionResponse;
42
use Webauthn\AuthenticatorAssertionResponseValidator;
43
use Webauthn\AuthenticatorAttestationResponse;
44
use Webauthn\AuthenticatorAttestationResponseValidator;
45
use Webauthn\AuthenticatorSelectionCriteria;
46
use Webauthn\PublicKeyCredentialCreationOptions;
47
use Webauthn\PublicKeyCredentialDescriptor;
48
use Webauthn\PublicKeyCredentialLoader;
49
use Webauthn\PublicKeyCredentialParameters;
50
use Webauthn\PublicKeyCredentialRequestOptions;
51
use Webauthn\PublicKeyCredentialRpEntity;
52
use Webauthn\PublicKeyCredentialSource;
53
use Webauthn\PublicKeyCredentialUserEntity;
54
use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;
55
56
class Manager {
57
58
	/** @var CredentialRepository */
59
	private $repository;
60
61
	/** @var PublicKeyCredentialMapper */
62
	private $credentialMapper;
63
64
	/** @var ILogger */
65
	private $logger;
66
67
	/** @var IConfig */
68
	private $config;
69
70
	public function __construct(
71
		CredentialRepository $repository,
72
		PublicKeyCredentialMapper $credentialMapper,
73
		ILogger $logger,
74
		IConfig $config
75
	) {
76
		$this->repository = $repository;
77
		$this->credentialMapper = $credentialMapper;
78
		$this->logger = $logger;
79
		$this->config = $config;
80
	}
81
82
	public function startRegistration(IUser $user, string $serverHost): PublicKeyCredentialCreationOptions {
83
		$rpEntity = new PublicKeyCredentialRpEntity(
84
			'Nextcloud', //Name
85
			$this->stripPort($serverHost),        //ID
86
			null                            //Icon
87
		);
88
89
		$userEntity = new PublicKeyCredentialUserEntity(
90
			$user->getUID(),                              //Name
91
			$user->getUID(),                              //ID
92
			$user->getDisplayName()                      //Display name
93
//            'https://foo.example.co/avatar/123e4567-e89b-12d3-a456-426655440000' //Icon
94
		);
95
96
		$challenge = random_bytes(32);
97
98
		$publicKeyCredentialParametersList = [
99
			new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_ES256),
100
			new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_RS256),
101
		];
102
103
		$timeout = 60000;
104
105
		$excludedPublicKeyDescriptors = [
106
		];
107
108
		$authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria();
109
110
		return new PublicKeyCredentialCreationOptions(
111
			$rpEntity,
112
			$userEntity,
113
			$challenge,
114
			$publicKeyCredentialParametersList,
115
			$timeout,
116
			$excludedPublicKeyDescriptors,
117
			$authenticatorSelectionCriteria,
118
			PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
119
			null
120
		);
121
	}
122
123
	public function finishRegister(PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, string $name, string $data): PublicKeyCredentialEntity {
124
		$tokenBindingHandler = new TokenBindingNotSupportedHandler();
125
126
		$attestationStatementSupportManager = new AttestationStatementSupportManager();
127
		$attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
128
129
		$attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager);
130
		$publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader);
131
132
		// Extension Output Checker Handler
133
		$extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler();
134
135
		// Authenticator Attestation Response Validator
136
		$authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator(
137
			$attestationStatementSupportManager,
138
			$this->repository,
139
			$tokenBindingHandler,
140
			$extensionOutputCheckerHandler
141
		);
142
143
		try {
144
			// Load the data
145
			$publicKeyCredential = $publicKeyCredentialLoader->load($data);
146
			$response = $publicKeyCredential->getResponse();
147
148
			// Check if the response is an Authenticator Attestation Response
149
			if (!$response instanceof AuthenticatorAttestationResponse) {
150
				throw new \RuntimeException('Not an authenticator attestation response');
151
			}
152
153
			// Check the response against the request
154
			$request = ServerRequest::fromGlobals();
155
156
			$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
157
				$response,
158
				$publicKeyCredentialCreationOptions,
159
				$request);
160
		} catch (\Throwable $exception) {
161
			throw $exception;
162
		}
163
164
		// Persist the data
165
		return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name);
166
	}
167
168
	private function stripPort(string $serverHost): string {
169
		return preg_replace('/(:\d+$)/', '', $serverHost);
170
	}
171
172
	public function startAuthentication(string $uid, string $serverHost): PublicKeyCredentialRequestOptions {
173
				// List of registered PublicKeyCredentialDescriptor classes associated to the user
174
		$registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) {
175
			$credential = $entity->toPublicKeyCredentialSource();
176
			return new PublicKeyCredentialDescriptor(
177
				$credential->getType(),
178
				$credential->getPublicKeyCredentialId()
179
			);
180
		}, $this->credentialMapper->findAllForUid($uid));
181
182
		// Public Key Credential Request Options
183
		return new PublicKeyCredentialRequestOptions(
184
			random_bytes(32),                                                    // Challenge
185
			60000,                                                              // Timeout
186
			$this->stripPort($serverHost),                                                                  // Relying Party ID
187
			$registeredPublicKeyCredentialDescriptors                                  // Registered PublicKeyCredentialDescriptor classes
188
		);
189
	}
190
191
	public function finishAuthentication(PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, string $data, string $uid) {
192
		$attestationStatementSupportManager = new AttestationStatementSupportManager();
193
		$attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
194
195
		$attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager);
196
		$publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader);
197
198
		$tokenBindingHandler = new TokenBindingNotSupportedHandler();
199
		$extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler();
200
		$algorithmManager = new \Cose\Algorithm\Manager();
201
		$algorithmManager->add(new ES256());
202
		$algorithmManager->add(new RS256());
203
204
		$authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
205
			$this->repository,
206
			$tokenBindingHandler,
207
			$extensionOutputCheckerHandler,
208
			$algorithmManager
209
		);
210
211
		try {
212
			$this->logger->debug('Loading publickey credentials from: ' . $data);
213
214
			// Load the data
215
			$publicKeyCredential = $publicKeyCredentialLoader->load($data);
216
			$response = $publicKeyCredential->getResponse();
217
218
			// Check if the response is an Authenticator Attestation Response
219
			if (!$response instanceof AuthenticatorAssertionResponse) {
220
				throw new \RuntimeException('Not an authenticator attestation response');
221
			}
222
223
			// Check the response against the request
224
			$request = ServerRequest::fromGlobals();
225
226
			$publicKeyCredentialSource = $authenticatorAssertionResponseValidator->check(
0 ignored issues
show
Unused Code introduced by
The assignment to $publicKeyCredentialSource is dead and can be removed.
Loading history...
227
				$publicKeyCredential->getRawId(),
228
				$response,
229
				$publicKeyCredentialRequestOptions,
230
				$request,
231
				$uid
232
			);
233
234
		} catch (\Throwable $e) {
235
			throw $e;
236
		}
237
238
239
240
		return true;
241
	}
242
243
	public function deleteRegistration(IUser $user, int $id): void {
244
		try {
245
			$entry = $this->credentialMapper->findById($user->getUID(), $id);
246
		} catch (DoesNotExistException $e) {
247
			$this->logger->warning("WebAuthn device $id does not exist, can't delete it");
248
			return;
249
		}
250
251
		$this->credentialMapper->delete($entry);
252
	}
253
254
	public function isWebAuthnAvailable(): bool {
255
		if (!extension_loaded('bcmath')) {
256
			return false;
257
		}
258
259
		if (!extension_loaded('gmp')) {
260
			return false;
261
		}
262
263
		if (!$this->config->getSystemValueBool('auth.webauthn.enabled', true)) {
264
			return false;
265
		}
266
267
		return true;
268
	}
269
}
270