/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/U2FTokenManager.h" #include "mozilla/dom/U2FTokenTransport.h" #include "mozilla/dom/U2FHIDTokenManager.h" #include "mozilla/dom/U2FSoftTokenManager.h" #include "mozilla/dom/PWebAuthnTransactionParent.h" #include "mozilla/MozPromise.h" #include "mozilla/dom/WebAuthnUtil.h" #include "mozilla/ipc/BackgroundParent.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/Preferences.h" #include "mozilla/Services.h" #include "mozilla/Unused.h" #include "nsIObserver.h" #include "nsIObserverService.h" #include "nsIThread.h" #include "nsTextFormatter.h" #include "mozilla/Telemetry.h" #ifdef MOZ_WIDGET_ANDROID # include "mozilla/dom/AndroidWebAuthnTokenManager.h" #endif // Not named "security.webauth.u2f_softtoken_counter" because setting that // name causes the window.u2f object to disappear until preferences get // reloaded, as its pref is a substring! #define PREF_U2F_NSSTOKEN_COUNTER "security.webauth.softtoken_counter" #define PREF_WEBAUTHN_SOFTTOKEN_ENABLED \ "security.webauth.webauthn_enable_softtoken" #define PREF_WEBAUTHN_USBTOKEN_ENABLED \ "security.webauth.webauthn_enable_usbtoken" #define PREF_WEBAUTHN_ALLOW_DIRECT_ATTESTATION \ "security.webauth.webauthn_testing_allow_direct_attestation" #define PREF_WEBAUTHN_ANDROID_FIDO2_ENABLED \ "security.webauth.webauthn_enable_android_fido2" namespace mozilla { namespace dom { /*********************************************************************** * Statics **********************************************************************/ class U2FPrefManager; namespace { static mozilla::LazyLogModule gU2FTokenManagerLog("u2fkeymanager"); StaticRefPtr gU2FTokenManager; StaticRefPtr gPrefManager; static nsIThread* gBackgroundThread; } // namespace // Data for WebAuthn UI prompt notifications. static const char16_t kRegisterPromptNotifcation[] = u"{\"action\":\"register\",\"tid\":%llu,\"origin\":\"%s\"," u"\"browsingContextId\":%llu}"; static const char16_t kRegisterDirectPromptNotifcation[] = u"{\"action\":\"register-direct\",\"tid\":%llu,\"origin\":\"%s\"," u"\"browsingContextId\":%llu}"; static const char16_t kSignPromptNotifcation[] = u"{\"action\":\"sign\",\"tid\":%llu,\"origin\":\"%s\"," u"\"browsingContextId\":%" u"llu}"; static const char16_t kCancelPromptNotifcation[] = u"{\"action\":\"cancel\",\"tid\":%llu}"; class U2FPrefManager final : public nsIObserver { private: U2FPrefManager() : mPrefMutex("U2FPrefManager Mutex") { UpdateValues(); } ~U2FPrefManager() = default; public: NS_DECL_ISUPPORTS static U2FPrefManager* GetOrCreate() { MOZ_ASSERT(NS_IsMainThread()); if (!gPrefManager) { gPrefManager = new U2FPrefManager(); Preferences::AddStrongObserver(gPrefManager, PREF_WEBAUTHN_SOFTTOKEN_ENABLED); Preferences::AddStrongObserver(gPrefManager, PREF_U2F_NSSTOKEN_COUNTER); Preferences::AddStrongObserver(gPrefManager, PREF_WEBAUTHN_USBTOKEN_ENABLED); Preferences::AddStrongObserver(gPrefManager, PREF_WEBAUTHN_ANDROID_FIDO2_ENABLED); Preferences::AddStrongObserver(gPrefManager, PREF_WEBAUTHN_ALLOW_DIRECT_ATTESTATION); ClearOnShutdown(&gPrefManager, ShutdownPhase::ShutdownThreads); } return gPrefManager; } static U2FPrefManager* Get() { return gPrefManager; } bool GetSoftTokenEnabled() { MutexAutoLock lock(mPrefMutex); return mSoftTokenEnabled; } int GetSoftTokenCounter() { MutexAutoLock lock(mPrefMutex); return mSoftTokenCounter; } bool GetUsbTokenEnabled() { MutexAutoLock lock(mPrefMutex); return mUsbTokenEnabled; } bool GetAndroidFido2Enabled() { MutexAutoLock lock(mPrefMutex); return mAndroidFido2Enabled; } bool GetAllowDirectAttestationForTesting() { MutexAutoLock lock(mPrefMutex); return mAllowDirectAttestation; } NS_IMETHODIMP Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) override { UpdateValues(); return NS_OK; } private: void UpdateValues() { MOZ_ASSERT(NS_IsMainThread()); MutexAutoLock lock(mPrefMutex); mSoftTokenEnabled = Preferences::GetBool(PREF_WEBAUTHN_SOFTTOKEN_ENABLED); mSoftTokenCounter = Preferences::GetUint(PREF_U2F_NSSTOKEN_COUNTER); mUsbTokenEnabled = Preferences::GetBool(PREF_WEBAUTHN_USBTOKEN_ENABLED); mAndroidFido2Enabled = Preferences::GetBool(PREF_WEBAUTHN_ANDROID_FIDO2_ENABLED); mAllowDirectAttestation = Preferences::GetBool(PREF_WEBAUTHN_ALLOW_DIRECT_ATTESTATION); } Mutex mPrefMutex; bool mSoftTokenEnabled; int mSoftTokenCounter; bool mUsbTokenEnabled; bool mAndroidFido2Enabled; bool mAllowDirectAttestation; }; NS_IMPL_ISUPPORTS(U2FPrefManager, nsIObserver); /*********************************************************************** * U2FManager Implementation **********************************************************************/ NS_IMPL_ISUPPORTS(U2FTokenManager, nsIU2FTokenManager); U2FTokenManager::U2FTokenManager() : mTransactionParent(nullptr), mLastTransactionId(0) { MOZ_ASSERT(XRE_IsParentProcess()); // Create on the main thread to make sure ClearOnShutdown() works. MOZ_ASSERT(NS_IsMainThread()); // Create the preference manager while we're initializing. U2FPrefManager::GetOrCreate(); } // static void U2FTokenManager::Initialize() { if (!XRE_IsParentProcess()) { return; } MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(!gU2FTokenManager); gU2FTokenManager = new U2FTokenManager(); ClearOnShutdown(&gU2FTokenManager); } // static U2FTokenManager* U2FTokenManager::Get() { MOZ_ASSERT(XRE_IsParentProcess()); // We should only be accessing this on the background thread MOZ_ASSERT(!NS_IsMainThread()); return gU2FTokenManager; } void U2FTokenManager::AbortTransaction(const uint64_t& aTransactionId, const nsresult& aError) { Unused << mTransactionParent->SendAbort(aTransactionId, aError); ClearTransaction(); } void U2FTokenManager::AbortOngoingTransaction() { if (mLastTransactionId > 0 && mTransactionParent) { // Send an abort to any other ongoing transaction Unused << mTransactionParent->SendAbort(mLastTransactionId, NS_ERROR_DOM_ABORT_ERR); } ClearTransaction(); } void U2FTokenManager::MaybeClearTransaction( PWebAuthnTransactionParent* aParent) { // Only clear if we've been requested to do so by our current transaction // parent. if (mTransactionParent == aParent) { ClearTransaction(); } } void U2FTokenManager::ClearTransaction() { if (mLastTransactionId) { // Remove any prompts we might be showing for the current transaction. SendPromptNotification(kCancelPromptNotifcation, mLastTransactionId); } mTransactionParent = nullptr; // Drop managers at the end of all transactions if (mTokenManagerImpl) { mTokenManagerImpl->Drop(); mTokenManagerImpl = nullptr; } // Forget promises, if necessary. mRegisterPromise.DisconnectIfExists(); mSignPromise.DisconnectIfExists(); // Clear transaction id. mLastTransactionId = 0; // Forget any pending registration. mPendingRegisterInfo.reset(); } template void U2FTokenManager::SendPromptNotification(const char16_t* aFormat, T... aArgs) { mozilla::ipc::AssertIsOnBackgroundThread(); nsAutoString json; nsTextFormatter::ssprintf(json, aFormat, aArgs...); nsCOMPtr r(NewRunnableMethod( "U2FTokenManager::RunSendPromptNotification", this, &U2FTokenManager::RunSendPromptNotification, json)); MOZ_ALWAYS_SUCCEEDS( GetMainThreadEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL)); } void U2FTokenManager::RunSendPromptNotification(nsString aJSON) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr os = services::GetObserverService(); if (NS_WARN_IF(!os)) { return; } nsCOMPtr self = this; MOZ_ALWAYS_SUCCEEDS( os->NotifyObservers(self, "webauthn-prompt", aJSON.get())); } RefPtr U2FTokenManager::GetTokenManagerImpl() { MOZ_ASSERT(U2FPrefManager::Get()); mozilla::ipc::AssertIsOnBackgroundThread(); if (mTokenManagerImpl) { return mTokenManagerImpl; } if (!gBackgroundThread) { gBackgroundThread = NS_GetCurrentThread(); MOZ_ASSERT(gBackgroundThread, "This should never be null!"); } auto pm = U2FPrefManager::Get(); #ifdef MOZ_WIDGET_ANDROID // On Android, prefer the platform support if enabled. if (pm->GetAndroidFido2Enabled()) { return AndroidWebAuthnTokenManager::GetInstance(); } #endif // Prefer the HW token, even if the softtoken is enabled too. // We currently don't support soft and USB tokens enabled at the // same time as the softtoken would always win the race to register. // We could support it for signing though... if (pm->GetUsbTokenEnabled()) { return new U2FHIDTokenManager(); } if (pm->GetSoftTokenEnabled()) { return new U2FSoftTokenManager(pm->GetSoftTokenCounter()); } // TODO Use WebAuthnRequest to aggregate results from all transports, // once we have multiple HW transport types. return nullptr; } void U2FTokenManager::Register( PWebAuthnTransactionParent* aTransactionParent, const uint64_t& aTransactionId, const WebAuthnMakeCredentialInfo& aTransactionInfo) { MOZ_LOG(gU2FTokenManagerLog, LogLevel::Debug, ("U2FAuthRegister")); AbortOngoingTransaction(); mTransactionParent = aTransactionParent; mTokenManagerImpl = GetTokenManagerImpl(); if (!mTokenManagerImpl) { AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR); return; } mLastTransactionId = aTransactionId; // Determine whether direct attestation was requested. bool directAttestationRequested = false; // On Android, let's always reject direct attestations until we have a // mechanism to solicit user consent, from Bug 1550164 #ifndef MOZ_WIDGET_ANDROID if (aTransactionInfo.Extra().isSome()) { const auto& extra = aTransactionInfo.Extra().ref(); AttestationConveyancePreference attestation = extra.attestationConveyancePreference(); directAttestationRequested = attestation == AttestationConveyancePreference::Direct; } #endif // not MOZ_WIDGET_ANDROID // Start a register request immediately if direct attestation // wasn't requested or the test pref is set. if (!directAttestationRequested || U2FPrefManager::Get()->GetAllowDirectAttestationForTesting()) { // Force "none" attestation when "direct" attestation wasn't requested. DoRegister(aTransactionInfo, !directAttestationRequested); return; } // If the RP request direct attestation, ask the user for permission and // store the transaction info until the user proceeds or cancels. NS_ConvertUTF16toUTF8 origin(aTransactionInfo.Origin()); SendPromptNotification(kRegisterDirectPromptNotifcation, aTransactionId, origin.get(), aTransactionInfo.BrowsingContextId()); MOZ_ASSERT(mPendingRegisterInfo.isNothing()); mPendingRegisterInfo = Some(aTransactionInfo); } void U2FTokenManager::DoRegister(const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation) { mozilla::ipc::AssertIsOnBackgroundThread(); MOZ_ASSERT(mLastTransactionId > 0); // Show a prompt that lets the user cancel the ongoing transaction. NS_ConvertUTF16toUTF8 origin(aInfo.Origin()); SendPromptNotification(kRegisterPromptNotifcation, mLastTransactionId, origin.get(), aInfo.BrowsingContextId()); uint64_t tid = mLastTransactionId; mozilla::TimeStamp startTime = mozilla::TimeStamp::Now(); mTokenManagerImpl->Register(aInfo, aForceNoneAttestation) ->Then( GetCurrentSerialEventTarget(), __func__, [tid, startTime](WebAuthnMakeCredentialResult&& aResult) { U2FTokenManager* mgr = U2FTokenManager::Get(); mgr->MaybeConfirmRegister(tid, aResult); Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, u"U2FRegisterFinish"_ns, 1); Telemetry::AccumulateTimeDelta( Telemetry::WEBAUTHN_CREATE_CREDENTIAL_MS, startTime); }, [tid](nsresult rv) { MOZ_ASSERT(NS_FAILED(rv)); U2FTokenManager* mgr = U2FTokenManager::Get(); mgr->MaybeAbortRegister(tid, rv); Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, u"U2FRegisterAbort"_ns, 1); }) ->Track(mRegisterPromise); } void U2FTokenManager::MaybeConfirmRegister( const uint64_t& aTransactionId, const WebAuthnMakeCredentialResult& aResult) { MOZ_ASSERT(mLastTransactionId == aTransactionId); mRegisterPromise.Complete(); Unused << mTransactionParent->SendConfirmRegister(aTransactionId, aResult); ClearTransaction(); } void U2FTokenManager::MaybeAbortRegister(const uint64_t& aTransactionId, const nsresult& aError) { MOZ_ASSERT(mLastTransactionId == aTransactionId); mRegisterPromise.Complete(); AbortTransaction(aTransactionId, aError); } void U2FTokenManager::Sign(PWebAuthnTransactionParent* aTransactionParent, const uint64_t& aTransactionId, const WebAuthnGetAssertionInfo& aTransactionInfo) { MOZ_LOG(gU2FTokenManagerLog, LogLevel::Debug, ("U2FAuthSign")); AbortOngoingTransaction(); mTransactionParent = aTransactionParent; mTokenManagerImpl = GetTokenManagerImpl(); if (!mTokenManagerImpl) { AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR); return; } // Show a prompt that lets the user cancel the ongoing transaction. NS_ConvertUTF16toUTF8 origin(aTransactionInfo.Origin()); SendPromptNotification(kSignPromptNotifcation, aTransactionId, origin.get(), aTransactionInfo.BrowsingContextId()); uint64_t tid = mLastTransactionId = aTransactionId; mozilla::TimeStamp startTime = mozilla::TimeStamp::Now(); mTokenManagerImpl->Sign(aTransactionInfo) ->Then( GetCurrentSerialEventTarget(), __func__, [tid, startTime](WebAuthnGetAssertionResult&& aResult) { U2FTokenManager* mgr = U2FTokenManager::Get(); mgr->MaybeConfirmSign(tid, aResult); Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, u"U2FSignFinish"_ns, 1); Telemetry::AccumulateTimeDelta(Telemetry::WEBAUTHN_GET_ASSERTION_MS, startTime); }, [tid](nsresult rv) { MOZ_ASSERT(NS_FAILED(rv)); U2FTokenManager* mgr = U2FTokenManager::Get(); mgr->MaybeAbortSign(tid, rv); Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, u"U2FSignAbort"_ns, 1); }) ->Track(mSignPromise); } void U2FTokenManager::MaybeConfirmSign( const uint64_t& aTransactionId, const WebAuthnGetAssertionResult& aResult) { MOZ_ASSERT(mLastTransactionId == aTransactionId); mSignPromise.Complete(); Unused << mTransactionParent->SendConfirmSign(aTransactionId, aResult); ClearTransaction(); } void U2FTokenManager::MaybeAbortSign(const uint64_t& aTransactionId, const nsresult& aError) { MOZ_ASSERT(mLastTransactionId == aTransactionId); mSignPromise.Complete(); AbortTransaction(aTransactionId, aError); } void U2FTokenManager::Cancel(PWebAuthnTransactionParent* aParent, const uint64_t& aTransactionId) { if (mTransactionParent != aParent || mLastTransactionId != aTransactionId) { return; } mTokenManagerImpl->Cancel(); ClearTransaction(); } // nsIU2FTokenManager NS_IMETHODIMP U2FTokenManager::ResumeRegister(uint64_t aTransactionId, bool aForceNoneAttestation) { MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(NS_IsMainThread()); if (!gBackgroundThread) { return NS_ERROR_FAILURE; } nsCOMPtr r(NewRunnableMethod( "U2FTokenManager::RunResumeRegister", this, &U2FTokenManager::RunResumeRegister, aTransactionId, aForceNoneAttestation)); return gBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); } void U2FTokenManager::RunResumeRegister(uint64_t aTransactionId, bool aForceNoneAttestation) { mozilla::ipc::AssertIsOnBackgroundThread(); if (NS_WARN_IF(mPendingRegisterInfo.isNothing())) { return; } if (mLastTransactionId != aTransactionId) { return; } // Resume registration and cleanup. DoRegister(mPendingRegisterInfo.ref(), aForceNoneAttestation); mPendingRegisterInfo.reset(); } NS_IMETHODIMP U2FTokenManager::Cancel(uint64_t aTransactionId) { MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(NS_IsMainThread()); if (!gBackgroundThread) { return NS_ERROR_FAILURE; } nsCOMPtr r( NewRunnableMethod("U2FTokenManager::RunCancel", this, &U2FTokenManager::RunCancel, aTransactionId)); return gBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); } void U2FTokenManager::RunCancel(uint64_t aTransactionId) { mozilla::ipc::AssertIsOnBackgroundThread(); if (mLastTransactionId != aTransactionId) { return; } // Cancel the request. mTokenManagerImpl->Cancel(); // Reject the promise. AbortTransaction(aTransactionId, NS_ERROR_DOM_ABORT_ERR); } } // namespace dom } // namespace mozilla