GithubHelp home page GithubHelp logo

duo-labs / android-webauthn-authenticator Goto Github PK

View Code? Open in Web Editor NEW
110.0 29.0 20.0 123 KB

A WebAuthn Authenticator for Android leveraging hardware-backed key storage and biometric user verification.

License: BSD 3-Clause "New" or "Revised" License

Java 100.00%
android android-library webauthn webauthn-library android-security fido2

android-webauthn-authenticator's Introduction

Android WebAuthn Authenticator Library

This library is meant to serve as an example implementation of the WebAuthn authenticator model. While the specification is currently in Candidate Recommendation, this library conforms as much as possible to the guidelines and implementation procedures outlined by the document.

This implementation currently requires Android API level 28 (Android 9.0) due to the use of the BiometricPrompt.

Quickstart

You can use JitPack to include this module in your Android project, or you can include the source code.

Using JitPack

Add this in your root build.gradle:

    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }

Add this to your dependencies list:

    dependencies {
        implementation 'com.github.duo-labs:android-webauthn-authenticator:master-SNAPSHOT'
    }

Using Source

Pull the source

$ cd ~/your/project/src/directory
$ git clone [email protected]:duo-labs/android-webauthn-authenticator.git

Add the module to your Android project

In Android Studio: File -> New -> Import Module and then point it at the android-webauthn-authenticator directory.

Add the module as a dependency

In Android Studio: File -> Project Structure -> App -> Dependencies -> + -> Module Dependency

Select the android-webauthn-authenticator module. After a Gradle sync, you should be able to use the duo.labs.webauthn package.

Usage

You must first instantiate an Authenticator object.

// Authenticator(Context ctx, boolean authenticationRequired, boolean strongboxRequired)
Authenticator authenticator = new Authenticator(context, true, true);

The Authenticator object is safe to instantiate multiple times.

The arguments passed to the constructor determine whether the keys it generates will require biometric authentication (i.e. can be turned off for testing) and if keys should be stored by the StrongBox Keymaster.

Note that StrongBox is only available on some Android devices.

Make Credential (User Registration)

You can create a new credential by passing an AuthenticatorMakeCredentialOptions object to Authenticator.makeCredential(). You can instantiate an AuthenticatorMakeCredentialOptions object directly and manually set its fields, or use our JSON format.

Our JSON format mostly tracks the arguments to authenticatorMakeCredential from the WebAuthn specification, with a few changes necessary for the serialization of binary data. Here's an example:

{
    "authenticatorExtensions": "", // optional and currently ignored
    "clientDataHash": "LTCT/hWLtJenIgi0oUhkJz7dE8ng+pej+i6YI1QQu60=", // base64
    "credTypesAndPubKeyAlgs": [
        ["public-key", -7]
    ],
    "excludeCredentials": [
        {
            "type": "public-key",
            "id": "lVGyXHwz6vdYignKyctbkIkJto/ADbYbHhE7+ss/87o=" // base64
            // "transports" member optional but ignored
        }
    ],
    "requireResidentKey": true,
    "requireUserPresence": false,
    "requireUserVerification": true,
    "rp": {
        "name": "webauthn.io",
        "id": "webauthn.io"
    },
    "user": {
        "name": "testuser",
        "displayName": "Test User",
        "id": "/QIAAAAAAAAAAA==" // base64
    }
}

Note that requireResidentKey and requireUserPresence are effectively ignored: keys are resident by design, and user presence will always be verified. User verification will always be performed if the Authenticator is instantiated with authenticationRequired set to true; otherwise biometric authentication will not be performed and credential generation will fail if requireUserVerification is true.

(Per the spec, requireUserPresence must be the inverse of requireUserVerification)

Create the options object from JSON:

AuthenticatorMakeCredentialOptions makeCredentialOptions = AuthenticatorMakeCredentialOptions.fromJSON(options);

Then, make a new credential with the options given.

AttestationObject attestationObject = authenticator.makeCredential(makeCredentialOptions);
// or if you want to require user verification and need the biometric dialog:
AttestationObject attestationObject = authenticator.makeCredential(makeCredentialOptions, context, cancellationSignal);

makeCredential requires an application context in order to show the BiometricPrompt, and also accepts an optional CancellationSignal to allow user-initiated cancellation.

Once you have an AttestationObject, you can also retrieve its CBOR representation as follows:

byte[] attestationObjectBytes = attestationObject.asCBOR();

Get Assertion (User Login)

Similar to makeCredential, getAssertion takes an AuthenticatorGetAssertionOptions object which you can either instantiate manually or deserialize from JSON.

The JSON format follows authenticatorGetAssertion with some changes made for handling of binary data. Here's an example:

{
    "allowCredentialDescriptorList": [{
        "id": "jVtTOKLHRMN17I66w48XWuJadCitXg0xZKaZvHdtW6RDCJhxO6Cfff9qbYnZiMQ1pl8CzPkXcXEHwpQYFknN2w==", // base64
        "type": "public-key"
    }],
    "authenticatorExtensions": "", // optional and ignored
    "clientDataHash": "BWlg/oAqeIhMHkGAo10C3sf4U/sy0IohfKB0OlcfHHU=", // base64
    "requireUserPresence": true,
    "requireUserVerification": false,
    "rpId": "webauthn.io"
}

Create the options object from JSON:

AuthenticatorGetAssertionOptions getAssertionOptions = AuthenticatorGetAssertionOptions.fromJSON(options);

Step 7 of authenticatorGetAssertion requires that the authenticator prompt a credential selection. You can use our provided SelectCredentialDialogFragment to provide an interface for user-selection, or implement the CredentialSelector interface to receive a callback when it is time to select a credential.

Programmatic Credential Selection

If you want to programatically select credentials, you'll need to implement CredentialSelector, which is a simple interface:

public interface CredentialSelector {
    public PublicKeyCredentialSource selectFrom(List<PublicKeyCredentialSource> credentialList);
}

Here's a barebones example:

AuthenticatorGetAssertionResult assertionObject = authenticator.getAssertion(getAssertionOptions, new CredentialSelector() {
    @Override
    public PublicKeyCredentialSource selectFrom(List<PublicKeyCredentialSource> credentialList) {
        return credentialList.get(0);
    }
});

User-driven Credential Selection

You can also create a credential selector dialog by using the SelectCredentialDialogFragment helper class, which takes a DialogFragment:

SelectCredentialDialogFragment credentialSelector = new SelectCredentialDialogFragment();
credentialSelector.populateFragmentActivity(fragmentActivity);
AuthenticatorGetAssertionResult assertionObject = authenticator.getAssertion(options, credentialSelector, context, cancellationSignal);

The fragmentActivity supplied should be the main Activity with which the user is currently interacting.

As with the makeCredential operation, in the user-driven case, getAssertion requires an application context in order to show the BiometricPrompt and accepts an optional CancellationSignal to allow user-initiated cancellation.

android-webauthn-authenticator's People

Contributors

jericks-duo avatar thavel avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

android-webauthn-authenticator's Issues

Does not handle user.id as json object gracefully

From webauthn.io I get this json (later in the issue) - the user.id in it is encoded as an object:

    "id": {
      "0": 243,
      "1": 210,
      "2": 9,
      "3": 0,
      "4": 0,
      "5": 0,
      "6": 0,
      "7": 0,
      "8": 0,
      "9": 0
    }

but this library expects it to be encoded in base64 and can not handle the object version of it.

{
  "challenge": {
    "0": 229,
    "1": 201,
    "2": 40,
    "3": 1,
    "4": 241,
    "5": 234,
    "6": 199,
    "7": 52,
    "8": 18,
    "9": 48,
    "10": 178,
    "11": 210,
    "12": 146,
    "13": 73,
    "14": 159,
    "15": 202,
    "16": 161,
    "17": 20,
    "18": 228,
    "19": 115,
    "20": 10,
    "21": 84,
    "22": 214,
    "23": 124,
    "24": 161,
    "25": 205,
    "26": 92,
    "27": 35,
    "28": 164,
    "29": 142,
    "30": 1,
    "31": 184
  },
  "rp": {
    "name": "webauthn.io",
    "id": "webauthn.io"
  },
  "user": {
    "name": "dzigf",
    "displayName": "dzigf",
    "id": {
      "0": 243,
      "1": 210,
      "2": 9,
      "3": 0,
      "4": 0,
      "5": 0,
      "6": 0,
      "7": 0,
      "8": 0,
      "9": 0
    }
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -35
    },
    {
      "type": "public-key",
      "alg": -36
    },
    {
      "type": "public-key",
      "alg": -257
    },
    {
      "type": "public-key",
      "alg": -258
    },
    {
      "type": "public-key",
      "alg": -259
    },
    {
      "type": "public-key",
      "alg": -37
    },
    {
      "type": "public-key",
      "alg": -38
    },
    {
      "type": "public-key",
      "alg": -39
    },
    {
      "type": "public-key",
      "alg": -8
    }
  ],
  "authenticatorSelection": {
    "requireResidentKey": false,
    "userVerification": "discouraged"
  },
  "timeout": 60000,
  "extensions": {
    "txAuthSimple": ""
  },
  "attestation": "none"
}

Attestation object length

Hi,

I'm experimenting and constructAttestationObject() fails on the assertion because authenticatorData is 164 bytes long instead of 141 and seemingly it's because the encodedPublicKey in constructAttestedCredentialData() is longer than expected. I'm using a Pixel 3 with Android 12 and here is the code snippet I'm useing to call your library:

fun register() {
        val authenticator = Authenticator(context, false, false)
        val options = AuthenticatorMakeCredentialOptions().apply {
            clientDataHash = generateId(32)
            credTypesAndPubKeyAlgs = listOf(Pair("public-key", -7))
            requireUserPresence = true
            rpEntity = RpEntity().apply { name = "Test.Company"; id = "testCompany" }
            userEntity = UserEntity().apply { name = "testUser"; displayName = "Test User"; id = generateId(32) }
        }
        val attestationObject = authenticator.makeCredential(options)
        Log.d(TAG, "attestation object id: ${attestationObject.credentialIdBase64}")
    }

    private fun generateId(length: Int): ByteArray {
        val random = SecureRandom()
        val bytes = ByteArray(length)
        random.nextBytes(bytes)
        return bytes
    }

Thanks in advance!

Clarification of Usage ~ Roaming Authenticator via Bluetooth

Just to be clear, this code base is for creating an authenticator on a mobile device to be used by that mobile device. This code base does not create an application that is "available as a roaming authenticator", correct"?

In section 6.2.1 the spec states, "For example, a platform authenticator integrated into a mobile device could make itself available as a roaming authenticator via Bluetooth. In this case the client would recognize it only as a roaming authenticator, and not as a platform authenticator."

Are there any samples available anywhere for creating a roaming authenticator on a mobile device and making it available by Bluetooth? Thx!

Error validating the assertion signature

Hello,

I'm trying to make Webauthn works on my Google Pixel 3XL (running under Android 9 Pie) with this library.

I've started building an authentication backend using duo-labs/webauthn Go library (amazing job, btw!), and I wrote a basic Android app. (SDK >= 28) with this lib., to register and to use my phone's TPM as an authenticator.

I've managed to make the registration works, but I ran into an issue during authentication:

Error validating the assertion signature: <nil>

Which is one of the last steps of the webauthn.FinishLogin() call.

Hence, I've updated my app. to use https://webauthn.io API instead, and ran into the issue:

POST https://webauthn.io/assertion
Error validating the assertion signature: <nil>

I'm probably doing something wrong when mapping API inputs/outputs with this lib parameters, but I can't figure out where...

Here is the Android (Kotlin) code I wrote so far:

val authenticator = Authenticator(applicationContext, true, true)
val user = "[email protected]"

fun register() {
    Thread(Runnable {
        // Begin authenticator registration
        val (_, res2, payload2) = Fuel.get("https://webauthn.io/makeCredential/$user?attType=none&authType=platform")
            .responseObject(RegistrationRequest.Deserializer())
            .throwError()
        val session = res2.headers["Set-Cookie"]?.get(0) as String
        val beginRegistration = payload2.get()
        Log.d("FIDO", "--- RegistrationRequest ---\n${beginRegistration.format()}")
        val attestation = authenticator.makeCredential(
            beginRegistration.toOptions(),
            applicationContext,
            CancellationSignal()
        )

        // Finish authenticator registration
        val data3 = RegistrationResponse.makeResponse(beginRegistration, attestation)
        Log.d("FIDO", "--- RegistrationResponse ---\n${data3.toJson()}")
        val (_, _, payload3) = Fuel.post("https://webauthn.io/makeCredential")
            .header("cookie" to session)
            .body(data3.toJson())
            .responseString()
            .throwError()
        val res = payload3.get()
        Log.d("FIDO", "--- RegistrationResult ---\n$res")
    }).start()
}

fun signin() {
    Thread(Runnable {
        // Begin login with authenticator
        val (_, res1, payload1) = Fuel.get("https://webauthn.io/assertion/$user")
            .responseObject(SigninRequest.Deserializer())
            .throwError()
        val beginLogin = payload1.get()
        val session = res1.headers["Set-Cookie"]?.get(0) as String
        Log.d("FIDO", "--- LoginRequest ---\n${beginLogin.format()}")
        val assertion = authenticator.getAssertion(
            beginLogin.toOptions(),
            { credentialList -> credentialList[0] },  // I've only one credential registered anyway
            applicationContext,
            CancellationSignal()
        )

        // Finish authenticator login
        val data2 = SigninResponse.makeResponse(beginLogin, assertion)
        Log.d("FIDO", "--- LoginRequest ---\n${data2.toJson()}")
        val (_, _, payload2) = Fuel.post("https://webauthn.io/assertion")
            .header("cookie" to session)
            .body(data2.toJson())
            .responseString()
            .throwError()
        val res = payload2.get()
        Log.d("FIDO", "--- LoginRequest ---\n$res")
    }).start()
}

Here is the code I wrote for data classes/mappers:

The ByteArrayUtils I'm using is:

import android.util.Base64
object ByteArrayUtils {
    fun base64URL(str: String): String {
        val decoded = Base64.decode(str, Base64.DEFAULT)
        return encodeBase64URL(decoded)
    }
    fun encodeBase64URL(bytes: ByteArray): String {
        return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
    }
    fun decodeBase64URL(str: String): ByteArray {
        return Base64.decode(str, Base64.NO_PADDING or Base64.NO_WRAP)
    }
}

You might notice that I built attestation and assertion options manually instead of using .fromJSON() methods suggested in the README. I tried .fromJSON() methods, but I still got the Error validating the assertion signature anyway.

I tried to use https://webauthn.io demo webapp from Chrome on my phone, and both registration and signin work (using my phone's TPM as authenticator type).

Any chance you guys can tell me what I am doing wrong?
Thanks in advance!

BiometricPrompt CallBack Not triggering

I try to implement either the Gradle way or the source code way, I realized none of the callbacks are working so it doesn't create any attestation object after I verified it with my fingerprint. Not too sure which part did I messed up with.

Stuck at the part for
"6. Obtain user consent for creating a new credential"

Update

Once I commented out the "exchanger" which is supposed to wait for the result of the attestationobject, the callback is working again, but no way to pass back the object through the method

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.