mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 21:01:08 +00:00
Bug 1391438 - Support FIDO2 for WebAuthn on Android r=snorp,keeler
Support using the Google Play-provided FIDO2 API for Web Authentication. FIDO U2F API support is being handled subsequently in Bug 1550625. This patch uses the privileged APIs and thus will only work on Fennec Nightly, Beta, and Release builds. Differential Revision: https://phabricator.services.mozilla.com/D1148 --HG-- extra : moz-landing-system : lando
This commit is contained in:
parent
cad430cae6
commit
9b769ac3e9
@ -79,6 +79,7 @@ buildscript {
|
||||
if (gradle.mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {
|
||||
ext.google_play_services_version = '15.0.1'
|
||||
ext.google_play_services_cast_version = '16.0.0'
|
||||
ext.google_play_services_fido_version = '17.0.0'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
330
dom/webauthn/AndroidWebAuthnTokenManager.cpp
Normal file
330
dom/webauthn/AndroidWebAuthnTokenManager.cpp
Normal file
@ -0,0 +1,330 @@
|
||||
/* -*- 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/ipc/BackgroundParent.h"
|
||||
#include "mozilla/jni/GeckoBundleUtils.h"
|
||||
#include "mozilla/StaticPtr.h"
|
||||
|
||||
#include "AndroidWebAuthnTokenManager.h"
|
||||
#include "GeneratedJNIWrappers.h"
|
||||
#include "JavaBuiltins.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
static nsIThread* gAndroidPBackgroundThread;
|
||||
|
||||
StaticRefPtr<AndroidWebAuthnTokenManager> gAndroidWebAuthnManager;
|
||||
|
||||
/* static */ AndroidWebAuthnTokenManager*
|
||||
AndroidWebAuthnTokenManager::GetInstance() {
|
||||
if (!gAndroidWebAuthnManager) {
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
gAndroidWebAuthnManager = new AndroidWebAuthnTokenManager();
|
||||
}
|
||||
return gAndroidWebAuthnManager;
|
||||
}
|
||||
|
||||
AndroidWebAuthnTokenManager::AndroidWebAuthnTokenManager() {
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
MOZ_ASSERT(!gAndroidWebAuthnManager);
|
||||
|
||||
gAndroidPBackgroundThread = NS_GetCurrentThread();
|
||||
MOZ_ASSERT(gAndroidPBackgroundThread, "This should never be null!");
|
||||
gAndroidWebAuthnManager = this;
|
||||
}
|
||||
|
||||
void AndroidWebAuthnTokenManager::AssertIsOnOwningThread() const {
|
||||
mozilla::ipc::AssertIsOnBackgroundThread();
|
||||
MOZ_ASSERT(gAndroidPBackgroundThread);
|
||||
#ifdef DEBUG
|
||||
bool current;
|
||||
MOZ_ASSERT(
|
||||
NS_SUCCEEDED(gAndroidPBackgroundThread->IsOnCurrentThread(¤t)));
|
||||
MOZ_ASSERT(current);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AndroidWebAuthnTokenManager::Drop() {
|
||||
AssertIsOnOwningThread();
|
||||
|
||||
ClearPromises();
|
||||
gAndroidWebAuthnManager = nullptr;
|
||||
gAndroidPBackgroundThread = nullptr;
|
||||
}
|
||||
|
||||
RefPtr<U2FRegisterPromise> AndroidWebAuthnTokenManager::Register(
|
||||
const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation) {
|
||||
AssertIsOnOwningThread();
|
||||
|
||||
if (aInfo.Extra().isNothing()) {
|
||||
// Mostly ready for U2F, but requires using a different provider
|
||||
// at the Java-side. Finish out in Bug 1550625
|
||||
return U2FRegisterPromise::CreateAndReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR,
|
||||
__func__);
|
||||
}
|
||||
|
||||
ClearPromises();
|
||||
|
||||
GetMainThreadEventTarget()->Dispatch(NS_NewRunnableFunction(
|
||||
"java::WebAuthnTokenManager::WebAuthnMakeCredential",
|
||||
[aInfo, aForceNoneAttestation]() {
|
||||
AssertIsOnMainThread();
|
||||
|
||||
// Produce the credential exclusion list
|
||||
jni::ObjectArray::LocalRef idList =
|
||||
jni::ObjectArray::New(aInfo.ExcludeList().Length());
|
||||
|
||||
nsTArray<uint8_t> transportBuf;
|
||||
int ix = 0;
|
||||
|
||||
for (const WebAuthnScopedCredential& cred : aInfo.ExcludeList()) {
|
||||
jni::ByteBuffer::LocalRef id = jni::ByteBuffer::New(
|
||||
const_cast<void*>(static_cast<const void*>(cred.id().Elements())),
|
||||
cred.id().Length());
|
||||
|
||||
idList->SetElement(ix, id);
|
||||
transportBuf.AppendElement(cred.transports());
|
||||
|
||||
ix += 1;
|
||||
}
|
||||
|
||||
jni::ByteBuffer::LocalRef transportList = jni::ByteBuffer::New(
|
||||
const_cast<void*>(
|
||||
static_cast<const void*>(transportBuf.Elements())),
|
||||
transportBuf.Length());
|
||||
|
||||
const nsTArray<uint8_t>& challBuf = aInfo.Challenge();
|
||||
jni::ByteBuffer::LocalRef challenge = jni::ByteBuffer::New(
|
||||
const_cast<void*>(static_cast<const void*>(challBuf.Elements())),
|
||||
challBuf.Length());
|
||||
|
||||
nsTArray<uint8_t> uidBuf;
|
||||
|
||||
// Get authenticator selection criteria
|
||||
GECKOBUNDLE_START(authSelBundle);
|
||||
GECKOBUNDLE_START(extensionsBundle);
|
||||
GECKOBUNDLE_START(identifierBundle);
|
||||
|
||||
if (aInfo.Extra().isSome()) {
|
||||
const auto& extra = aInfo.Extra().ref();
|
||||
const auto& rp = extra.Rp();
|
||||
const auto& user = extra.User();
|
||||
|
||||
// If we have extra data, then this is WebAuthn, not U2F
|
||||
GECKOBUNDLE_PUT(identifierBundle, "isWebAuthn",
|
||||
java::sdk::Integer::ValueOf(1));
|
||||
|
||||
// Get the attestation preference and override if the user asked
|
||||
AttestationConveyancePreference attestation =
|
||||
extra.attestationConveyancePreference();
|
||||
|
||||
if (aForceNoneAttestation) {
|
||||
// Add UI support to trigger this, bug 1550164
|
||||
attestation = AttestationConveyancePreference::None;
|
||||
}
|
||||
|
||||
if (static_cast<uint32_t>(attestation) <
|
||||
static_cast<uint32_t>(
|
||||
AttestationConveyancePreference::EndGuard_)) {
|
||||
// Protect the direct array reference below. Note this is still a
|
||||
// ContiguousEnumValidator type.
|
||||
nsString attestPref;
|
||||
const EnumEntry& attPrefEntry =
|
||||
AttestationConveyancePreferenceValues::strings
|
||||
[static_cast<uint32_t>(attestation)];
|
||||
attestPref.AssignASCII(attPrefEntry.value, attPrefEntry.length);
|
||||
GECKOBUNDLE_PUT(authSelBundle, "attestationPreference",
|
||||
jni::StringParam(attestPref));
|
||||
}
|
||||
|
||||
const WebAuthnAuthenticatorSelection& sel =
|
||||
extra.AuthenticatorSelection();
|
||||
if (sel.requireResidentKey()) {
|
||||
GECKOBUNDLE_PUT(authSelBundle, "requireResidentKey",
|
||||
java::sdk::Integer::ValueOf(1));
|
||||
}
|
||||
|
||||
if (sel.userVerificationRequirement() ==
|
||||
UserVerificationRequirement::Required) {
|
||||
GECKOBUNDLE_PUT(authSelBundle, "requireUserVerification",
|
||||
java::sdk::Integer::ValueOf(1));
|
||||
}
|
||||
|
||||
if (sel.authenticatorAttachment().isSome()) {
|
||||
const AuthenticatorAttachment authenticatorAttachment =
|
||||
sel.authenticatorAttachment().value();
|
||||
if (authenticatorAttachment == AuthenticatorAttachment::Platform) {
|
||||
GECKOBUNDLE_PUT(authSelBundle, "requirePlatformAttachment",
|
||||
java::sdk::Integer::ValueOf(1));
|
||||
}
|
||||
}
|
||||
|
||||
// Get extensions
|
||||
for (const WebAuthnExtension& ext : extra.Extensions()) {
|
||||
if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) {
|
||||
GECKOBUNDLE_PUT(
|
||||
extensionsBundle, "fidoAppId",
|
||||
jni::StringParam(
|
||||
ext.get_WebAuthnExtensionAppId().appIdentifier()));
|
||||
}
|
||||
}
|
||||
|
||||
uidBuf.Assign(user.Id());
|
||||
|
||||
GECKOBUNDLE_PUT(identifierBundle, "rpName",
|
||||
jni::StringParam(rp.Name()));
|
||||
GECKOBUNDLE_PUT(identifierBundle, "rpIcon",
|
||||
jni::StringParam(rp.Icon()));
|
||||
GECKOBUNDLE_PUT(identifierBundle, "userName",
|
||||
jni::StringParam(user.Name()));
|
||||
GECKOBUNDLE_PUT(identifierBundle, "userIcon",
|
||||
jni::StringParam(user.Icon()));
|
||||
GECKOBUNDLE_PUT(identifierBundle, "userDisplayName",
|
||||
jni::StringParam(user.DisplayName()));
|
||||
}
|
||||
|
||||
GECKOBUNDLE_FINISH(authSelBundle);
|
||||
GECKOBUNDLE_FINISH(extensionsBundle);
|
||||
GECKOBUNDLE_FINISH(identifierBundle);
|
||||
|
||||
// For non-WebAuthn cases, uidBuf is empty (and unused)
|
||||
jni::ByteBuffer::LocalRef uid = jni::ByteBuffer::New(
|
||||
const_cast<void*>(static_cast<const void*>(uidBuf.Elements())),
|
||||
uidBuf.Length());
|
||||
|
||||
java::WebAuthnTokenManager::WebAuthnMakeCredential(
|
||||
aInfo.RpId(), identifierBundle, uid, challenge, aInfo.TimeoutMS(),
|
||||
aInfo.Origin(), idList, transportList, authSelBundle,
|
||||
extensionsBundle);
|
||||
}));
|
||||
|
||||
return mRegisterPromise.Ensure(__func__);
|
||||
}
|
||||
|
||||
void AndroidWebAuthnTokenManager::HandleRegisterResult(
|
||||
const AndroidWebAuthnResult& aResult) {
|
||||
// This is likely running on the main thread, so we'll always dispatch to the
|
||||
// background for state updates.
|
||||
if (aResult.IsError()) {
|
||||
nsresult aError = aResult.GetError();
|
||||
|
||||
gAndroidPBackgroundThread->Dispatch(NS_NewRunnableFunction(
|
||||
"AndroidWebAuthnTokenManager::RegisterAbort",
|
||||
[self = RefPtr<AndroidWebAuthnTokenManager>(this), aError]() {
|
||||
self->mRegisterPromise.RejectIfExists(aError, __func__);
|
||||
}));
|
||||
} else {
|
||||
gAndroidPBackgroundThread->Dispatch(NS_NewRunnableFunction(
|
||||
"AndroidWebAuthnTokenManager::RegisterComplete",
|
||||
[self = RefPtr<AndroidWebAuthnTokenManager>(this), aResult]() {
|
||||
CryptoBuffer emptyBuffer;
|
||||
WebAuthnMakeCredentialResult result(aResult.mClientDataJSON,
|
||||
aResult.mAttObj,
|
||||
aResult.mKeyHandle, emptyBuffer);
|
||||
self->mRegisterPromise.Resolve(std::move(result), __func__);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
RefPtr<U2FSignPromise> AndroidWebAuthnTokenManager::Sign(
|
||||
const WebAuthnGetAssertionInfo& aInfo) {
|
||||
AssertIsOnOwningThread();
|
||||
|
||||
ClearPromises();
|
||||
|
||||
GetMainThreadEventTarget()->Dispatch(NS_NewRunnableFunction(
|
||||
"java::WebAuthnTokenManager::WebAuthnGetAssertion", [aInfo]() {
|
||||
AssertIsOnMainThread();
|
||||
|
||||
jni::ObjectArray::LocalRef idList =
|
||||
jni::ObjectArray::New(aInfo.AllowList().Length());
|
||||
|
||||
nsTArray<uint8_t> transportBuf;
|
||||
|
||||
int ix = 0;
|
||||
for (const WebAuthnScopedCredential& cred : aInfo.AllowList()) {
|
||||
jni::ByteBuffer::LocalRef id = jni::ByteBuffer::New(
|
||||
const_cast<void*>(static_cast<const void*>(cred.id().Elements())),
|
||||
cred.id().Length());
|
||||
|
||||
idList->SetElement(ix, id);
|
||||
transportBuf.AppendElement(cred.transports());
|
||||
|
||||
ix += 1;
|
||||
}
|
||||
|
||||
jni::ByteBuffer::LocalRef transportList = jni::ByteBuffer::New(
|
||||
const_cast<void*>(
|
||||
static_cast<const void*>(transportBuf.Elements())),
|
||||
transportBuf.Length());
|
||||
|
||||
const nsTArray<uint8_t>& challBuf = aInfo.Challenge();
|
||||
jni::ByteBuffer::LocalRef challenge = jni::ByteBuffer::New(
|
||||
const_cast<void*>(static_cast<const void*>(challBuf.Elements())),
|
||||
challBuf.Length());
|
||||
|
||||
// Get extensions
|
||||
GECKOBUNDLE_START(extensionsBundle);
|
||||
if (aInfo.Extra().isSome()) {
|
||||
const auto& extra = aInfo.Extra().ref();
|
||||
|
||||
for (const WebAuthnExtension& ext : extra.Extensions()) {
|
||||
if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) {
|
||||
GECKOBUNDLE_PUT(
|
||||
extensionsBundle, "fidoAppId",
|
||||
jni::StringParam(
|
||||
ext.get_WebAuthnExtensionAppId().appIdentifier()));
|
||||
}
|
||||
}
|
||||
}
|
||||
GECKOBUNDLE_FINISH(extensionsBundle);
|
||||
|
||||
java::WebAuthnTokenManager::WebAuthnGetAssertion(
|
||||
aInfo.RpId(), challenge, aInfo.TimeoutMS(), aInfo.Origin(), idList,
|
||||
transportList, extensionsBundle);
|
||||
}));
|
||||
|
||||
return mSignPromise.Ensure(__func__);
|
||||
}
|
||||
|
||||
void AndroidWebAuthnTokenManager::HandleSignResult(
|
||||
const AndroidWebAuthnResult& aResult) {
|
||||
// This is likely running on the main thread, so we'll always dispatch to the
|
||||
// background for state updates.
|
||||
if (aResult.IsError()) {
|
||||
nsresult aError = aResult.GetError();
|
||||
|
||||
gAndroidPBackgroundThread->Dispatch(NS_NewRunnableFunction(
|
||||
"AndroidWebAuthnTokenManager::SignAbort",
|
||||
[self = RefPtr<AndroidWebAuthnTokenManager>(this), aError]() {
|
||||
self->mSignPromise.RejectIfExists(aError, __func__);
|
||||
}));
|
||||
} else {
|
||||
gAndroidPBackgroundThread->Dispatch(NS_NewRunnableFunction(
|
||||
"AndroidWebAuthnTokenManager::SignComplete",
|
||||
[self = RefPtr<AndroidWebAuthnTokenManager>(this), aResult]() {
|
||||
CryptoBuffer emptyBuffer;
|
||||
|
||||
nsTArray<WebAuthnExtensionResult> emptyExtensions;
|
||||
WebAuthnGetAssertionResult result(
|
||||
aResult.mClientDataJSON, aResult.mKeyHandle, aResult.mSignature,
|
||||
aResult.mAuthData, emptyExtensions, emptyBuffer,
|
||||
aResult.mUserHandle);
|
||||
self->mSignPromise.Resolve(std::move(result), __func__);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void AndroidWebAuthnTokenManager::Cancel() {
|
||||
AssertIsOnOwningThread();
|
||||
|
||||
ClearPromises();
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
129
dom/webauthn/AndroidWebAuthnTokenManager.h
Normal file
129
dom/webauthn/AndroidWebAuthnTokenManager.h
Normal file
@ -0,0 +1,129 @@
|
||||
/* -*- 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/. */
|
||||
|
||||
#ifndef mozilla_dom_AndroidWebAuthnTokenManager_h
|
||||
#define mozilla_dom_AndroidWebAuthnTokenManager_h
|
||||
|
||||
#include "mozilla/dom/CryptoBuffer.h"
|
||||
#include "mozilla/dom/U2FTokenTransport.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
// Collected from
|
||||
// https://developers.google.com/android/reference/com/google/android/gms/fido/fido2/api/common/ErrorCode
|
||||
NS_NAMED_LITERAL_STRING(kSecurityError, "SECURITY_ERR");
|
||||
NS_NAMED_LITERAL_STRING(kConstraintError, "CONSTRAINT_ERR");
|
||||
NS_NAMED_LITERAL_STRING(kNotSupportedError, "NOT_SUPPORTED_ERR");
|
||||
NS_NAMED_LITERAL_STRING(kInvalidStateError, "INVALID_STATE_ERR");
|
||||
NS_NAMED_LITERAL_STRING(kNotAllowedError, "NOT_ALLOWED_ERR");
|
||||
NS_NAMED_LITERAL_STRING(kAbortError, "ABORT_ERR");
|
||||
NS_NAMED_LITERAL_STRING(kEncodingError, "ENCODING_ERR");
|
||||
NS_NAMED_LITERAL_STRING(kDataError, "DATA_ERR");
|
||||
NS_NAMED_LITERAL_STRING(kTimeoutError, "TIMEOUT_ERR");
|
||||
NS_NAMED_LITERAL_STRING(kNetworkError, "NETWORK_ERR");
|
||||
NS_NAMED_LITERAL_STRING(kUnknownError, "UNKNOWN_ERR");
|
||||
|
||||
class AndroidWebAuthnResult {
|
||||
public:
|
||||
explicit AndroidWebAuthnResult(const nsAString& aErrorCode)
|
||||
: mErrorCode(aErrorCode) {}
|
||||
|
||||
explicit AndroidWebAuthnResult() {}
|
||||
|
||||
bool IsError() const { return NS_FAILED(GetError()); }
|
||||
|
||||
nsresult GetError() const {
|
||||
if (mErrorCode.IsEmpty()) {
|
||||
return NS_OK;
|
||||
} else if (mErrorCode.Equals(kSecurityError)) {
|
||||
return NS_ERROR_DOM_SECURITY_ERR;
|
||||
} else if (mErrorCode.Equals(kConstraintError)) {
|
||||
// TODO: The message is right, but it's not about indexeddb.
|
||||
// See https://heycam.github.io/webidl/#constrainterror
|
||||
return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR;
|
||||
} else if (mErrorCode.Equals(kNotSupportedError)) {
|
||||
return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
|
||||
} else if (mErrorCode.Equals(kInvalidStateError)) {
|
||||
return NS_ERROR_DOM_INVALID_STATE_ERR;
|
||||
} else if (mErrorCode.Equals(kNotAllowedError)) {
|
||||
return NS_ERROR_DOM_NOT_ALLOWED_ERR;
|
||||
} else if (mErrorCode.Equals(kEncodingError)) {
|
||||
return NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR;
|
||||
} else if (mErrorCode.Equals(kDataError)) {
|
||||
return NS_ERROR_DOM_DATA_ERR;
|
||||
} else if (mErrorCode.Equals(kTimeoutError)) {
|
||||
return NS_ERROR_DOM_TIMEOUT_ERR;
|
||||
} else if (mErrorCode.Equals(kNetworkError)) {
|
||||
return NS_ERROR_DOM_NETWORK_ERR;
|
||||
} else if (mErrorCode.Equals(kAbortError)) {
|
||||
return NS_ERROR_DOM_ABORT_ERR;
|
||||
} else if (mErrorCode.Equals(kUnknownError)) {
|
||||
return NS_ERROR_DOM_UNKNOWN_ERR;
|
||||
} else {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "Gecko",
|
||||
"RegisterAbort unknown code: %s",
|
||||
NS_ConvertUTF16toUTF8(mErrorCode).get());
|
||||
return NS_ERROR_DOM_UNKNOWN_ERR;
|
||||
}
|
||||
}
|
||||
|
||||
// Attestation-only
|
||||
CryptoBuffer mAttObj;
|
||||
|
||||
// Attestations and assertions
|
||||
CryptoBuffer mKeyHandle;
|
||||
nsCString mClientDataJSON;
|
||||
|
||||
// Assertions-only
|
||||
CryptoBuffer mAuthData;
|
||||
CryptoBuffer mSignature;
|
||||
CryptoBuffer mUserHandle;
|
||||
|
||||
private:
|
||||
const nsString mErrorCode;
|
||||
};
|
||||
|
||||
/*
|
||||
* WebAuthnAndroidTokenManager is a token implementation communicating with
|
||||
* Android Fido2 APIs.
|
||||
*/
|
||||
class AndroidWebAuthnTokenManager final : public U2FTokenTransport {
|
||||
public:
|
||||
explicit AndroidWebAuthnTokenManager();
|
||||
~AndroidWebAuthnTokenManager() {}
|
||||
|
||||
RefPtr<U2FRegisterPromise> Register(const WebAuthnMakeCredentialInfo& aInfo,
|
||||
bool aForceNoneAttestation) override;
|
||||
|
||||
RefPtr<U2FSignPromise> Sign(const WebAuthnGetAssertionInfo& aInfo) override;
|
||||
|
||||
void Cancel() override;
|
||||
|
||||
void Drop() override;
|
||||
|
||||
void HandleRegisterResult(const AndroidWebAuthnResult& aResult);
|
||||
|
||||
void HandleSignResult(const AndroidWebAuthnResult& aResult);
|
||||
|
||||
static AndroidWebAuthnTokenManager* GetInstance();
|
||||
|
||||
private:
|
||||
void ClearPromises() {
|
||||
mRegisterPromise.RejectIfExists(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
|
||||
mSignPromise.RejectIfExists(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
|
||||
}
|
||||
|
||||
void AssertIsOnOwningThread() const;
|
||||
|
||||
MozPromiseHolder<U2FRegisterPromise> mRegisterPromise;
|
||||
MozPromiseHolder<U2FSignPromise> mSignPromise;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_dom_AndroidWebAuthnTokenManager_h
|
@ -16,6 +16,10 @@
|
||||
#include "mozilla/Unused.h"
|
||||
#include "nsTextFormatter.h"
|
||||
|
||||
#ifdef 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!
|
||||
@ -26,7 +30,8 @@
|
||||
"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 {
|
||||
|
||||
@ -70,6 +75,8 @@ class U2FPrefManager final : public nsIObserver {
|
||||
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);
|
||||
@ -94,6 +101,11 @@ class U2FPrefManager final : public nsIObserver {
|
||||
return mUsbTokenEnabled;
|
||||
}
|
||||
|
||||
bool GetAndroidFido2Enabled() {
|
||||
MutexAutoLock lock(mPrefMutex);
|
||||
return mAndroidFido2Enabled;
|
||||
}
|
||||
|
||||
bool GetAllowDirectAttestationForTesting() {
|
||||
MutexAutoLock lock(mPrefMutex);
|
||||
return mAllowDirectAttestation;
|
||||
@ -113,6 +125,8 @@ class U2FPrefManager final : public nsIObserver {
|
||||
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);
|
||||
}
|
||||
@ -121,6 +135,7 @@ class U2FPrefManager final : public nsIObserver {
|
||||
bool mSoftTokenEnabled;
|
||||
int mSoftTokenCounter;
|
||||
bool mUsbTokenEnabled;
|
||||
bool mAndroidFido2Enabled;
|
||||
bool mAllowDirectAttestation;
|
||||
};
|
||||
|
||||
@ -253,6 +268,13 @@ RefPtr<U2FTokenTransport> U2FTokenManager::GetTokenManagerImpl() {
|
||||
|
||||
auto pm = U2FPrefManager::Get();
|
||||
|
||||
#ifdef 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.
|
||||
|
@ -63,6 +63,14 @@ LOCAL_INCLUDES += [
|
||||
'/security/manager/ssl',
|
||||
]
|
||||
|
||||
if CONFIG['OS_TARGET'] == 'Android':
|
||||
EXPORTS.mozilla.dom += [
|
||||
'AndroidWebAuthnTokenManager.h',
|
||||
]
|
||||
UNIFIED_SOURCES += [
|
||||
'AndroidWebAuthnTokenManager.cpp',
|
||||
]
|
||||
|
||||
if CONFIG['OS_ARCH'] == 'WINNT':
|
||||
OS_LIBS += [
|
||||
'hid',
|
||||
|
@ -148,6 +148,10 @@ android {
|
||||
exclude 'org/mozilla/gecko/gcm/**/*.java'
|
||||
exclude 'org/mozilla/gecko/push/**/*.java'
|
||||
}
|
||||
|
||||
if (!mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {
|
||||
exclude 'org/mozilla/gecko/util/WebAuthnUtils.java'
|
||||
}
|
||||
}
|
||||
|
||||
res {
|
||||
@ -242,6 +246,10 @@ dependencies {
|
||||
implementation "com.google.android.gms:play-services-gcm:$google_play_services_version"
|
||||
}
|
||||
|
||||
if (mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {
|
||||
implementation "com.google.android.gms:play-services-fido:$google_play_services_fido_version"
|
||||
}
|
||||
|
||||
// Include LeakCanary in local builds, but not in official builds.
|
||||
if (mozconfig.substs.MOZILLA_OFFICIAL) {
|
||||
implementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
|
||||
@ -471,7 +479,8 @@ configurations.all {
|
||||
resolutionStrategy {
|
||||
eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.group == 'com.google.android.gms'
|
||||
&& details.requested.name != 'play-services-cast') {
|
||||
&& details.requested.name != 'play-services-cast'
|
||||
&& details.requested.name != 'play-services-fido') {
|
||||
details.useVersion "$google_play_services_version"
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,13 @@ public class ActivityHandlerHelper {
|
||||
private static final String LOGTAG = "GeckoActivityHandlerHelper";
|
||||
private static final ActivityResultHandlerMap mActivityResultHandlerMap = new ActivityResultHandlerMap();
|
||||
|
||||
private static int makeRequestCode(ActivityResultHandler aHandler) {
|
||||
/**
|
||||
* If you need to launch a PendingIntent from a Gecko activity but still collect
|
||||
* the result, then you can use registerActivityHandler() to observe the
|
||||
* result when it comes, as long as the Gecko activity is calling
|
||||
* handleActivityResult(), which they do.
|
||||
*/
|
||||
public static int registerActivityHandler(ActivityResultHandler aHandler) {
|
||||
return mActivityResultHandlerMap.put(aHandler);
|
||||
}
|
||||
|
||||
@ -45,7 +51,6 @@ public class ActivityHandlerHelper {
|
||||
activity.startActivityForResult(intent, mActivityResultHandlerMap.put(activityResultHandler));
|
||||
}
|
||||
|
||||
|
||||
public static boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
ActivityResultHandler handler = mActivityResultHandlerMap.getAndRemove(requestCode);
|
||||
if (handler != null) {
|
||||
|
@ -0,0 +1,404 @@
|
||||
/* -*- Mode: Java; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* 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/. */
|
||||
|
||||
package org.mozilla.gecko.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.mozilla.gecko.ActivityHandlerHelper;
|
||||
import org.mozilla.gecko.WebAuthnTokenManager;
|
||||
import org.mozilla.gecko.GeckoActivityMonitor;
|
||||
import org.mozilla.gecko.util.ActivityResultHandler;
|
||||
import org.mozilla.gecko.util.GeckoBundle;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentSender;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.google.android.gms.fido.Fido;
|
||||
import com.google.android.gms.fido.common.Transport;
|
||||
import com.google.android.gms.fido.fido2.Fido2PendingIntent;
|
||||
import com.google.android.gms.fido.fido2.Fido2PrivilegedApiClient;
|
||||
import com.google.android.gms.fido.fido2.api.common.Algorithm;
|
||||
import com.google.android.gms.fido.fido2.api.common.Attachment;
|
||||
import com.google.android.gms.fido.fido2.api.common.AttestationConveyancePreference;
|
||||
import com.google.android.gms.fido.fido2.api.common.AuthenticationExtensions;
|
||||
import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse;
|
||||
import com.google.android.gms.fido.fido2.api.common.AuthenticatorAttestationResponse;
|
||||
import com.google.android.gms.fido.fido2.api.common.AuthenticatorErrorResponse;
|
||||
import com.google.android.gms.fido.fido2.api.common.AuthenticatorSelectionCriteria;
|
||||
import com.google.android.gms.fido.fido2.api.common.BrowserPublicKeyCredentialCreationOptions;
|
||||
import com.google.android.gms.fido.fido2.api.common.BrowserPublicKeyCredentialRequestOptions;
|
||||
import com.google.android.gms.fido.fido2.api.common.EC2Algorithm;
|
||||
import com.google.android.gms.fido.fido2.api.common.FidoAppIdExtension;
|
||||
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions;
|
||||
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialDescriptor;
|
||||
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialParameters;
|
||||
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions;
|
||||
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRpEntity;
|
||||
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialType;
|
||||
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity;
|
||||
import com.google.android.gms.fido.fido2.api.common.RSAAlgorithm;
|
||||
import com.google.android.gms.tasks.Task;
|
||||
import com.google.android.gms.tasks.OnSuccessListener;
|
||||
import com.google.android.gms.tasks.OnFailureListener;
|
||||
|
||||
public class WebAuthnUtils
|
||||
{
|
||||
private static final String LOG_TAG = "WebAuthnUtils";
|
||||
|
||||
// from u2fhid-capi.h
|
||||
private static final byte AUTHENTICATOR_TRANSPORT_USB = 1;
|
||||
private static final byte AUTHENTICATOR_TRANSPORT_NFC = 2;
|
||||
private static final byte AUTHENTICATOR_TRANSPORT_BLE = 4;
|
||||
|
||||
private static List<Transport> getTransportsForByte(final byte transports) {
|
||||
ArrayList<Transport> result = new ArrayList<Transport>();
|
||||
if ((transports & AUTHENTICATOR_TRANSPORT_USB) == AUTHENTICATOR_TRANSPORT_USB) {
|
||||
result.add(Transport.USB);
|
||||
}
|
||||
if ((transports & AUTHENTICATOR_TRANSPORT_NFC) == AUTHENTICATOR_TRANSPORT_NFC) {
|
||||
result.add(Transport.NFC);
|
||||
}
|
||||
if ((transports & AUTHENTICATOR_TRANSPORT_BLE) == AUTHENTICATOR_TRANSPORT_BLE) {
|
||||
result.add(Transport.BLUETOOTH_LOW_ENERGY);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void makeCredential(final String rpId, final GeckoBundle identifiers,
|
||||
final byte[] userId, final byte[] challenge,
|
||||
final long timeoutMs, final String originStr,
|
||||
final WebAuthnTokenManager.WebAuthnPublicCredential[] excludeList,
|
||||
final GeckoBundle authenticatorSelection,
|
||||
final GeckoBundle extensions,
|
||||
WebAuthnTokenManager.WebAuthnMakeCredentialResponse handler) {
|
||||
final Activity currentActivity =
|
||||
GeckoActivityMonitor.getInstance().getCurrentActivity();
|
||||
|
||||
if (currentActivity == null) {
|
||||
handler.onFailure("UNKNOWN_ERR");
|
||||
return;
|
||||
}
|
||||
|
||||
PublicKeyCredentialCreationOptions.Builder requestBuilder =
|
||||
new PublicKeyCredentialCreationOptions.Builder();
|
||||
|
||||
Fido2PrivilegedApiClient fidoClient = // Only works in released builds
|
||||
Fido.getFido2PrivilegedApiClient(currentActivity.getApplicationContext());
|
||||
|
||||
List<PublicKeyCredentialParameters> params =
|
||||
new ArrayList<PublicKeyCredentialParameters>();
|
||||
|
||||
// WebAuthn suuports more algorithms
|
||||
for (Algorithm algo : new Algorithm[]{
|
||||
EC2Algorithm.ES256, EC2Algorithm.ES384, EC2Algorithm.ES512,
|
||||
EC2Algorithm.ED256, /* no ED384 */ EC2Algorithm.ED512,
|
||||
RSAAlgorithm.PS256, RSAAlgorithm.PS384, RSAAlgorithm.PS512,
|
||||
RSAAlgorithm.RS256, RSAAlgorithm.RS384, RSAAlgorithm.RS512
|
||||
}) {
|
||||
params.add(new PublicKeyCredentialParameters(
|
||||
PublicKeyCredentialType.PUBLIC_KEY.toString(),
|
||||
algo.getAlgoValue()));
|
||||
}
|
||||
|
||||
PublicKeyCredentialUserEntity user =
|
||||
new PublicKeyCredentialUserEntity(userId,
|
||||
identifiers.getString("userName", ""),
|
||||
identifiers.getString("userIcon", ""),
|
||||
identifiers.getString("userDisplayName", ""));
|
||||
|
||||
AttestationConveyancePreference pref =
|
||||
AttestationConveyancePreference.NONE;
|
||||
String attestationPreference =
|
||||
authenticatorSelection.getString("attestationPreference", "NONE");
|
||||
if (attestationPreference.equalsIgnoreCase(
|
||||
AttestationConveyancePreference.DIRECT.name())) {
|
||||
pref = AttestationConveyancePreference.DIRECT;
|
||||
} else if (attestationPreference.equalsIgnoreCase(
|
||||
AttestationConveyancePreference.INDIRECT.name())) {
|
||||
pref = AttestationConveyancePreference.INDIRECT;
|
||||
}
|
||||
|
||||
AuthenticatorSelectionCriteria.Builder selBuild =
|
||||
new AuthenticatorSelectionCriteria.Builder();
|
||||
if (extensions.containsKey("requirePlatformAttachment")) {
|
||||
if (authenticatorSelection.getInt("requirePlatformAttachment") == 1) {
|
||||
selBuild.setAttachment(Attachment.PLATFORM);
|
||||
}
|
||||
}
|
||||
AuthenticatorSelectionCriteria sel = selBuild.build();
|
||||
|
||||
AuthenticationExtensions.Builder extBuilder =
|
||||
new AuthenticationExtensions.Builder();
|
||||
if (extensions.containsKey("fidoAppId")) {
|
||||
extBuilder.setFido2Extension(
|
||||
new FidoAppIdExtension(extensions.getString("fidoAppId")));
|
||||
}
|
||||
AuthenticationExtensions ext = extBuilder.build();
|
||||
|
||||
// requireResidentKey andrequireUserVerification are not yet
|
||||
// consumed by Android's API
|
||||
|
||||
List<PublicKeyCredentialDescriptor> excludedList =
|
||||
new ArrayList<PublicKeyCredentialDescriptor>();
|
||||
for (WebAuthnTokenManager.WebAuthnPublicCredential cred : excludeList) {
|
||||
excludedList.add(
|
||||
new PublicKeyCredentialDescriptor(
|
||||
PublicKeyCredentialType.PUBLIC_KEY.toString(),
|
||||
cred.mId,
|
||||
getTransportsForByte(cred.mTransports)));
|
||||
}
|
||||
|
||||
PublicKeyCredentialRpEntity rp =
|
||||
new PublicKeyCredentialRpEntity(rpId,
|
||||
identifiers.getString("rpName", ""),
|
||||
identifiers.getString("rpIcon", ""));
|
||||
|
||||
PublicKeyCredentialCreationOptions requestOptions =
|
||||
requestBuilder
|
||||
.setUser(user)
|
||||
.setAttestationConveyancePreference(pref)
|
||||
.setAuthenticatorSelection(sel)
|
||||
.setAuthenticationExtensions(ext)
|
||||
.setChallenge(challenge)
|
||||
.setRp(rp)
|
||||
.setParameters(params)
|
||||
.setTimeoutSeconds(timeoutMs / 1000.0)
|
||||
.setExcludeList(excludedList)
|
||||
.build();
|
||||
|
||||
Uri origin = Uri.parse(originStr);
|
||||
|
||||
BrowserPublicKeyCredentialCreationOptions browserOptions =
|
||||
new BrowserPublicKeyCredentialCreationOptions.Builder()
|
||||
.setPublicKeyCredentialCreationOptions(requestOptions)
|
||||
.setOrigin(origin)
|
||||
.build();
|
||||
|
||||
Task<Fido2PendingIntent> result = fidoClient.getRegisterIntent(browserOptions);
|
||||
|
||||
result.addOnSuccessListener(new OnSuccessListener<Fido2PendingIntent>() {
|
||||
@Override
|
||||
public void onSuccess(Fido2PendingIntent pendingIntent) {
|
||||
if (pendingIntent.hasPendingIntent()) {
|
||||
final WebAuthnMakeCredentialResult resultHandler =
|
||||
new WebAuthnMakeCredentialResult(handler);
|
||||
|
||||
try {
|
||||
pendingIntent.launchPendingIntent(currentActivity,
|
||||
ActivityHandlerHelper.registerActivityHandler(resultHandler));
|
||||
} catch (IntentSender.SendIntentException e) {
|
||||
handler.onFailure("UNKNOWN_ERR");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
result.addOnFailureListener(new OnFailureListener() {
|
||||
@Override
|
||||
public void onFailure(Exception e) {
|
||||
Log.w(LOG_TAG, "onFailure=" + e);
|
||||
e.printStackTrace();
|
||||
handler.onFailure("UNKNOWN_ERR");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class WebAuthnMakeCredentialResult implements ActivityResultHandler {
|
||||
private WebAuthnTokenManager.WebAuthnMakeCredentialResponse mHandler;
|
||||
|
||||
WebAuthnMakeCredentialResult(WebAuthnTokenManager.WebAuthnMakeCredentialResponse handler) {
|
||||
this.mHandler = handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int resultCode, final Intent data) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (data.hasExtra(Fido.FIDO2_KEY_ERROR_EXTRA)) {
|
||||
byte[] errData = data.getByteArrayExtra(Fido.FIDO2_KEY_ERROR_EXTRA);
|
||||
AuthenticatorErrorResponse responseData =
|
||||
AuthenticatorErrorResponse.deserializeFromBytes(errData);
|
||||
|
||||
Log.e(LOG_TAG, "errorCode.name: " + responseData.getErrorCode());
|
||||
Log.e(LOG_TAG, "errorMessage: " + responseData.getErrorMessage());
|
||||
|
||||
mHandler.onFailure(responseData.getErrorCode().name());
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.hasExtra(Fido.FIDO2_KEY_RESPONSE_EXTRA)) {
|
||||
byte[] rspData = data.getByteArrayExtra(Fido.FIDO2_KEY_RESPONSE_EXTRA);
|
||||
AuthenticatorAttestationResponse responseData =
|
||||
AuthenticatorAttestationResponse.deserializeFromBytes(rspData);
|
||||
|
||||
Log.e(LOG_TAG, "key handle: " + Base64.encodeToString(responseData.getKeyHandle(), Base64.DEFAULT));
|
||||
Log.e(LOG_TAG, "clientDataJSON: " + Base64.encodeToString(responseData.getClientDataJSON(), Base64.DEFAULT));
|
||||
Log.e(LOG_TAG, "attestation Object: " + Base64.encodeToString(responseData.getAttestationObject(), Base64.DEFAULT));
|
||||
|
||||
mHandler.onSuccess(
|
||||
responseData.getClientDataJSON(),
|
||||
responseData.getKeyHandle(),
|
||||
responseData.getAttestationObject());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
Log.w(LOG_TAG, "RESULT_CANCELED" + resultCode);
|
||||
mHandler.onFailure("ABORT_ERR");
|
||||
return;
|
||||
}
|
||||
|
||||
mHandler.onFailure("UNKNOWN_ERR");
|
||||
}
|
||||
}
|
||||
|
||||
public static void getAssertion(final String rpId, final byte[] challenge,
|
||||
final long timeoutMs, final String originStr,
|
||||
final WebAuthnTokenManager.WebAuthnPublicCredential[] allowList,
|
||||
final GeckoBundle extensions,
|
||||
WebAuthnTokenManager.WebAuthnGetAssertionResponse handler) {
|
||||
final Activity currentActivity =
|
||||
GeckoActivityMonitor.getInstance().getCurrentActivity();
|
||||
|
||||
if (currentActivity == null) {
|
||||
handler.onFailure("UNKNOWN_ERR");
|
||||
return;
|
||||
}
|
||||
|
||||
List<PublicKeyCredentialDescriptor> allowedList =
|
||||
new ArrayList<PublicKeyCredentialDescriptor>();
|
||||
for (WebAuthnTokenManager.WebAuthnPublicCredential cred : allowList) {
|
||||
allowedList.add(
|
||||
new PublicKeyCredentialDescriptor(
|
||||
PublicKeyCredentialType.PUBLIC_KEY.toString(),
|
||||
cred.mId,
|
||||
getTransportsForByte(cred.mTransports)));
|
||||
}
|
||||
|
||||
Fido2PrivilegedApiClient fidoClient = // Only works in released builds
|
||||
Fido.getFido2PrivilegedApiClient(currentActivity.getApplicationContext());
|
||||
|
||||
AuthenticationExtensions.Builder extBuilder =
|
||||
new AuthenticationExtensions.Builder();
|
||||
if (extensions.containsKey("fidoAppId")) {
|
||||
extBuilder.setFido2Extension(
|
||||
new FidoAppIdExtension(extensions.getString("fidoAppId")));
|
||||
}
|
||||
AuthenticationExtensions ext = extBuilder.build();
|
||||
|
||||
PublicKeyCredentialRequestOptions requestOptions =
|
||||
new PublicKeyCredentialRequestOptions.Builder()
|
||||
.setChallenge(challenge)
|
||||
.setAllowList(allowedList)
|
||||
.setTimeoutSeconds(timeoutMs / 1000.0)
|
||||
.setRpId(rpId)
|
||||
.setAuthenticationExtensions(ext)
|
||||
.build();
|
||||
|
||||
Uri origin = Uri.parse(originStr);
|
||||
BrowserPublicKeyCredentialRequestOptions browserOptions =
|
||||
new BrowserPublicKeyCredentialRequestOptions.Builder()
|
||||
.setPublicKeyCredentialRequestOptions(requestOptions)
|
||||
.setOrigin(origin)
|
||||
.build();
|
||||
|
||||
Task<Fido2PendingIntent> result = fidoClient.getSignIntent(browserOptions);
|
||||
|
||||
result.addOnSuccessListener(new OnSuccessListener<Fido2PendingIntent>() {
|
||||
@Override
|
||||
public void onSuccess(Fido2PendingIntent pendingIntent) {
|
||||
if (pendingIntent.hasPendingIntent()) {
|
||||
final WebAuthnGetAssertionResult resultHandler =
|
||||
new WebAuthnGetAssertionResult(handler);
|
||||
|
||||
try {
|
||||
pendingIntent.launchPendingIntent(currentActivity,
|
||||
ActivityHandlerHelper.registerActivityHandler(resultHandler));
|
||||
} catch (IntentSender.SendIntentException e) {
|
||||
Log.w(LOG_TAG, "pendingIntent failure", e);
|
||||
handler.onFailure("UNKNOWN_ERR");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
result.addOnFailureListener(new OnFailureListener() {
|
||||
@Override
|
||||
public void onFailure(Exception e) {
|
||||
Log.w(LOG_TAG, "onFailure=" + e);
|
||||
e.printStackTrace();
|
||||
handler.onFailure("UNKNOWN_ERR");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class WebAuthnGetAssertionResult implements ActivityResultHandler {
|
||||
private WebAuthnTokenManager.WebAuthnGetAssertionResponse mHandler;
|
||||
|
||||
WebAuthnGetAssertionResult(WebAuthnTokenManager.WebAuthnGetAssertionResponse handler) {
|
||||
this.mHandler = handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int resultCode, Intent data) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
|
||||
if (data.hasExtra(Fido.FIDO2_KEY_ERROR_EXTRA)) {
|
||||
Log.w(LOG_TAG, "FIDO2_KEY_ERROR_EXTRA and right");
|
||||
byte[] errData = data.getByteArrayExtra(Fido.FIDO2_KEY_ERROR_EXTRA);
|
||||
AuthenticatorErrorResponse responseData =
|
||||
AuthenticatorErrorResponse.deserializeFromBytes(errData);
|
||||
|
||||
Log.e(LOG_TAG, "errorCode.name: " + responseData.getErrorCode());
|
||||
Log.e(LOG_TAG, "errorMessage: " + responseData.getErrorMessage());
|
||||
|
||||
mHandler.onFailure(responseData.getErrorCode().name());
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.hasExtra(Fido.FIDO2_KEY_RESPONSE_EXTRA)) {
|
||||
Log.w(LOG_TAG, "FIDO2_KEY_RESPONSE_EXTRA and right");
|
||||
byte[] rspData = data.getByteArrayExtra(Fido.FIDO2_KEY_RESPONSE_EXTRA);
|
||||
AuthenticatorAssertionResponse responseData =
|
||||
AuthenticatorAssertionResponse.deserializeFromBytes(rspData);
|
||||
|
||||
Log.e(LOG_TAG, "key handle: " + Base64.encodeToString(responseData.getKeyHandle(), Base64.DEFAULT));
|
||||
Log.e(LOG_TAG, "clientDataJSON: " + Base64.encodeToString(responseData.getClientDataJSON(), Base64.DEFAULT));
|
||||
Log.e(LOG_TAG, "auth data: " + Base64.encodeToString(responseData.getAuthenticatorData(), Base64.DEFAULT));
|
||||
Log.e(LOG_TAG, "signature: " + Base64.encodeToString(responseData.getSignature(), Base64.DEFAULT));
|
||||
|
||||
// Nullable field
|
||||
byte[] userHandle = responseData.getUserHandle();
|
||||
if (userHandle == null) {
|
||||
userHandle = new byte[0];
|
||||
}
|
||||
|
||||
mHandler.onSuccess(
|
||||
responseData.getClientDataJSON(),
|
||||
responseData.getKeyHandle(),
|
||||
responseData.getAuthenticatorData(),
|
||||
responseData.getSignature(),
|
||||
userHandle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
Log.w(LOG_TAG, "RESULT_CANCELED" + resultCode);
|
||||
mHandler.onFailure("ABORT_ERR");
|
||||
return;
|
||||
}
|
||||
|
||||
mHandler.onFailure("UNKNOWN_ERR");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,212 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* 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/. */
|
||||
|
||||
package org.mozilla.gecko;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.mozilla.gecko.annotation.WrapForJNI;
|
||||
import org.mozilla.gecko.util.GeckoBundle;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public class WebAuthnTokenManager {
|
||||
private static final String LOGTAG = "WebAuthnTokenManager";
|
||||
|
||||
public static class WebAuthnPublicCredential {
|
||||
public final byte[] mId;
|
||||
public final byte mTransports;
|
||||
|
||||
public WebAuthnPublicCredential(final byte[] aId, final byte aTransports) {
|
||||
this.mId = aId;
|
||||
this.mTransports = aTransports;
|
||||
}
|
||||
|
||||
static ArrayList<WebAuthnPublicCredential> CombineBuffers(
|
||||
final Object[] idObjectList, final ByteBuffer transportList) {
|
||||
if (idObjectList.length != transportList.remaining()) {
|
||||
throw new RuntimeException("Couldn't extract allowed list!");
|
||||
}
|
||||
|
||||
ArrayList<WebAuthnPublicCredential> credList =
|
||||
new ArrayList<WebAuthnPublicCredential>();
|
||||
|
||||
byte[] transportBytes = new byte[transportList.remaining()];
|
||||
transportList.get(transportBytes);
|
||||
|
||||
for (int i = 0; i < idObjectList.length; i++) {
|
||||
final ByteBuffer id = (ByteBuffer)idObjectList[i];
|
||||
byte[] idBytes = new byte[id.remaining()];
|
||||
id.get(idBytes);
|
||||
|
||||
credList.add(new WebAuthnPublicCredential(idBytes, transportBytes[i]));
|
||||
}
|
||||
return credList;
|
||||
}
|
||||
}
|
||||
|
||||
// From WebAuthentication.webidl
|
||||
public enum AttestationPreference {
|
||||
NONE,
|
||||
INDIRECT,
|
||||
DIRECT,
|
||||
}
|
||||
|
||||
public interface WebAuthnMakeCredentialResponse {
|
||||
void onSuccess(final byte[] clientDataJson, final byte[] keyHandle,
|
||||
final byte[] attestationObject);
|
||||
void onFailure(String errorCode);
|
||||
}
|
||||
|
||||
@WrapForJNI(calledFrom = "gecko")
|
||||
private static void webAuthnMakeCredential(final String rpId, final GeckoBundle identifiers,
|
||||
final ByteBuffer userId,
|
||||
final ByteBuffer challenge,
|
||||
final long timeoutMs, final String origin,
|
||||
final Object[] idList,
|
||||
final ByteBuffer transportList,
|
||||
final GeckoBundle authenticatorSelection,
|
||||
final GeckoBundle extensions) {
|
||||
ArrayList<WebAuthnPublicCredential> excludeList;
|
||||
|
||||
// TODO: Return a GeckoResult instead, Bug 1550116
|
||||
|
||||
if (!GeckoAppShell.isFennec()) {
|
||||
Log.w(LOGTAG, "Currently only supported on Fennec");
|
||||
webAuthnMakeCredentialReturnError("NOT_SUPPORTED_ERR");
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] challBytes = new byte[challenge.remaining()];
|
||||
byte[] userBytes = new byte[userId.remaining()];
|
||||
try {
|
||||
challenge.get(challBytes);
|
||||
userId.get(userBytes);
|
||||
|
||||
excludeList = WebAuthnPublicCredential.CombineBuffers(idList,
|
||||
transportList);
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(LOGTAG, "Couldn't extract nio byte arrays!", e);
|
||||
webAuthnMakeCredentialReturnError("UNKNOWN_ERR");
|
||||
return;
|
||||
}
|
||||
|
||||
WebAuthnMakeCredentialResponse handler = new WebAuthnMakeCredentialResponse() {
|
||||
@Override
|
||||
public void onSuccess(final byte[] clientDataJson, final byte[] keyHandle,
|
||||
final byte[] attestationObject) {
|
||||
webAuthnMakeCredentialFinish(clientDataJson, keyHandle,
|
||||
attestationObject);
|
||||
}
|
||||
@Override
|
||||
public void onFailure(final String errorCode) {
|
||||
webAuthnMakeCredentialReturnError(errorCode);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
final Class<?> cls = Class.forName("org.mozilla.gecko.util.WebAuthnUtils");
|
||||
Class<?>[] argTypes = new Class<?>[] { String.class, GeckoBundle.class,
|
||||
byte[].class, byte[].class, long.class,
|
||||
String.class, WebAuthnPublicCredential[].class,
|
||||
GeckoBundle.class, GeckoBundle.class,
|
||||
WebAuthnMakeCredentialResponse.class };
|
||||
Method make = cls.getDeclaredMethod("makeCredential", argTypes);
|
||||
|
||||
make.invoke(null, rpId, identifiers, userBytes, challBytes,
|
||||
timeoutMs, origin,
|
||||
excludeList.toArray(new WebAuthnPublicCredential[0]),
|
||||
authenticatorSelection, extensions, handler);
|
||||
} catch (Exception e) {
|
||||
Log.w(LOGTAG, "Couldn't run WebAuthnUtils", e);
|
||||
webAuthnMakeCredentialReturnError("UNKNOWN_ERR");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
|
||||
/* package */ static native void webAuthnMakeCredentialFinish(final byte[] clientDataJson,
|
||||
final byte[] keyHandle,
|
||||
final byte[] attestationObject);
|
||||
@WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
|
||||
/* package */ static native void webAuthnMakeCredentialReturnError(String errorCode);
|
||||
|
||||
public interface WebAuthnGetAssertionResponse {
|
||||
void onSuccess(final byte[] clientDataJson, final byte[] keyHandle,
|
||||
final byte[] authData, final byte[] signature,
|
||||
final byte[] userHandle);
|
||||
void onFailure(String errorCode);
|
||||
}
|
||||
|
||||
@WrapForJNI(calledFrom = "gecko")
|
||||
private static void webAuthnGetAssertion(final String rpId, final ByteBuffer challenge,
|
||||
final long timeoutMs, final String origin,
|
||||
final Object[] idList,
|
||||
final ByteBuffer transportList,
|
||||
final GeckoBundle extensions) {
|
||||
ArrayList<WebAuthnPublicCredential> allowList;
|
||||
|
||||
// TODO: Return a GeckoResult instead, Bug 1550116
|
||||
|
||||
if (!GeckoAppShell.isFennec()) {
|
||||
Log.w(LOGTAG, "Currently only supported on Fennec");
|
||||
webAuthnGetAssertionReturnError("NOT_SUPPORTED_ERR");
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] challBytes = new byte[challenge.remaining()];
|
||||
try {
|
||||
challenge.get(challBytes);
|
||||
allowList = WebAuthnPublicCredential.CombineBuffers(idList,
|
||||
transportList);
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(LOGTAG, "Couldn't extract nio byte arrays!", e);
|
||||
webAuthnGetAssertionReturnError("UNKNOWN_ERR");
|
||||
return;
|
||||
}
|
||||
|
||||
WebAuthnGetAssertionResponse handler = new WebAuthnGetAssertionResponse() {
|
||||
@Override
|
||||
public void onSuccess(final byte[] clientDataJson, final byte[] keyHandle,
|
||||
final byte[] authData, final byte[] signature,
|
||||
final byte[] userHandle) {
|
||||
webAuthnGetAssertionFinish(clientDataJson, keyHandle, authData,
|
||||
signature, userHandle);
|
||||
}
|
||||
@Override
|
||||
public void onFailure(final String errorCode) {
|
||||
webAuthnGetAssertionReturnError(errorCode);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
final Class<?> cls = Class.forName("org.mozilla.gecko.util.WebAuthnUtils");
|
||||
Class<?>[] argTypes = new Class<?>[] { String.class, byte[].class, long.class,
|
||||
String.class, WebAuthnPublicCredential[].class,
|
||||
GeckoBundle.class,
|
||||
WebAuthnGetAssertionResponse.class };
|
||||
Method make = cls.getDeclaredMethod("getAssertion", argTypes);
|
||||
|
||||
make.invoke(null, rpId, challBytes, timeoutMs, origin,
|
||||
allowList.toArray(new WebAuthnPublicCredential[0]),
|
||||
extensions, handler);
|
||||
} catch (Exception e) {
|
||||
Log.w(LOGTAG, "Couldn't run WebAuthnUtils", e);
|
||||
webAuthnGetAssertionReturnError("UNKNOWN_ERR");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
|
||||
/* package */ static native void webAuthnGetAssertionFinish(final byte[] clientDataJson,
|
||||
final byte[] keyHandle,
|
||||
final byte[] authData,
|
||||
final byte[] signature,
|
||||
final byte[] userHandle);
|
||||
@WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
|
||||
/* package */ static native void webAuthnGetAssertionReturnError(String errorCode);
|
||||
}
|
@ -121,6 +121,7 @@ pref("security.pki.certificate_transparency.mode", 0);
|
||||
// Hardware Origin-bound Second Factor Support
|
||||
pref("security.webauth.u2f", true);
|
||||
pref("security.webauth.webauthn", true);
|
||||
pref("security.webauth.webauthn_enable_android_fido2", false);
|
||||
// Only one of "enable_softtoken" and "enable_usbtoken" can be true
|
||||
// at a time.
|
||||
pref("security.webauth.webauthn_enable_softtoken", false);
|
||||
|
72
widget/android/WebAuthnTokenManager.cpp
Normal file
72
widget/android/WebAuthnTokenManager.cpp
Normal file
@ -0,0 +1,72 @@
|
||||
/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* 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/AndroidWebAuthnTokenManager.h"
|
||||
|
||||
namespace mozilla {
|
||||
class WebAuthnTokenManager final
|
||||
: public java::WebAuthnTokenManager::Natives<WebAuthnTokenManager> {
|
||||
public:
|
||||
static void WebAuthnMakeCredentialFinish(
|
||||
jni::ByteArray::Param aClientDataJson, jni::ByteArray::Param aKeyHandle,
|
||||
jni::ByteArray::Param aAttestationObject) {
|
||||
mozilla::dom::AndroidWebAuthnResult result;
|
||||
|
||||
result.mClientDataJSON.Assign(
|
||||
reinterpret_cast<const char*>(
|
||||
aClientDataJson->GetElements().Elements()),
|
||||
aClientDataJson->Length());
|
||||
result.mKeyHandle.Assign(
|
||||
reinterpret_cast<uint8_t*>(aKeyHandle->GetElements().Elements()),
|
||||
aKeyHandle->Length());
|
||||
result.mAttObj.Assign(reinterpret_cast<uint8_t*>(
|
||||
aAttestationObject->GetElements().Elements()),
|
||||
aAttestationObject->Length());
|
||||
|
||||
mozilla::dom::AndroidWebAuthnTokenManager::GetInstance()
|
||||
->HandleRegisterResult(std::move(result));
|
||||
}
|
||||
|
||||
static void WebAuthnMakeCredentialReturnError(jni::String::Param aErrorCode) {
|
||||
mozilla::dom::AndroidWebAuthnResult result(aErrorCode->ToString());
|
||||
mozilla::dom::AndroidWebAuthnTokenManager::GetInstance()
|
||||
->HandleRegisterResult(std::move(result));
|
||||
}
|
||||
|
||||
static void WebAuthnGetAssertionFinish(jni::ByteArray::Param aClientDataJson,
|
||||
jni::ByteArray::Param aKeyHandle,
|
||||
jni::ByteArray::Param aAuthData,
|
||||
jni::ByteArray::Param aSignature,
|
||||
jni::ByteArray::Param aUserHandle) {
|
||||
mozilla::dom::AndroidWebAuthnResult result;
|
||||
|
||||
result.mClientDataJSON.Assign(
|
||||
reinterpret_cast<const char*>(
|
||||
aClientDataJson->GetElements().Elements()),
|
||||
aClientDataJson->Length());
|
||||
result.mKeyHandle.Assign(
|
||||
reinterpret_cast<uint8_t*>(aKeyHandle->GetElements().Elements()),
|
||||
aKeyHandle->Length());
|
||||
result.mAuthData.Assign(
|
||||
reinterpret_cast<uint8_t*>(aAuthData->GetElements().Elements()),
|
||||
aAuthData->Length());
|
||||
result.mSignature.Assign(
|
||||
reinterpret_cast<uint8_t*>(aSignature->GetElements().Elements()),
|
||||
aSignature->Length());
|
||||
result.mUserHandle.Assign(
|
||||
reinterpret_cast<uint8_t*>(aUserHandle->GetElements().Elements()),
|
||||
aUserHandle->Length());
|
||||
|
||||
mozilla::dom::AndroidWebAuthnTokenManager::GetInstance()->HandleSignResult(
|
||||
std::move(result));
|
||||
}
|
||||
|
||||
static void WebAuthnGetAssertionReturnError(jni::String::Param aErrorCode) {
|
||||
mozilla::dom::AndroidWebAuthnResult result(aErrorCode->ToString());
|
||||
mozilla::dom::AndroidWebAuthnTokenManager::GetInstance()->HandleSignResult(
|
||||
std::move(result));
|
||||
}
|
||||
};
|
||||
} // namespace mozilla
|
@ -65,6 +65,7 @@ UNIFIED_SOURCES += [
|
||||
'nsWindow.cpp',
|
||||
'ProcInfo.cpp',
|
||||
'ScreenHelperAndroid.cpp',
|
||||
'WebAuthnTokenManager.cpp',
|
||||
'WebExecutorSupport.cpp',
|
||||
]
|
||||
|
||||
|
@ -412,6 +412,7 @@ nsAppShell::nsAppShell()
|
||||
mozilla::widget::WebExecutorSupport::Init();
|
||||
nsWindow::InitNatives();
|
||||
mozilla::gl::AndroidSurfaceTexture::Init();
|
||||
mozilla::WebAuthnTokenManager::Init();
|
||||
|
||||
if (jni::IsFennec()) {
|
||||
BrowserLocaleManagerSupport::Init();
|
||||
|
Loading…
Reference in New Issue
Block a user