Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Android 6 / version 23 #9

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
54 changes: 20 additions & 34 deletions src/main/java/duo/labs/webauthn/Authenticator.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,30 @@
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;
public static final int AUTHENTICATOR_DATA_LENGTH = 141;

private static final Pair<String, Long> ES256_COSE = new Pair<>("public-key", (long) -7);
private static final PubKeyCredParam ES256_COSE = new PubKeyCredParam("public-key", -7);
CredentialSafe credentialSafe;
WebAuthnCryptography cryptoProvider;

Expand Down Expand Up @@ -98,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();
}
Expand Down Expand Up @@ -145,7 +128,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");
}
Expand Down Expand Up @@ -243,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;
}

Expand Down Expand Up @@ -293,7 +276,8 @@ public AuthenticatorGetAssertionResult getAssertion(AuthenticatorGetAssertionOpt
}

for (PublicKeyCredentialSource credential : credentials) {
if (allowedCredentialIds.contains(ByteBuffer.wrap(credential.id))) {
if (allowedCredentialIds.contains(ByteBuffer.wrap(credential.id))
|| allowedCredentialIds.contains(ByteBuffer.wrap(credential.userHandle))) {
filteredCredentials.add(credential);
}
}
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -500,10 +484,15 @@ 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 {
// 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
Expand All @@ -527,13 +516,10 @@ 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);

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);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ public class AuthenticatorGetAssertionOptions {
public String rpId;
@SerializedName("clientDataHash")
public byte[] clientDataHash;
@SerializedName("allowCredentialDescriptorList")
@SerializedName("allowCredentials")
public List<PublicKeyCredentialDescriptor> allowCredentialDescriptorList;
@SerializedName("requireUserPresence")
public boolean requireUserPresence;
@SerializedName("requireUserVerification")
public boolean requireUserVerification;
@SerializedName("challenge")
public String challenge;
// TODO: authenticatorExtensions

public boolean areWellFormed() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -23,8 +24,12 @@
import rocks.xmpp.precis.PrecisProfiles;

public class AuthenticatorMakeCredentialOptions {
@SerializedName("attestation")
public String attestation;
@SerializedName("clientDataHash")
public byte[] clientDataHash;
@SerializedName("challenge")
public String challenge;
@SerializedName("rp")
public RpEntity rpEntity;
@SerializedName("user")
Expand All @@ -35,8 +40,8 @@ public class AuthenticatorMakeCredentialOptions {
public boolean requireUserPresence;
@SerializedName("requireUserVerification")
public boolean requireUserVerification;
@SerializedName("credTypesAndPubKeyAlgs")
public List<Pair<String, Long>> credTypesAndPubKeyAlgs;
@SerializedName("pubKeyCredParams")
public List<PubKeyCredParam> pubKeyCredParams;
@SerializedName("excludeCredentials")
public List<PublicKeyCredentialDescriptor> excludeCredentialDescriptorList;
// TODO: possibly support extensions in the future
Expand All @@ -54,15 +59,15 @@ 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) {
if (userEntity.id.length > 64) {
return false;
}
if (!(requireUserPresence ^ requireUserVerification)) { // only one may be set
return false;
}
if (credTypesAndPubKeyAlgs.isEmpty()) {
if (pubKeyCredParams.isEmpty()) {
return false;
}
return true;
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/duo/labs/webauthn/models/PubKeyCredParam.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
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";
private static final String TAG = "BiometricGetAssertionC";

private Authenticator authenticator;
private AuthenticatorGetAssertionOptions options;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
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;
import duo.labs.webauthn.models.AttestationObject;
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";
private static final String TAG = "BiometricMCredentialC";

private Authenticator authenticator;
private AuthenticatorMakeCredentialOptions options;
Expand Down
Loading