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 m_requestTimeOutTimer.stop();
134 }
135 clearState();
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 m_requestTimeOutTimer.stop();
153 }
154 clearState();
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 if (m_pendingCompletionHandler)
178 return;
179 m_pendingRequestData = { };
180 m_services.clear();
181 m_authenticators.clear();
182}
183
184void AuthenticatorManager::authenticatorAdded(Ref<Authenticator>&& authenticator)
185{
186 ASSERT(RunLoop::isMain());
187 authenticator->setObserver(*this);
188 authenticator->handleRequest(m_pendingRequestData);
189 auto addResult = m_authenticators.add(WTFMove(authenticator));
190 ASSERT_UNUSED(addResult, addResult.isNewEntry);
191}
192
193void AuthenticatorManager::respondReceived(Respond&& respond)
194{
195 ASSERT(RunLoop::isMain());
196 if (!m_requestTimeOutTimer.isActive())
197 return;
198 ASSERT(m_pendingCompletionHandler);
199
200 auto shouldComplete = WTF::holds_alternative<PublicKeyCredentialData>(respond);
201 if (!shouldComplete)
202 shouldComplete = WTF::get<ExceptionData>(respond).code == InvalidStateError;
203 if (shouldComplete) {
204 m_pendingCompletionHandler(WTFMove(respond));
205 clearStateAsync();
206 m_requestTimeOutTimer.stop();
207 return;
208 }
209 respondReceivedInternal(WTFMove(respond));
210}
211
212void AuthenticatorManager::downgrade(Authenticator* id, Ref<Authenticator>&& downgradedAuthenticator)
213{
214 RunLoop::main().dispatch([weakThis = makeWeakPtr(*this), id] {
215 if (!weakThis)
216 return;
217 auto removed = weakThis->m_authenticators.remove(id);
218 ASSERT_UNUSED(removed, removed);
219 });
220 authenticatorAdded(WTFMove(downgradedAuthenticator));
221}
222
223UniqueRef<AuthenticatorTransportService> AuthenticatorManager::createService(WebCore::AuthenticatorTransport transport, AuthenticatorTransportService::Observer& observer) const
224{
225 return AuthenticatorTransportService::create(transport, observer);
226}
227
228void AuthenticatorManager::respondReceivedInternal(Respond&&)
229{
230}
231
232void AuthenticatorManager::startDiscovery(const TransportSet& transports)
233{
234 using namespace AuthenticatorManagerInternal;
235
236 ASSERT(m_services.isEmpty() && transports.size() <= maxTransportNumber);
237 for (auto& transport : transports) {
238 auto service = createService(transport, *this);
239 service->startDiscovery();
240 m_services.append(WTFMove(service));
241 }
242}
243
244void AuthenticatorManager::initTimeOutTimer(const Optional<unsigned>& timeOutInMs)
245{
246 using namespace AuthenticatorManagerInternal;
247
248 unsigned timeOutInMsValue = std::min(maxTimeOutValue, timeOutInMs.valueOr(maxTimeOutValue));
249 m_requestTimeOutTimer.startOneShot(Seconds::fromMilliseconds(timeOutInMsValue));
250}
251
252void AuthenticatorManager::timeOutTimerFired()
253{
254 ASSERT(m_requestTimeOutTimer.isActive());
255 m_pendingCompletionHandler((ExceptionData { NotAllowedError, "Operation timed out."_s }));
256 clearState();
257}
258
259} // namespace WebKit
260
261#endif // ENABLE(WEB_AUTHN)
262