1/*
2 * Copyright (C) 2018 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "AuthenticatorManager.h"
28
29#if ENABLE(WEB_AUTHN)
30
31#include <WebCore/AuthenticatorTransport.h>
32#include <WebCore/PublicKeyCredentialCreationOptions.h>
33#include <wtf/MonotonicTime.h>
34
35namespace WebKit {
36using namespace WebCore;
37
38namespace AuthenticatorManagerInternal {
39
40#if PLATFORM(MAC)
41const size_t maxTransportNumber = 2;
42#else
43const size_t maxTransportNumber = 1;
44#endif
45
46// Suggested by WebAuthN spec as of 7 August 2018.
47const unsigned maxTimeOutValue = 120000;
48
49// FIXME(188624, 188625): Support NFC and BLE authenticators.
50static AuthenticatorManager::TransportSet collectTransports(const Optional<PublicKeyCredentialCreationOptions::AuthenticatorSelectionCriteria>& authenticatorSelection)
51{
52 AuthenticatorManager::TransportSet result;
53 if (!authenticatorSelection || !authenticatorSelection->authenticatorAttachment) {
54 auto addResult = result.add(AuthenticatorTransport::Internal);
55 ASSERT_UNUSED(addResult, addResult.isNewEntry);
56#if PLATFORM(MAC)
57 addResult = result.add(AuthenticatorTransport::Usb);
58 ASSERT_UNUSED(addResult, addResult.isNewEntry);
59#endif
60 return result;
61 }
62
63 if (authenticatorSelection->authenticatorAttachment == PublicKeyCredentialCreationOptions::AuthenticatorAttachment::Platform) {
64 auto addResult = result.add(AuthenticatorTransport::Internal);
65 ASSERT_UNUSED(addResult, addResult.isNewEntry);
66 return result;
67 }
68 if (authenticatorSelection->authenticatorAttachment == PublicKeyCredentialCreationOptions::AuthenticatorAttachment::CrossPlatform) {
69#if PLATFORM(MAC)
70 auto addResult = result.add(AuthenticatorTransport::Usb);
71 ASSERT_UNUSED(addResult, addResult.isNewEntry);
72#endif
73 return result;
74 }
75
76 ASSERT_NOT_REACHED();
77 return result;
78}
79
80// FIXME(188624, 188625): Support NFC and BLE authenticators.
81// The goal is to find a union of different transports from allowCredentials.
82// If it is not specified or any of its credentials doesn't specify its own. We should discover all.
83// This is a variant of Step. 18.*.4 from https://www.w3.org/TR/webauthn/#discover-from-external-source
84// as of 7 August 2018.
85static AuthenticatorManager::TransportSet collectTransports(const Vector<PublicKeyCredentialDescriptor>& allowCredentials)
86{
87 AuthenticatorManager::TransportSet result;
88 if (allowCredentials.isEmpty()) {
89 auto addResult = result.add(AuthenticatorTransport::Internal);
90 ASSERT_UNUSED(addResult, addResult.isNewEntry);
91#if PLATFORM(MAC)
92 addResult = result.add(AuthenticatorTransport::Usb);
93 ASSERT_UNUSED(addResult, addResult.isNewEntry);
94#endif
95 return result;
96 }
97
98 for (auto& allowCredential : allowCredentials) {
99 if (allowCredential.transports.isEmpty()) {
100 result.add(AuthenticatorTransport::Internal);
101#if PLATFORM(MAC)
102 result.add(AuthenticatorTransport::Usb);
103 return result;
104#endif
105 }
106 if (!result.contains(AuthenticatorTransport::Internal) && allowCredential.transports.contains(AuthenticatorTransport::Internal))
107 result.add(AuthenticatorTransport::Internal);
108#if PLATFORM(MAC)
109 if (!result.contains(AuthenticatorTransport::Usb) && allowCredential.transports.contains(AuthenticatorTransport::Usb))
110 result.add(AuthenticatorTransport::Usb);
111#endif
112 if (result.size() >= maxTransportNumber)
113 return result;
114 }
115
116 ASSERT(result.size() < maxTransportNumber);
117 return result;
118}
119
120} // namespace AuthenticatorManagerInternal
121
122AuthenticatorManager::AuthenticatorManager()
123 : m_requestTimeOutTimer(RunLoop::main(), this, &AuthenticatorManager::timeOutTimerFired)
124{
125}
126
127void AuthenticatorManager::makeCredential(const Vector<uint8_t>& hash, const PublicKeyCredentialCreationOptions& options, Callback&& callback)
128{
129 using namespace AuthenticatorManagerInternal;
130
131 if (m_pendingCompletionHandler) {
132 m_pendingCompletionHandler(ExceptionData { NotAllowedError, "This request has been cancelled by a new request."_s });
133 clearState();
134 m_requestTimeOutTimer.stop();
135 }
136
137 // 1. Save request for async operations.
138 m_pendingRequestData = { hash, true, options, { } };
139 m_pendingCompletionHandler = WTFMove(callback);
140 initTimeOutTimer(options.timeout);
141
142 // 2. Get available transports and start discovering authenticators on them.
143 startDiscovery(collectTransports(options.authenticatorSelection));
144}
145
146void AuthenticatorManager::getAssertion(const Vector<uint8_t>& hash, const PublicKeyCredentialRequestOptions& options, Callback&& callback)
147{
148 using namespace AuthenticatorManagerInternal;
149
150 if (m_pendingCompletionHandler) {
151 m_pendingCompletionHandler(ExceptionData { NotAllowedError, "This request has been cancelled by a new request."_s });
152 clearState();
153 m_requestTimeOutTimer.stop();
154 }
155
156 // 1. Save request for async operations.
157 m_pendingRequestData = { hash, false, { }, options };
158 m_pendingCompletionHandler = WTFMove(callback);
159 initTimeOutTimer(options.timeout);
160
161 // 2. Get available transports and start discovering authenticators on them.
162 ASSERT(m_services.isEmpty());
163 startDiscovery(collectTransports(options.allowCredentials));
164}
165
166void AuthenticatorManager::clearStateAsync()
167{
168 RunLoop::main().dispatch([weakThis = makeWeakPtr(*this)] {
169 if (!weakThis)
170 return;
171 weakThis->clearState();
172 });
173}
174
175void AuthenticatorManager::clearState()
176{
177 m_pendingRequestData = { };
178 ASSERT(!m_pendingCompletionHandler);
179 m_services.clear();
180 m_authenticators.clear();
181}
182
183void AuthenticatorManager::authenticatorAdded(Ref<Authenticator>&& authenticator)
184{
185 ASSERT(RunLoop::isMain());
186 authenticator->setObserver(*this);
187 authenticator->handleRequest(m_pendingRequestData);
188 auto addResult = m_authenticators.add(WTFMove(authenticator));
189 ASSERT_UNUSED(addResult, addResult.isNewEntry);
190}
191
192void AuthenticatorManager::respondReceived(Respond&& respond)
193{
194 ASSERT(RunLoop::isMain());
195 if (!m_requestTimeOutTimer.isActive())
196 return;
197 ASSERT(m_pendingCompletionHandler);
198
199 auto shouldComplete = WTF::holds_alternative<PublicKeyCredentialData>(respond);
200 if (!shouldComplete)
201 shouldComplete = WTF::get<ExceptionData>(respond).code == InvalidStateError;
202 if (shouldComplete) {
203 m_pendingCompletionHandler(WTFMove(respond));
204 clearStateAsync();
205 m_requestTimeOutTimer.stop();
206 return;
207 }
208 respondReceivedInternal(WTFMove(respond));
209}
210
211void AuthenticatorManager::downgrade(Authenticator* id, Ref<Authenticator>&& downgradedAuthenticator)
212{
213 RunLoop::main().dispatch([weakThis = makeWeakPtr(*this), id] {
214 if (!weakThis)
215 return;
216 auto removed = weakThis->m_authenticators.remove(id);
217 ASSERT_UNUSED(removed, removed);
218 });
219 authenticatorAdded(WTFMove(downgradedAuthenticator));
220}
221
222UniqueRef<AuthenticatorTransportService> AuthenticatorManager::createService(WebCore::AuthenticatorTransport transport, AuthenticatorTransportService::Observer& observer) const
223{
224 return AuthenticatorTransportService::create(transport, observer);
225}
226
227void AuthenticatorManager::respondReceivedInternal(Respond&&)
228{
229}
230
231void AuthenticatorManager::startDiscovery(const TransportSet& transports)
232{
233 using namespace AuthenticatorManagerInternal;
234
235 ASSERT(m_services.isEmpty() && transports.size() <= maxTransportNumber);
236 for (auto& transport : transports) {
237 auto service = createService(transport, *this);
238 service->startDiscovery();
239 m_services.append(WTFMove(service));
240 }
241}
242
243void AuthenticatorManager::initTimeOutTimer(const Optional<unsigned>& timeOutInMs)
244{
245 using namespace AuthenticatorManagerInternal;
246
247 unsigned timeOutInMsValue = std::min(maxTimeOutValue, timeOutInMs.valueOr(maxTimeOutValue));
248 m_requestTimeOutTimer.startOneShot(Seconds::fromMilliseconds(timeOutInMsValue));
249}
250
251void AuthenticatorManager::timeOutTimerFired()
252{
253 ASSERT(m_requestTimeOutTimer.isActive());
254 m_pendingCompletionHandler((ExceptionData { NotAllowedError, "Operation timed out."_s }));
255 clearState();
256}
257
258} // namespace WebKit
259
260#endif // ENABLE(WEB_AUTHN)
261