From 9d6f16dc4a2961a306f6e020f0e6c04c1e632fec Mon Sep 17 00:00:00 2001 From: "Stefan.Lobbenmeier" Date: Thu, 15 Oct 2020 18:55:21 +0200 Subject: [PATCH 01/13] Migration to AndroidX and set minSdk to 23 / Android 6 --- build.gradle | 26 ++++++++++--------- .../models/PublicKeyCredentialSource.java | 10 +++---- .../labs/webauthn/util/CredentialSafe.java | 8 +++--- .../util/SelectCredentialDialogFragment.java | 4 +-- .../webauthn/util/database/CredentialDao.java | 14 +++++----- .../util/database/CredentialDatabase.java | 6 ++--- .../database/CredentialListViewModel.java | 4 +-- 7 files changed, 37 insertions(+), 35 deletions(-) diff --git a/build.gradle b/build.gradle index 0aabd0c..f295d57 100644 --- a/build.gradle +++ b/build.gradle @@ -15,14 +15,14 @@ apply plugin: 'com.github.dcendents.android-maven' group='com.github.duo-labs' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { - minSdkVersion 28 - targetSdkVersion 28 + minSdkVersion 23 + targetSdkVersion 29 versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' javaCompileOptions { annotationProcessorOptions { @@ -42,29 +42,31 @@ android { dependencies { //implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'androidx.appcompat:appcompat:1.0.0' testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' //androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' implementation 'co.nstant.in:cbor:0.8' implementation 'com.google.code.gson:gson:2.8.5' def lifecycle_version = "1.1.1" - implementation "android.arch.lifecycle:extensions:$lifecycle_version" + implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' //def room_version = "2.1.0-alpha02" //implementation "androidx.room:room-runtime:$room_version" //annotationProcessor "androidx.room:room-compiler:$room_version" def room_version = "1.1.1" - implementation "android.arch.persistence.room:runtime:$room_version" - annotationProcessor "android.arch.persistence.room:compiler:$room_version" + implementation 'androidx.room:room-runtime:2.0.0' + annotationProcessor 'androidx.room:room-compiler:2.0.0' // use kapt for Kotlin // optional - RxJava support for Room - implementation "android.arch.persistence.room:rxjava2:$room_version" + implementation 'androidx.room:room-rxjava2:2.0.0' // optional - Guava support for Room, including Optional and ListenableFuture - implementation "android.arch.persistence.room:guava:$room_version" + implementation 'androidx.room:room-guava:2.0.0' // Test helpers - testImplementation "android.arch.persistence.room:testing:$room_version" + testImplementation 'androidx.room:room-testing:2.0.0' // precis for unicode name validation implementation 'rocks.xmpp:precis:1.0.0' + + implementation 'androidx.security:security-crypto:1.1.0-alpha02' } allprojects { diff --git a/src/main/java/duo/labs/webauthn/models/PublicKeyCredentialSource.java b/src/main/java/duo/labs/webauthn/models/PublicKeyCredentialSource.java index bc63ba2..6f91c12 100644 --- a/src/main/java/duo/labs/webauthn/models/PublicKeyCredentialSource.java +++ b/src/main/java/duo/labs/webauthn/models/PublicKeyCredentialSource.java @@ -1,10 +1,10 @@ package duo.labs.webauthn.models; -import android.arch.persistence.room.Entity; -import android.arch.persistence.room.Ignore; -import android.arch.persistence.room.Index; -import android.arch.persistence.room.PrimaryKey; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.Ignore; +import androidx.room.Index; +import androidx.room.PrimaryKey; import android.util.Base64; import java.security.SecureRandom; diff --git a/src/main/java/duo/labs/webauthn/util/CredentialSafe.java b/src/main/java/duo/labs/webauthn/util/CredentialSafe.java index ec8c9a2..0848fd7 100644 --- a/src/main/java/duo/labs/webauthn/util/CredentialSafe.java +++ b/src/main/java/duo/labs/webauthn/util/CredentialSafe.java @@ -4,7 +4,6 @@ import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyInfo; import android.security.keystore.KeyProperties; -import android.support.annotation.NonNull; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -26,6 +25,7 @@ import java.security.spec.InvalidKeySpecException; import java.util.List; +import androidx.annotation.NonNull; import co.nstant.in.cbor.CborBuilder; import co.nstant.in.cbor.CborEncoder; import co.nstant.in.cbor.CborException; @@ -106,9 +106,9 @@ private KeyPair generateNewES256KeyPair(String alias) throws VirgilException { .setAlgorithmParameterSpec(new ECGenParameterSpec(CURVE_NAME)) .setDigests(KeyProperties.DIGEST_SHA256) .setUserAuthenticationRequired(this.authenticationRequired) // fingerprint or similar - .setUserConfirmationRequired(false) // TODO: Decide if we support Android Trusted Confirmations - .setInvalidatedByBiometricEnrollment(false) - .setIsStrongBoxBacked(this.strongboxRequired) +// .setUserConfirmationRequired(false) // TODO: Decide if we support Android Trusted Confirmations +// .setInvalidatedByBiometricEnrollment(false) +// .setIsStrongBoxBacked(this.strongboxRequired) .build(); try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, KEYSTORE_TYPE); diff --git a/src/main/java/duo/labs/webauthn/util/SelectCredentialDialogFragment.java b/src/main/java/duo/labs/webauthn/util/SelectCredentialDialogFragment.java index 4b87058..fc3f6d3 100644 --- a/src/main/java/duo/labs/webauthn/util/SelectCredentialDialogFragment.java +++ b/src/main/java/duo/labs/webauthn/util/SelectCredentialDialogFragment.java @@ -4,8 +4,8 @@ import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.FragmentActivity; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentActivity; import android.util.Log; import java.lang.ref.WeakReference; diff --git a/src/main/java/duo/labs/webauthn/util/database/CredentialDao.java b/src/main/java/duo/labs/webauthn/util/database/CredentialDao.java index 191a52b..ac88669 100644 --- a/src/main/java/duo/labs/webauthn/util/database/CredentialDao.java +++ b/src/main/java/duo/labs/webauthn/util/database/CredentialDao.java @@ -1,12 +1,12 @@ package duo.labs.webauthn.util.database; -import android.arch.lifecycle.LiveData; -import android.arch.persistence.room.Dao; -import android.arch.persistence.room.Delete; -import android.arch.persistence.room.Insert; -import android.arch.persistence.room.Query; -import android.arch.persistence.room.Transaction; -import android.arch.persistence.room.Update; +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.Query; +import androidx.room.Transaction; +import androidx.room.Update; import java.util.List; diff --git a/src/main/java/duo/labs/webauthn/util/database/CredentialDatabase.java b/src/main/java/duo/labs/webauthn/util/database/CredentialDatabase.java index d53a886..b3b0b60 100644 --- a/src/main/java/duo/labs/webauthn/util/database/CredentialDatabase.java +++ b/src/main/java/duo/labs/webauthn/util/database/CredentialDatabase.java @@ -1,9 +1,9 @@ package duo.labs.webauthn.util.database; -import android.arch.persistence.room.Database; -import android.arch.persistence.room.Room; -import android.arch.persistence.room.RoomDatabase; +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; import android.content.Context; import duo.labs.webauthn.models.PublicKeyCredentialSource; diff --git a/src/main/java/duo/labs/webauthn/util/database/CredentialListViewModel.java b/src/main/java/duo/labs/webauthn/util/database/CredentialListViewModel.java index 8b95b47..610765e 100644 --- a/src/main/java/duo/labs/webauthn/util/database/CredentialListViewModel.java +++ b/src/main/java/duo/labs/webauthn/util/database/CredentialListViewModel.java @@ -1,8 +1,8 @@ package duo.labs.webauthn.util.database; import android.app.Application; -import android.arch.lifecycle.AndroidViewModel; -import android.arch.lifecycle.LiveData; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; import android.os.AsyncTask; import java.security.PublicKey; From 1eea2925af853ceffd88d233dcd291c2bcab7399 Mon Sep 17 00:00:00 2001 From: "Stefan.Lobbenmeier" Date: Fri, 16 Oct 2020 09:54:17 +0200 Subject: [PATCH 02/13] Make code compilable for version 23 --- .../java/duo/labs/webauthn/Authenticator.java | 30 +++---------- .../util/BiometricGetAssertionCallback.java | 13 +++--- .../util/BiometricMakeCredentialCallback.java | 10 +++-- .../labs/webauthn/util/CredentialSafe.java | 45 +++++++++---------- 4 files changed, 39 insertions(+), 59 deletions(-) diff --git a/src/main/java/duo/labs/webauthn/Authenticator.java b/src/main/java/duo/labs/webauthn/Authenticator.java index c5eca19..4cb4142 100644 --- a/src/main/java/duo/labs/webauthn/Authenticator.java +++ b/src/main/java/duo/labs/webauthn/Authenticator.java @@ -3,41 +3,25 @@ import android.content.Context; import android.content.DialogInterface; import android.hardware.biometrics.BiometricPrompt; +import android.os.Build; import android.os.CancellationSignal; import android.util.Log; import android.util.Pair; +import duo.labs.webauthn.exceptions.UnknownError; +import duo.labs.webauthn.exceptions.*; +import duo.labs.webauthn.models.*; +import duo.labs.webauthn.util.*; import java.nio.ByteBuffer; import java.security.KeyPair; import java.security.PrivateKey; import java.security.Signature; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Exchanger; -import duo.labs.webauthn.exceptions.ConstraintError; -import duo.labs.webauthn.exceptions.InvalidStateError; -import duo.labs.webauthn.exceptions.NotAllowedError; -import duo.labs.webauthn.exceptions.NotSupportedError; -import duo.labs.webauthn.exceptions.UnknownError; -import duo.labs.webauthn.exceptions.VirgilException; -import duo.labs.webauthn.exceptions.WebAuthnException; -import duo.labs.webauthn.models.AttestationObject; -import duo.labs.webauthn.models.AuthenticatorGetAssertionOptions; -import duo.labs.webauthn.models.AuthenticatorGetAssertionResult; -import duo.labs.webauthn.models.AuthenticatorMakeCredentialOptions; -import duo.labs.webauthn.models.NoneAttestationObject; -import duo.labs.webauthn.models.PublicKeyCredentialDescriptor; -import duo.labs.webauthn.models.PublicKeyCredentialSource; -import duo.labs.webauthn.util.BiometricGetAssertionCallback; -import duo.labs.webauthn.util.BiometricMakeCredentialCallback; -import duo.labs.webauthn.util.CredentialSelector; -import duo.labs.webauthn.util.CredentialSafe; -import duo.labs.webauthn.util.WebAuthnCryptography; - public class Authenticator { private static final String TAG = "WebauthnAuthenticator"; public static final int SHA_LENGTH = 32; @@ -145,7 +129,7 @@ public AttestationObject makeCredential(AuthenticatorMakeCredentialOptions optio // if we need to obtain user verification, create a biometric prompt for that // else just generate a new credential/attestation object AttestationObject attestationObject = null; - if (credentialSafe.supportsUserVerification()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && credentialSafe.supportsUserVerification()) { if (ctx == null) { throw new VirgilException("User Verification requires passing a context to makeCredential"); } @@ -321,7 +305,7 @@ public AuthenticatorGetAssertionResult getAssertion(AuthenticatorGetAssertionOpt // get verification, if necessary AuthenticatorGetAssertionResult result; boolean keyNeedsUnlocking = credentialSafe.keyRequiresVerification(selectedCredential.keyPairAlias); - if (options.requireUserVerification || keyNeedsUnlocking) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && (options.requireUserVerification || keyNeedsUnlocking)) { if (ctx == null) { throw new VirgilException("User Verification requires passing a context to getAssertion"); } diff --git a/src/main/java/duo/labs/webauthn/util/BiometricGetAssertionCallback.java b/src/main/java/duo/labs/webauthn/util/BiometricGetAssertionCallback.java index cafc4b1..a93232c 100644 --- a/src/main/java/duo/labs/webauthn/util/BiometricGetAssertionCallback.java +++ b/src/main/java/duo/labs/webauthn/util/BiometricGetAssertionCallback.java @@ -1,21 +1,20 @@ package duo.labs.webauthn.util; import android.hardware.biometrics.BiometricPrompt; -import android.os.AsyncTask; +import android.os.Build; import android.util.Log; - -import java.security.Signature; -import java.util.concurrent.Exchanger; - +import androidx.annotation.RequiresApi; import duo.labs.webauthn.Authenticator; import duo.labs.webauthn.exceptions.VirgilException; import duo.labs.webauthn.exceptions.WebAuthnException; -import duo.labs.webauthn.models.AttestationObject; import duo.labs.webauthn.models.AuthenticatorGetAssertionOptions; import duo.labs.webauthn.models.AuthenticatorGetAssertionResult; -import duo.labs.webauthn.models.AuthenticatorMakeCredentialOptions; import duo.labs.webauthn.models.PublicKeyCredentialSource; +import java.security.Signature; +import java.util.concurrent.Exchanger; + +@RequiresApi(api = Build.VERSION_CODES.P) public class BiometricGetAssertionCallback extends BiometricPrompt.AuthenticationCallback { private static final String TAG = "BiometricGetAssertionCallback"; diff --git a/src/main/java/duo/labs/webauthn/util/BiometricMakeCredentialCallback.java b/src/main/java/duo/labs/webauthn/util/BiometricMakeCredentialCallback.java index c2ce58d..a51ba1e 100644 --- a/src/main/java/duo/labs/webauthn/util/BiometricMakeCredentialCallback.java +++ b/src/main/java/duo/labs/webauthn/util/BiometricMakeCredentialCallback.java @@ -1,11 +1,9 @@ package duo.labs.webauthn.util; import android.hardware.biometrics.BiometricPrompt; +import android.os.Build; import android.util.Log; - -import java.security.Signature; -import java.util.concurrent.Exchanger; - +import androidx.annotation.RequiresApi; import duo.labs.webauthn.Authenticator; import duo.labs.webauthn.exceptions.VirgilException; import duo.labs.webauthn.exceptions.WebAuthnException; @@ -13,6 +11,10 @@ import duo.labs.webauthn.models.AuthenticatorMakeCredentialOptions; import duo.labs.webauthn.models.PublicKeyCredentialSource; +import java.security.Signature; +import java.util.concurrent.Exchanger; + +@RequiresApi(api = Build.VERSION_CODES.P) public class BiometricMakeCredentialCallback extends BiometricPrompt.AuthenticationCallback { private static final String TAG = "BiometricMakeCredentialCallback"; diff --git a/src/main/java/duo/labs/webauthn/util/CredentialSafe.java b/src/main/java/duo/labs/webauthn/util/CredentialSafe.java index 0848fd7..52b023c 100644 --- a/src/main/java/duo/labs/webauthn/util/CredentialSafe.java +++ b/src/main/java/duo/labs/webauthn/util/CredentialSafe.java @@ -1,23 +1,21 @@ package duo.labs.webauthn.util; import android.content.Context; +import android.os.Build; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyInfo; import android.security.keystore.KeyProperties; +import androidx.annotation.NonNull; +import co.nstant.in.cbor.CborBuilder; +import co.nstant.in.cbor.CborEncoder; +import co.nstant.in.cbor.CborException; +import duo.labs.webauthn.exceptions.VirgilException; +import duo.labs.webauthn.models.PublicKeyCredentialSource; +import duo.labs.webauthn.util.database.CredentialDatabase; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.UnrecoverableEntryException; +import java.security.*; import java.security.cert.CertificateException; import java.security.interfaces.ECPublicKey; import java.security.spec.ECGenParameterSpec; @@ -25,14 +23,6 @@ import java.security.spec.InvalidKeySpecException; import java.util.List; -import androidx.annotation.NonNull; -import co.nstant.in.cbor.CborBuilder; -import co.nstant.in.cbor.CborEncoder; -import co.nstant.in.cbor.CborException; -import duo.labs.webauthn.exceptions.VirgilException; -import duo.labs.webauthn.models.PublicKeyCredentialSource; -import duo.labs.webauthn.util.database.CredentialDatabase; - /** * CredentialSafe uses the Android KeyStore to generate and store @@ -102,14 +92,19 @@ public boolean supportsUserVerification() { * @throws VirgilException */ private KeyPair generateNewES256KeyPair(String alias) throws VirgilException { - KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_SIGN) + KeyGenParameterSpec.Builder specBuilder = new KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_SIGN) .setAlgorithmParameterSpec(new ECGenParameterSpec(CURVE_NAME)) .setDigests(KeyProperties.DIGEST_SHA256) - .setUserAuthenticationRequired(this.authenticationRequired) // fingerprint or similar -// .setUserConfirmationRequired(false) // TODO: Decide if we support Android Trusted Confirmations -// .setInvalidatedByBiometricEnrollment(false) -// .setIsStrongBoxBacked(this.strongboxRequired) - .build(); + .setUserAuthenticationRequired(this.authenticationRequired); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + specBuilder.setInvalidatedByBiometricEnrollment(false); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + specBuilder.setUserConfirmationRequired(false) + .setIsStrongBoxBacked(this.strongboxRequired); + } + KeyGenParameterSpec spec = specBuilder.build(); try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, KEYSTORE_TYPE); keyPairGenerator.initialize(spec); From 27f32cb6d9c448898006c2f68e528a919e9e6ddc Mon Sep 17 00:00:00 2001 From: "Stefan.Lobbenmeier" Date: Fri, 16 Oct 2020 10:00:50 +0200 Subject: [PATCH 03/13] Shortened Tags to resolve lint "Too Long Log Tags" The logging tag can be at most 23 characters, was 29 The logging tag can be at most 23 characters, was 31 --- .../duo/labs/webauthn/util/BiometricGetAssertionCallback.java | 2 +- .../duo/labs/webauthn/util/BiometricMakeCredentialCallback.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/duo/labs/webauthn/util/BiometricGetAssertionCallback.java b/src/main/java/duo/labs/webauthn/util/BiometricGetAssertionCallback.java index a93232c..4858a48 100644 --- a/src/main/java/duo/labs/webauthn/util/BiometricGetAssertionCallback.java +++ b/src/main/java/duo/labs/webauthn/util/BiometricGetAssertionCallback.java @@ -16,7 +16,7 @@ @RequiresApi(api = Build.VERSION_CODES.P) public class BiometricGetAssertionCallback extends BiometricPrompt.AuthenticationCallback { - private static final String TAG = "BiometricGetAssertionCallback"; + private static final String TAG = "BiometricGetAssertionC"; private Authenticator authenticator; private AuthenticatorGetAssertionOptions options; diff --git a/src/main/java/duo/labs/webauthn/util/BiometricMakeCredentialCallback.java b/src/main/java/duo/labs/webauthn/util/BiometricMakeCredentialCallback.java index a51ba1e..be77db0 100644 --- a/src/main/java/duo/labs/webauthn/util/BiometricMakeCredentialCallback.java +++ b/src/main/java/duo/labs/webauthn/util/BiometricMakeCredentialCallback.java @@ -16,7 +16,7 @@ @RequiresApi(api = Build.VERSION_CODES.P) public class BiometricMakeCredentialCallback extends BiometricPrompt.AuthenticationCallback { - private static final String TAG = "BiometricMakeCredentialCallback"; + private static final String TAG = "BiometricMCredentialC"; private Authenticator authenticator; private AuthenticatorMakeCredentialOptions options; From 8f2052e04f99dff2d576f6190443f22809feb541 Mon Sep 17 00:00:00 2001 From: "Stefan.Lobbenmeier" Date: Fri, 16 Oct 2020 11:33:58 +0200 Subject: [PATCH 04/13] Fix import in Unit Tests --- src/androidTest/java/duo/labs/webauthn/AuthenticatorTest.java | 3 +-- src/androidTest/java/duo/labs/webauthn/CredentialSafeTest.java | 2 +- .../java/duo/labs/webauthn/WebAuthnCryptographyTest.java | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/androidTest/java/duo/labs/webauthn/AuthenticatorTest.java b/src/androidTest/java/duo/labs/webauthn/AuthenticatorTest.java index d0f0a99..0708fe7 100644 --- a/src/androidTest/java/duo/labs/webauthn/AuthenticatorTest.java +++ b/src/androidTest/java/duo/labs/webauthn/AuthenticatorTest.java @@ -1,9 +1,8 @@ package duo.labs.webauthn; import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.util.Base64; +import androidx.test.InstrumentationRegistry; import org.junit.Assert; import org.junit.Before; import org.junit.Test; diff --git a/src/androidTest/java/duo/labs/webauthn/CredentialSafeTest.java b/src/androidTest/java/duo/labs/webauthn/CredentialSafeTest.java index ff353bf..d7d1a7c 100644 --- a/src/androidTest/java/duo/labs/webauthn/CredentialSafeTest.java +++ b/src/androidTest/java/duo/labs/webauthn/CredentialSafeTest.java @@ -1,8 +1,8 @@ package duo.labs.webauthn; import android.content.Context; -import android.support.test.InstrumentationRegistry; +import androidx.test.InstrumentationRegistry; import org.junit.Before; import org.junit.Test; diff --git a/src/androidTest/java/duo/labs/webauthn/WebAuthnCryptographyTest.java b/src/androidTest/java/duo/labs/webauthn/WebAuthnCryptographyTest.java index d084658..fc9a3b0 100644 --- a/src/androidTest/java/duo/labs/webauthn/WebAuthnCryptographyTest.java +++ b/src/androidTest/java/duo/labs/webauthn/WebAuthnCryptographyTest.java @@ -1,8 +1,8 @@ package duo.labs.webauthn; import android.content.Context; -import android.support.test.InstrumentationRegistry; +import androidx.test.InstrumentationRegistry; import org.junit.Before; import org.junit.Test; From a39a4f1972c6f64f7ee192a29af359f963007d6a Mon Sep 17 00:00:00 2001 From: "Stefan.Lobbenmeier" Date: Mon, 19 Oct 2020 12:30:57 +0200 Subject: [PATCH 05/13] Replaced Pair with PubKeyCredParam to make it easier to put in the actual JSON into this library (does not conform to standard though) --- .../java/duo/labs/webauthn/Authenticator.java | 5 ++-- .../AuthenticatorMakeCredentialOptions.java | 6 ++--- .../labs/webauthn/models/PubKeyCredParam.java | 27 +++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 src/main/java/duo/labs/webauthn/models/PubKeyCredParam.java diff --git a/src/main/java/duo/labs/webauthn/Authenticator.java b/src/main/java/duo/labs/webauthn/Authenticator.java index 4cb4142..8591570 100644 --- a/src/main/java/duo/labs/webauthn/Authenticator.java +++ b/src/main/java/duo/labs/webauthn/Authenticator.java @@ -6,7 +6,6 @@ import android.os.Build; import android.os.CancellationSignal; import android.util.Log; -import android.util.Pair; import duo.labs.webauthn.exceptions.UnknownError; import duo.labs.webauthn.exceptions.*; import duo.labs.webauthn.models.*; @@ -27,7 +26,7 @@ public class Authenticator { public static final int SHA_LENGTH = 32; public static final int AUTHENTICATOR_DATA_LENGTH = 141; - private static final Pair ES256_COSE = new Pair<>("public-key", (long) -7); + private static final PubKeyCredParam ES256_COSE = new PubKeyCredParam("public-key", -7); CredentialSafe credentialSafe; WebAuthnCryptography cryptoProvider; @@ -82,7 +81,7 @@ public AttestationObject makeCredential(AuthenticatorMakeCredentialOptions optio } // 2. Check if we support a compatible credential type - if (!options.credTypesAndPubKeyAlgs.contains(ES256_COSE)) { + if (!options.pubKeyCredParams.contains(ES256_COSE)) { Log.w(TAG, "only ES256 is supported"); throw new NotSupportedError(); } diff --git a/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java b/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java index edf820a..3382bd1 100644 --- a/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java +++ b/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java @@ -35,8 +35,8 @@ public class AuthenticatorMakeCredentialOptions { public boolean requireUserPresence; @SerializedName("requireUserVerification") public boolean requireUserVerification; - @SerializedName("credTypesAndPubKeyAlgs") - public List> credTypesAndPubKeyAlgs; + @SerializedName("pubKeyCredParams") + public List pubKeyCredParams; @SerializedName("excludeCredentials") public List excludeCredentialDescriptorList; // TODO: possibly support extensions in the future @@ -62,7 +62,7 @@ public boolean areWellFormed() { if (!(requireUserPresence ^ requireUserVerification)) { // only one may be set return false; } - if (credTypesAndPubKeyAlgs.isEmpty()) { + if (pubKeyCredParams.isEmpty()) { return false; } return true; diff --git a/src/main/java/duo/labs/webauthn/models/PubKeyCredParam.java b/src/main/java/duo/labs/webauthn/models/PubKeyCredParam.java new file mode 100644 index 0000000..7d1a084 --- /dev/null +++ b/src/main/java/duo/labs/webauthn/models/PubKeyCredParam.java @@ -0,0 +1,27 @@ +package duo.labs.webauthn.models; + +import java.util.Objects; + +public class PubKeyCredParam { + public String type; + public int alg; + + public PubKeyCredParam(String type, int alg) { + this.type = type; + this.alg = alg; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PubKeyCredParam)) return false; + PubKeyCredParam that = (PubKeyCredParam) o; + return alg == that.alg && + Objects.equals(type, that.type); + } + + @Override + public int hashCode() { + return Objects.hash(type, alg); + } +} From e76efa40039d1d7ae291c083c6cae0c58cf0c53d Mon Sep 17 00:00:00 2001 From: "Stefan.Lobbenmeier" Date: Mon, 19 Oct 2020 12:31:57 +0200 Subject: [PATCH 06/13] Ignores profile.enforce (and only logs) as webauthn.me does not comply with this --- .../webauthn/models/AuthenticatorMakeCredentialOptions.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java b/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java index 3382bd1..3a67806 100644 --- a/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java +++ b/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java @@ -1,6 +1,7 @@ package duo.labs.webauthn.models; import android.util.Base64; +import android.util.Log; import android.util.Pair; import com.google.gson.Gson; @@ -54,7 +55,7 @@ public boolean areWellFormed() { profile.enforce(rpEntity.name); profile.enforce(userEntity.name); } catch (Exception e) { - return false; + Log.d("AuthMakeCredentialO", String.format("Failed to enforce profile, '%s', '%s'", rpEntity.name, userEntity.name), e); } if (userEntity.id.length <= 0 || userEntity.id.length > 64) { return false; From 3a6b120bcc010dbc76ee02d8a356fe21225069a5 Mon Sep 17 00:00:00 2001 From: "Stefan.Lobbenmeier" Date: Mon, 19 Oct 2020 12:33:19 +0200 Subject: [PATCH 07/13] Added challenge to AuthenticatorMakeCredentialOptions so the challenge is also read from the JSON --- .../labs/webauthn/models/AuthenticatorGetAssertionOptions.java | 2 ++ .../webauthn/models/AuthenticatorMakeCredentialOptions.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/duo/labs/webauthn/models/AuthenticatorGetAssertionOptions.java b/src/main/java/duo/labs/webauthn/models/AuthenticatorGetAssertionOptions.java index 3e62595..f3925a2 100644 --- a/src/main/java/duo/labs/webauthn/models/AuthenticatorGetAssertionOptions.java +++ b/src/main/java/duo/labs/webauthn/models/AuthenticatorGetAssertionOptions.java @@ -28,6 +28,8 @@ public class AuthenticatorGetAssertionOptions { public boolean requireUserPresence; @SerializedName("requireUserVerification") public boolean requireUserVerification; + @SerializedName("challenge") + public String challenge; // TODO: authenticatorExtensions public boolean areWellFormed() { diff --git a/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java b/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java index 3a67806..2c97b9c 100644 --- a/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java +++ b/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java @@ -26,6 +26,8 @@ public class AuthenticatorMakeCredentialOptions { @SerializedName("clientDataHash") public byte[] clientDataHash; + @SerializedName("challenge") + public String challenge; @SerializedName("rp") public RpEntity rpEntity; @SerializedName("user") From 6b271a6b7a6392d2fae4d25f06da74b303e1e298 Mon Sep 17 00:00:00 2001 From: "Stefan.Lobbenmeier" Date: Mon, 19 Oct 2020 12:41:28 +0200 Subject: [PATCH 08/13] Now checks "attestation" attribute to decide whether to use PackedSelfAttestationObject or NoneAttestationObject --- .../java/duo/labs/webauthn/Authenticator.java | 15 ++++++++------- .../AuthenticatorMakeCredentialOptions.java | 2 ++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/duo/labs/webauthn/Authenticator.java b/src/main/java/duo/labs/webauthn/Authenticator.java index 8591570..6a610ae 100644 --- a/src/main/java/duo/labs/webauthn/Authenticator.java +++ b/src/main/java/duo/labs/webauthn/Authenticator.java @@ -226,7 +226,7 @@ public AttestationObject makeInternalCredential(AuthenticatorMakeCredentialOptio byte[] authenticatorData = constructAuthenticatorData(rpIdHash, attestedCredentialData, 0); // 141 bytes // 13. Return attestation object - AttestationObject attestationObject = constructAttestationObject(authenticatorData, options.clientDataHash, credentialSource.keyPairAlias, signature); + AttestationObject attestationObject = constructAttestationObject(authenticatorData, options.clientDataHash, credentialSource.keyPairAlias, signature, options.attestation); return attestationObject; } @@ -483,10 +483,11 @@ private byte[] constructAuthenticatorData(byte[] rpIdHash, byte[] attestedCreden * @param authenticatorData byte array containing the raw authenticatorData object * @param clientDataHash byte array containing the sha256 hash of the client data object (request type, challenge, origin) * @param keyPairAlias alias to lookup the key pair to be used to sign the attestation object + * @param attestation * @return a well-formed AttestationObject structure * @throws VirgilException */ - private AttestationObject constructAttestationObject(byte[] authenticatorData, byte[] clientDataHash, String keyPairAlias, Signature signature) throws VirgilException { + private AttestationObject constructAttestationObject(byte[] authenticatorData, byte[] clientDataHash, String keyPairAlias, Signature signature, String attestation) throws VirgilException { // Our goal in this function is primarily to create a signature over the relevant data fields // From https://www.w3.org/TR/webauthn/#packed-attestation we can see that for self-signed attestation, // `sig` is generated by signing the concatenation of authenticatorData and clientDataHash @@ -510,13 +511,13 @@ private AttestationObject constructAttestationObject(byte[] authenticatorData, b // grab our keypair for this credential KeyPair keyPair = this.credentialSafe.getKeyPairByAlias(keyPairAlias); - byte[] signatureBytes = this.cryptoProvider.performSignature(keyPair.getPrivate(), toSign, signature); + if ("none".equals(attestation)) + return new NoneAttestationObject(authenticatorData); + + byte[] signatureBytes = this.cryptoProvider.performSignature(keyPair.getPrivate(), toSign, signature); // construct our attestation object (attestationObject.asCBOR() can be used to generate the raw object in calling function) - // AttestationObject attestationObject = new PackedSelfAttestationObject(authenticatorData, signatureBytes); - // TODO: Discuss tradeoffs wrt none / packed attestation formats. Switching to none here because packed lacks support. - AttestationObject attestationObject = new NoneAttestationObject(authenticatorData); - return attestationObject; + return new PackedSelfAttestationObject(authenticatorData, signatureBytes); } diff --git a/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java b/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java index 2c97b9c..843be2e 100644 --- a/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java +++ b/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java @@ -24,6 +24,8 @@ import rocks.xmpp.precis.PrecisProfiles; public class AuthenticatorMakeCredentialOptions { + @SerializedName("attestation") + public String attestation; @SerializedName("clientDataHash") public byte[] clientDataHash; @SerializedName("challenge") From 17f0ac5939d4b7431e66f59c6179913a3f06cbb4 Mon Sep 17 00:00:00 2001 From: "Stefan.Lobbenmeier" Date: Mon, 19 Oct 2020 12:41:28 +0200 Subject: [PATCH 09/13] Now checks "attestation" attribute to decide whether to use PackedSelfAttestationObject or NoneAttestationObject --- src/main/java/duo/labs/webauthn/Authenticator.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/duo/labs/webauthn/Authenticator.java b/src/main/java/duo/labs/webauthn/Authenticator.java index 6a610ae..46d422f 100644 --- a/src/main/java/duo/labs/webauthn/Authenticator.java +++ b/src/main/java/duo/labs/webauthn/Authenticator.java @@ -488,6 +488,10 @@ private byte[] constructAuthenticatorData(byte[] rpIdHash, byte[] attestedCreden * @throws VirgilException */ private AttestationObject constructAttestationObject(byte[] authenticatorData, byte[] clientDataHash, String keyPairAlias, Signature signature, String attestation) throws VirgilException { + // No signature needed + if ("none".equals(attestation)) + return new NoneAttestationObject(authenticatorData); + // Our goal in this function is primarily to create a signature over the relevant data fields // From https://www.w3.org/TR/webauthn/#packed-attestation we can see that for self-signed attestation, // `sig` is generated by signing the concatenation of authenticatorData and clientDataHash @@ -512,9 +516,6 @@ private AttestationObject constructAttestationObject(byte[] authenticatorData, b // grab our keypair for this credential KeyPair keyPair = this.credentialSafe.getKeyPairByAlias(keyPairAlias); - if ("none".equals(attestation)) - return new NoneAttestationObject(authenticatorData); - byte[] signatureBytes = this.cryptoProvider.performSignature(keyPair.getPrivate(), toSign, signature); // construct our attestation object (attestationObject.asCBOR() can be used to generate the raw object in calling function) return new PackedSelfAttestationObject(authenticatorData, signatureBytes); From 39abd857140eba0b35302731dfad8d3993b25564 Mon Sep 17 00:00:00 2001 From: "Stefan.Lobbenmeier" Date: Mon, 19 Oct 2020 18:36:12 +0200 Subject: [PATCH 10/13] Renamed to allowCredentials to match JSON provided to browser --- .../labs/webauthn/models/AuthenticatorGetAssertionOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/duo/labs/webauthn/models/AuthenticatorGetAssertionOptions.java b/src/main/java/duo/labs/webauthn/models/AuthenticatorGetAssertionOptions.java index f3925a2..8bb3be1 100644 --- a/src/main/java/duo/labs/webauthn/models/AuthenticatorGetAssertionOptions.java +++ b/src/main/java/duo/labs/webauthn/models/AuthenticatorGetAssertionOptions.java @@ -22,7 +22,7 @@ public class AuthenticatorGetAssertionOptions { public String rpId; @SerializedName("clientDataHash") public byte[] clientDataHash; - @SerializedName("allowCredentialDescriptorList") + @SerializedName("allowCredentials") public List allowCredentialDescriptorList; @SerializedName("requireUserPresence") public boolean requireUserPresence; From 36e63b0f50a8aded2d9cda9b3d8bdcadf1911fcf Mon Sep 17 00:00:00 2001 From: "Stefan.Lobbenmeier" Date: Thu, 22 Oct 2020 11:38:07 +0200 Subject: [PATCH 11/13] No longer checks userEntity.id.length <= 0 as Azure provides an empty id and firefox also only checks > 64 (https://searchfox.org/mozilla-central/source/dom/webauthn/WebAuthnManager.cpp#267) --- .../webauthn/models/AuthenticatorMakeCredentialOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java b/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java index 843be2e..0937b0f 100644 --- a/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java +++ b/src/main/java/duo/labs/webauthn/models/AuthenticatorMakeCredentialOptions.java @@ -61,7 +61,7 @@ public boolean areWellFormed() { } catch (Exception e) { Log.d("AuthMakeCredentialO", String.format("Failed to enforce profile, '%s', '%s'", rpEntity.name, userEntity.name), e); } - if (userEntity.id.length <= 0 || userEntity.id.length > 64) { + if (userEntity.id.length > 64) { return false; } if (!(requireUserPresence ^ requireUserVerification)) { // only one may be set From d45b0a28435f2fcb015dba2440f7ae063604740a Mon Sep 17 00:00:00 2001 From: "Stefan.Lobbenmeier" Date: Fri, 23 Oct 2020 11:20:10 +0200 Subject: [PATCH 12/13] Fix compatibility with filtered userids like on webauthn.me - credendial.id was random id internal to the database, while userHandle is the id communicated with the server --- src/main/java/duo/labs/webauthn/Authenticator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/duo/labs/webauthn/Authenticator.java b/src/main/java/duo/labs/webauthn/Authenticator.java index 46d422f..eb76214 100644 --- a/src/main/java/duo/labs/webauthn/Authenticator.java +++ b/src/main/java/duo/labs/webauthn/Authenticator.java @@ -276,7 +276,7 @@ public AuthenticatorGetAssertionResult getAssertion(AuthenticatorGetAssertionOpt } for (PublicKeyCredentialSource credential : credentials) { - if (allowedCredentialIds.contains(ByteBuffer.wrap(credential.id))) { + if (allowedCredentialIds.contains(ByteBuffer.wrap(credential.userHandle))) { filteredCredentials.add(credential); } } From 49b4b4513b8b67092fb6a9525b1d095ea558718b Mon Sep 17 00:00:00 2001 From: "Stefan.Lobbenmeier" Date: Fri, 23 Oct 2020 16:42:37 +0200 Subject: [PATCH 13/13] Actually webauthn.io uses credential.id to filter while webauthn.me uses credential.userHandle to filter. Now checks for both on the client --- src/main/java/duo/labs/webauthn/Authenticator.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/duo/labs/webauthn/Authenticator.java b/src/main/java/duo/labs/webauthn/Authenticator.java index eb76214..d28e060 100644 --- a/src/main/java/duo/labs/webauthn/Authenticator.java +++ b/src/main/java/duo/labs/webauthn/Authenticator.java @@ -276,7 +276,8 @@ public AuthenticatorGetAssertionResult getAssertion(AuthenticatorGetAssertionOpt } for (PublicKeyCredentialSource credential : credentials) { - if (allowedCredentialIds.contains(ByteBuffer.wrap(credential.userHandle))) { + if (allowedCredentialIds.contains(ByteBuffer.wrap(credential.id)) + || allowedCredentialIds.contains(ByteBuffer.wrap(credential.userHandle))) { filteredCredentials.add(credential); } }