diff --git a/.gitignore b/.gitignore index c2ea76b..3b905b2 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ ant.properties #Gradle .gradle build -gradle.properties #Maven target diff --git a/README.md b/README.md index 95c3b65..c210297 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This includes the following artifacts of the SDK (cf. [overview of all artifacts | de.cotech:hwsecurity-intent-nfc | 14 | | | de.cotech:hwsecurity-intent-usb | 14 | | | de.cotech:hwsecurity-fido | 14 | 19 | +| de.cotech:hwsecurity-fido2 | 14 | 19 | | de.cotech:hwsecurity-openpgp | 14 | | | de.cotech:hwsecurity-piv | 14 | | | de.cotech:hwsecurity-ui | 14 | 19 | @@ -24,7 +25,7 @@ This includes the following artifacts of the SDK (cf. [overview of all artifacts ## Notice This open source release does not reflect the newest version of the SDK. -Some parts are currently not released as GPLv3, such as FIDO2 support. +Some parts are currently not released as GPLv3. ## Contributing diff --git a/build.gradle b/build.gradle index eea7c10..f22a60e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,9 +5,8 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.android.tools.build:gradle:4.0.0' classpath 'org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.17' - classpath 'digital.wup:android-maven-publish:3.6.2' } } @@ -19,6 +18,6 @@ allprojects { } ext { - compileSdkVersion = 28 - hwSdkVersionName = '3.2.1' + compileSdkVersion = 29 + hwSdkVersionName = '4.1.0' } diff --git a/gen-dokka.sh b/gen-dokka.sh deleted file mode 100755 index e8197fe..0000000 --- a/gen-dokka.sh +++ /dev/null @@ -1,2 +0,0 @@ -rm -R dokka/ -java -jar hw-security/libs/dokka-hugo-fatjar-0.9.17.jar hwsecurity/src/main/java/ -output dokka/reference/ -format hugo diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..63d7b24 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,16 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + + +android.useAndroidX=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4202d0..ab53446 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Jun 18 09:22:28 CEST 2019 +#Wed Jun 24 11:02:11 CEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/gradlew b/gradlew index af6708f..b0d6d0a 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/gradlew.bat b/gradlew.bat index 0f8d593..15e1ee3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/hwsecurity/core/build.gradle b/hwsecurity/core/build.gradle index 4ab2e28..c8b6980 100644 --- a/hwsecurity/core/build.gradle +++ b/hwsecurity/core/build.gradle @@ -1,11 +1,11 @@ apply plugin: 'com.android.library' -apply plugin: 'digital.wup.android-maven-publish' +apply plugin: 'maven-publish' apply plugin: 'org.jetbrains.dokka-android' dependencies { - implementation 'androidx.lifecycle:lifecycle-runtime:2.0.0' + implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0' - compileOnly 'androidx.annotation:annotation:1.0.0' + compileOnly 'androidx.annotation:annotation:1.1.0' api 'com.google.auto.value:auto-value-annotations:1.6.2' annotationProcessor 'com.google.auto.value:auto-value:1.6.2' @@ -35,51 +35,54 @@ android { } } -publishing { - publications { - mavenAar(MavenPublication) { - groupId = 'de.cotech' - artifactId = 'hwsecurity' - version = android.defaultConfig.versionName +// https://developer.android.com/studio/build/maven-publish-plugin +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release - from components.android + groupId = 'de.cotech' + artifactId = 'hwsecurity' + version = android.defaultConfig.versionName - pom { - url = 'https://hwsecurity.dev' - licenses { - license { - name = 'Commercial' - url = 'https://hwsecurity.dev/sales/' - distribution = 'repo' + pom { + url = 'https://hwsecurity.dev' + licenses { + license { + name = 'Commercial' + url = 'https://hwsecurity.dev/sales/' + distribution = 'repo' + } + license { + name = 'GNU General Public License, version 3' + url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + } } - license { - name = 'GNU General Public License, version 3' - url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + organization { + name = 'Confidential Technologies GmbH' + url = 'https://www.cotech.de' } } - organization { - name = 'Confidential Technologies GmbH' - url = 'https://www.cotech.de' - } } } - } - /* - * To upload release, create file gradle.properties in ~/.gradle/ with this content: - * - * cotechMavenName=xxx - * cotechMavenPassword=xxx - */ - if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { - println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" + /* + * To upload release, create file gradle.properties in ~/.gradle/ with this content: + * + * cotechMavenName=xxx + * cotechMavenPassword=xxx + */ + if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { + println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" - repositories { - maven { - credentials { - username cotechMavenName - password cotechMavenPassword + repositories { + maven { + credentials { + username cotechMavenName + password cotechMavenPassword + } + url = "https://maven.cotech.de" } - url = "https://maven.cotech.de" } } } @@ -90,8 +93,9 @@ dokka { dokkaFatJar = files('libs/dokka-hugo-fatjar-0.9.17.jar') // does not work correctly with Maven: //dokkaFatJar = 'de.cotech:dokka-hugo-fatjar:0.9.17' + moduleName = 'hwsecurity' outputFormat = "hugo" - outputDirectory = "$buildDir/dokka/reference" + outputDirectory = "$projectDir/../../hwsecurity.dev/content/reference" sourceDirs = files('src/main/java') packageOptions { diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKey.java b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKey.java index 2994b55..0b8e060 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKey.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKey.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -29,7 +29,7 @@ import androidx.annotation.AnyThread; import androidx.annotation.WorkerThread; -import androidx.lifecycle.LifecycleOwner; + import de.cotech.hw.internal.transport.SecurityKeyInfo.TransportType; import de.cotech.hw.internal.transport.Transport; @@ -81,7 +81,7 @@ public boolean isTransportNfc() { */ @AnyThread public boolean isTransportUsb() { - return transport.getTransportType() == TransportType.USB_CCID || transport.getTransportType() == TransportType.USB_U2FHID; + return transport.getTransportType() == TransportType.USB_CCID || transport.getTransportType() == TransportType.USB_CTAPHID; } /** diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyAuthenticator.java b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyAuthenticator.java index cacc6ba..8007770 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyAuthenticator.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyAuthenticator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyCallback.java b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyCallback.java index 4e93f42..ac2f909 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyCallback.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -65,7 +65,7 @@ default void onSecurityKeyDiscoveryFailed(@NonNull IOException exception) { *

* This callback is only called on Security Keys for which {@link SecurityKey#isPersistentlyConnected()} * returns true. This typically applies to USB devices, but can be applied to NFC devices as well if - * NFC tag monitoring has been enabled via {@link SecurityKeyManagerConfig.Builder#setEnableNfcTagMonitoring}. + * persistent NFC connection has been enabled via {@link SecurityKeyManagerConfig.Builder#setEnablePersistentNfcConnection}. * Those Security Keys are also listed under {@link SecurityKeyManager#getConnectedPersistentSecurityKeys()}. * *

diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyConnectionMode.java b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyConnectionMode.java index 638847a..9b4c38b 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyConnectionMode.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyConnectionMode.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyException.java b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyException.java index e531514..b354194 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyManager.java b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyManager.java index 346b314..9ba33a3 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyManager.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -206,7 +206,7 @@ protected boolean isLoggable(String tag, int priority) { callbackHandlerWorker, config.isAllowUntestedUsbDevices(), config.isEnableDebugLogging()); nfcTagManager = NfcTagManager.createInstance( this::transportConnectAndDeliverOrPostponeOrFail, - callbackHandlerWorker, config.isEnableDebugLogging(), config.isEnableNfcTagMonitoring()); + callbackHandlerWorker, config.isEnableDebugLogging(), config.isEnablePersistentNfcConnection()); application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks); installCotechProviderIfAvailable(); @@ -243,6 +243,12 @@ private void bindToActivity(Activity activity) { if (isUsbDispatchActivity(activity)) { return; } + if (config.getExcludedActivityClasses().contains(activity.getClass())) { + HwTimber.d( + "Activity with class %s is excluded, skipping SecurityKeyManager lifecycle initialization.", + activity.getClass().getSimpleName()); + return; + } if (activity == boundActivity) { return; } @@ -401,12 +407,20 @@ private boolean hasActiveCallbacks() { /** * Registers a callback for when a security key is discovered. + * + * @throws IllegalArgumentException if LifecycleOwner is an excluded class, see {@link SecurityKeyManagerConfig.Builder#addExcludedActivityClass}. */ public void registerCallback(SecurityKeyConnectionMode mode, LifecycleOwner lifecycleOwner, SecurityKeyCallback callback) { if (config == null) { throw new IllegalStateException("SecurityKeyManager must be initialized in your Application class!"); } + if (config.getExcludedActivityClasses().contains(lifecycleOwner.getClass())) { + throw new IllegalArgumentException( + "Cannot registerCallback for Activity with excluded class " + + lifecycleOwner.getClass().getSimpleName() + ". " + + "This is a programming error, check your SecurityKeyManagerConfig."); + } RegisteredConnectionMode registeredConnectionMode = new RegisteredConnectionMode<>(mode, callback, false); lifecycleOwner.getLifecycle().addObserver(registeredConnectionMode); registeredCallbacks.add(0, registeredConnectionMode); diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyManagerConfig.java b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyManagerConfig.java index f39fcda..035cd94 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyManagerConfig.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyManagerConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -25,10 +25,14 @@ package de.cotech.hw; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import android.app.Activity; import android.app.Application; import androidx.annotation.Nullable; - import com.google.auto.value.AutoValue; import de.cotech.hw.internal.dispatch.UsbIntentDispatchActivity; import de.cotech.hw.util.HwTimber; @@ -50,12 +54,14 @@ public abstract class SecurityKeyManagerConfig { @Nullable public abstract HwTimber.Tree getLoggingTree(); - public abstract boolean isEnableNfcTagMonitoring(); + public abstract boolean isEnablePersistentNfcConnection(); public abstract boolean isIgnoreNfcTagAfterUse(); public abstract boolean isDisableNfcDiscoverySound(); + public abstract List> getExcludedActivityClasses(); + static SecurityKeyManagerConfig getDefaultConfig() { return new Builder() .build(); @@ -70,9 +76,10 @@ public static class Builder { private boolean isAllowUntestedUsbDevices = false; private boolean isEnableDebugLogging = false; private HwTimber.Tree loggingTree = null; - private boolean isEnableNfcTagMonitoring = false; + private boolean isEnablePersistentNfcConnection = false; private boolean isIgnoreNfcTagAfterUse = false; private boolean isDisableNfcDiscoverySound = false; + private ArrayList> excludedActivityClasses = new ArrayList<>(); /** * This setting controls USB permission request behavior. @@ -156,8 +163,8 @@ public Builder setLoggingTree(HwTimber.Tree loggingTree) { * Enable this setting if you need to retrieve NFC Security Keys via * {@link SecurityKeyManager#getConnectedPersistentSecurityKeys()}. */ - public Builder setEnableNfcTagMonitoring(boolean isEnableNfcTagMonitoring) { - this.isEnableNfcTagMonitoring = isEnableNfcTagMonitoring; + public Builder setEnablePersistentNfcConnection(boolean isEnablePersistentNfcConnection) { + this.isEnablePersistentNfcConnection = isEnablePersistentNfcConnection; return this; } @@ -184,6 +191,29 @@ public Builder setDisableNfcDiscoverySound(boolean isDisableNfcDiscoverySound) { return this; } + /** + * Add an Activity to the list of excluded Activities. + *

+ * This adds a specific Activity to an exclusion list, making it exempt from the lifecycle + * management by SecurityKeyManager. This effectively disables all features of the hwsecurity + * SDK while this Activity is in the foreground. This is useful for Activities that manage + * their own NFC or USB connections, for example by enabling NFC reader mode via + * {@link android.nfc.NfcAdapter#enableReaderMode}, or yielding processing to a + * {@link android.nfc.cardemulation.HostApduService}. + *

+ * A call to the {@link SecurityKeyManager#registerCallback} method for an Activity that has + * been excluded in this way will result in an {@link IllegalArgumentException}. + * + *

{@code
+         * new SecurityKeyManagerConfig.Builder()
+         *   .addExcludedActivityClass(MyCustomNfcActivity.class)
+         * }
+ */ + public Builder addExcludedActivityClass(Class clazz) { + this.excludedActivityClasses.add(clazz); + return this; + } + /** * Constructs a SecurityKeyManagerConfig from the Builder. */ @@ -193,9 +223,10 @@ public SecurityKeyManagerConfig build() { isAllowUntestedUsbDevices, isEnableDebugLogging, loggingTree, - isEnableNfcTagMonitoring, + isEnablePersistentNfcConnection, isIgnoreNfcTagAfterUse, - isDisableNfcDiscoverySound + isDisableNfcDiscoverySound, + Collections.unmodifiableList(excludedActivityClasses) ); } } diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyTlsClientCertificateAuthenticator.java b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyTlsClientCertificateAuthenticator.java index d4b1b52..4b26791 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyTlsClientCertificateAuthenticator.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/SecurityKeyTlsClientCertificateAuthenticator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/AppletFileNotFoundException.java b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/AppletFileNotFoundException.java index ebbf00f..d6545ca 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/AppletFileNotFoundException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/AppletFileNotFoundException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/AuthenticationMethodBlockedException.java b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/AuthenticationMethodBlockedException.java index ed15fab..bd6e812 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/AuthenticationMethodBlockedException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/AuthenticationMethodBlockedException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/ClaNotSupportedException.java b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/ClaNotSupportedException.java index e0e6cd3..7ee4053 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/ClaNotSupportedException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/ClaNotSupportedException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/ConditionsNotSatisfiedException.java b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/ConditionsNotSatisfiedException.java index 3acfd5a..c644b67 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/ConditionsNotSatisfiedException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/ConditionsNotSatisfiedException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/FileInTerminationStateException.java b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/FileInTerminationStateException.java index 871cd08..0945aa4 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/FileInTerminationStateException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/FileInTerminationStateException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/InsNotSupportedException.java b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/InsNotSupportedException.java index 79ee203..9451bfd 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/InsNotSupportedException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/InsNotSupportedException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SecurityKeyDisconnectedException.java b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SecurityKeyDisconnectedException.java new file mode 100644 index 0000000..8fd1f33 --- /dev/null +++ b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SecurityKeyDisconnectedException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.exceptions; + + +import java.io.IOException; + + +public class SecurityKeyDisconnectedException extends IOException { + public SecurityKeyDisconnectedException(Throwable cause) { + super("Security Key is no longer connected (NFC: it has been taken away from the reader, USB: it has been plugged out)", cause); + } + + public SecurityKeyDisconnectedException() { + this(null); + } +} diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SecurityStatusNotSatisfiedException.java b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SecurityStatusNotSatisfiedException.java index ea7c78a..5982e0c 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SecurityStatusNotSatisfiedException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SecurityStatusNotSatisfiedException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SelectAppletException.java b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SelectAppletException.java index fdc95c5..d438365 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SelectAppletException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SelectAppletException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/WrongDataException.java b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/WrongDataException.java index 9cb930c..e82b704 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/WrongDataException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/WrongDataException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/WrongRequestLengthException.java b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/WrongRequestLengthException.java index 94c700c..6660175 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/WrongRequestLengthException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/WrongRequestLengthException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/dispatch/NfcIntentDispatchActivity.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/dispatch/NfcIntentDispatchActivity.java index 6831ba2..2b0db0b 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/dispatch/NfcIntentDispatchActivity.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/dispatch/NfcIntentDispatchActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/dispatch/UsbIntentDispatchActivity.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/dispatch/UsbIntentDispatchActivity.java index e92c99e..c7b18ba 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/dispatch/UsbIntentDispatchActivity.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/dispatch/UsbIntentDispatchActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/CommandApdu.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/CommandApdu.java index d054fe9..114da62 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/CommandApdu.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/CommandApdu.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -31,7 +31,9 @@ import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; + import com.google.auto.value.AutoValue; + import de.cotech.hw.util.Hex; @@ -42,6 +44,23 @@ @AutoValue @RestrictTo(Scope.LIBRARY_GROUP) public abstract class CommandApdu { + public static final int MAX_APDU_NC_SHORT = 255; + public static final int MAX_APDU_NC_EXTENDED = 65535; + public static final int MAX_APDU_NE_SHORT = 256; + public static final int MAX_APDU_NE_EXTENDED = 65536; + + /** + * Default Ne (expected response data length) is 0. + * This means that the response should return NO DATA + * (responses will be the result code only, such as "0x9000"). + *

+ * Javacards will probably still return data as they don't comply with ISO/IEC 7816-4 in this case. + *

+ * Result: Command's Le value is absent + */ + public static final int DEFAULT_APDU_NE_ZERO = 0; + + public abstract int getCLA(); public abstract int getINS(); public abstract int getP1(); @@ -64,7 +83,7 @@ public static CommandApdu create(CommandApduDescriber describer, byte[] apdu, in } public static CommandApdu create(int cla, int ins, int p1, int p2) { - return create(cla, ins, p1, p2, null, 0, 0, 0, null); + return create(cla, ins, p1, p2, null, 0, 0, DEFAULT_APDU_NE_ZERO, null); } public static CommandApdu create(int cla, int ins, int p1, int p2, int ne) { @@ -72,11 +91,11 @@ public static CommandApdu create(int cla, int ins, int p1, int p2, int ne) { } public static CommandApdu create(int cla, int ins, int p1, int p2, byte[] data) { - return create(cla, ins, p1, p2, data, 0, data.length, 0, null); + return create(cla, ins, p1, p2, data, 0, data.length, DEFAULT_APDU_NE_ZERO, null); } public static CommandApdu create(int cla, int ins, int p1, int p2, byte[] data, int dataOffset, int dataLength) { - return create(cla, ins, p1, p2, data, dataOffset, dataLength, 0, null); + return create(cla, ins, p1, p2, data, dataOffset, dataLength, DEFAULT_APDU_NE_ZERO, null); } public static CommandApdu create(int cla, int ins, int p1, int p2, byte[] data, int ne, CommandApduDescriber describer) { @@ -89,10 +108,10 @@ public static CommandApdu create(int cla, int ins, int p1, int p2, byte[] data, public static CommandApdu create( int cla, int ins, int p1, int p2, byte[] data, int dataOffset, int dataLength, int ne, CommandApduDescriber describer) { - if (ne < 0) { + if (ne < DEFAULT_APDU_NE_ZERO) { throw new IllegalArgumentException("ne must not be negative"); } - if (ne > 65536) { + if (ne > MAX_APDU_NE_EXTENDED) { throw new IllegalArgumentException("ne is too large"); } if (data != null) { @@ -108,6 +127,45 @@ public CommandApdu withNe(int ne) { return create(getCLA(), getINS(), getP1(), getP2(), getData(), ne, getDescriber()); } + /** + * Set Ne (expected response data length) so that the response returns all bytes + * within the limit of 256. + *

+ * Only sets Ne if default of 0 has no changed. + *

+ * Result: Command's Le value is set to 0x00 + */ + public CommandApdu withShortApduNe() { + if (getNe() == DEFAULT_APDU_NE_ZERO) { + return withNe(MAX_APDU_NE_SHORT); + } else { + return this; + } + } + + /** + * Set Ne (expected response data length) so that the response returns all bytes + * within the limit of 65536. + *

+ * Only sets Ne if default of 0 has no changed. + *

+ * Result: Command's Le value is set to 0x0000 + */ + public CommandApdu withExtendedApduNe() { + if (getNe() == DEFAULT_APDU_NE_ZERO) { + return withNe(MAX_APDU_NE_EXTENDED); + } else { + return this; + } + } + + /** + * Override Ne to 65536 + */ + public CommandApdu forceExtendedApduNe() { + return withNe(MAX_APDU_NE_EXTENDED); + } + public CommandApdu withDescriber(CommandApduDescriber describer) { return create(getCLA(), getINS(), getP1(), getP2(), getData(), getNe(), describer); } @@ -129,6 +187,8 @@ public static CommandApdu fromBytes(byte[] apdu, int offset, int length) throws *

* LE, LE1, LE2 may be 0x00. * LC must not be 0x00 and LC1|LC2 must not be 0x00|0x00 + *

+ * see https://docs.oracle.com/javacard/3.0.5/prognotes/extended-apdu-nominal-cases.htm */ public static CommandApdu fromBytes(byte[] apdu) throws IOException { if (apdu.length < 4) { diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/CommandApduDescriber.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/CommandApduDescriber.java index c21b8c5..b7ff3b1 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/CommandApduDescriber.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/CommandApduDescriber.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/Iso7816TLV.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/Iso7816TLV.java index a5b25c1..d209bca 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/Iso7816TLV.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/Iso7816TLV.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/ResponseApdu.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/ResponseApdu.java index 9c8e744..1ef6019 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/ResponseApdu.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/iso7816/ResponseApdu.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/SecurityKeyInfo.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/SecurityKeyInfo.java index bcc2bfa..e51d14e 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/SecurityKeyInfo.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/SecurityKeyInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -83,7 +83,7 @@ public static SecurityKeyInfo create(TransportType transportType, SecurityKeyTyp } public enum TransportType { - NFC, USB_CCID, USB_U2FHID + NFC, USB_CCID, USB_CTAPHID } public enum SecurityKeyType { diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/Transport.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/Transport.java index 38f95b1..60f3476 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/Transport.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/Transport.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/Version.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/Version.java index a885f79..9348c2a 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/Version.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/Version.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/nfc/NfcConnectionDispatcher.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/nfc/NfcConnectionDispatcher.java index fd609ba..b01283f 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/nfc/NfcConnectionDispatcher.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/nfc/NfcConnectionDispatcher.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/nfc/NfcTagManager.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/nfc/NfcTagManager.java index 0231db0..ede6e4e 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/nfc/NfcTagManager.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/nfc/NfcTagManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -50,21 +50,21 @@ public class NfcTagManager { private final OnDiscoveredNfcTagListener callback; private final Handler callbackHandler; private final boolean enableDebugLogging; - private final boolean enableNfcTagMonitoring; + private final boolean enablePersistentNfcConnection; private final HashMap managedNfcTags = new HashMap<>(); public static NfcTagManager createInstance(OnDiscoveredNfcTagListener callback, - Handler handler, boolean enableDebugLogging, boolean enableTagMonitoring) { - return new NfcTagManager(callback, handler, enableDebugLogging, enableTagMonitoring); + Handler handler, boolean enableDebugLogging, boolean enablePersistentNfcConnection) { + return new NfcTagManager(callback, handler, enableDebugLogging, enablePersistentNfcConnection); } private NfcTagManager(OnDiscoveredNfcTagListener callback, Handler handler, boolean enableDebugLogging, - boolean enableNfcTagMonitoring) { + boolean enablePersistentNfcConnection) { this.callback = callback; this.callbackHandler = handler; this.enableDebugLogging = enableDebugLogging; - this.enableNfcTagMonitoring = enableNfcTagMonitoring; + this.enablePersistentNfcConnection = enablePersistentNfcConnection; } @UiThread @@ -127,7 +127,7 @@ synchronized void createNewActiveNfcTransport() { return; } - NfcTransport nfcTransport = NfcTransport.createNfcTransport(nfcTag, enableDebugLogging, enableNfcTagMonitoring); + NfcTransport nfcTransport = NfcTransport.createNfcTransport(nfcTag, enableDebugLogging, enablePersistentNfcConnection); activeTransport = nfcTransport; startMonitorThread(this).start(); callbackHandler.post(() -> callback.nfcTransportDiscovered(nfcTransport)); @@ -175,7 +175,7 @@ public void run() { @WorkerThread boolean deviceIsStillConnected() { long lastTransceiveTime = managedNfcTag.activeTransport.getLastTransceiveTime(); - if (enableNfcTagMonitoring) { + if (enablePersistentNfcConnection) { boolean connectionIsActive = lastTransceiveTime + MONITOR_PING_DELAY > System.currentTimeMillis(); return connectionIsActive || managedNfcTag.activeTransport.ping(); } else { diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/nfc/NfcTransport.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/nfc/NfcTransport.java index fd4cfd5..ee252a5 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/nfc/NfcTransport.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/nfc/NfcTransport.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -38,7 +38,7 @@ import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; import androidx.annotation.WorkerThread; -import de.cotech.hw.exceptions.SecurityKeyLostException; +import de.cotech.hw.exceptions.SecurityKeyDisconnectedException; import de.cotech.hw.internal.iso7816.CommandApdu; import de.cotech.hw.internal.iso7816.ResponseApdu; import de.cotech.hw.internal.transport.SecurityKeyInfo; @@ -83,7 +83,7 @@ private NfcTransport(Tag tag, boolean enableDebugLogging, boolean isPersistently @Override public ResponseApdu transceive(final CommandApdu commandApdu) throws IOException { if (!isConnected()) { - throw new SecurityKeyLostException(); + throw new SecurityKeyDisconnectedException(); } synchronized (connectionLock) { try { @@ -109,7 +109,7 @@ public ResponseApdu transceive(final CommandApdu commandApdu) throws IOException } return responseApdu; } catch (TagLostException e) { - throw new SecurityKeyLostException(); + throw new SecurityKeyDisconnectedException(); } finally { lastTransceiveTime = System.currentTimeMillis(); isTransceiving = false; diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UnsupportedUsbSecurityKeyException.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UnsupportedUsbSecurityKeyException.java index 466c350..697294c 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UnsupportedUsbSecurityKeyException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UnsupportedUsbSecurityKeyException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbConnectionDispatcher.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbConnectionDispatcher.java index 60413ab..b40cd70 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbConnectionDispatcher.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbConnectionDispatcher.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbDeviceManager.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbDeviceManager.java index 31a980f..1433e1b 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbDeviceManager.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbDeviceManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -51,7 +51,7 @@ import androidx.annotation.WorkerThread; import de.cotech.hw.internal.transport.Transport; import de.cotech.hw.internal.transport.usb.ccid.UsbCcidTransport; -import de.cotech.hw.internal.transport.usb.u2fhid.UsbU2fHidTransport; +import de.cotech.hw.internal.transport.usb.ctaphid.UsbCtapHidTransport; import de.cotech.hw.util.Hex; import de.cotech.hw.util.HwTimber; @@ -140,7 +140,7 @@ boolean refreshDeviceIfManaged(UsbDevice usbDevice) { private ManagedUsbDevice createManagedUsbDevice(UsbDevice usbDevice) throws UsbTransportException { HwTimber.d("Initializing managed USB security key"); - List usbInterfaces = UsbUtils.getCcidAndU2fHidInterfaces(usbDevice); + List usbInterfaces = UsbUtils.getCcidAndCtapHidInterfaces(usbDevice); if (usbInterfaces.isEmpty()) { throw new UsbTransportException("USB error: No usable USB class interface found. (Is CCID mode enabled on your security key?)"); } @@ -162,7 +162,7 @@ boolean isRelevantDevice(UsbDevice usbDevice) { boolean hasCcidInterface = false; for (int i = 0; i < usbDevice.getInterfaceCount(); i++) { UsbInterface usbInterface = usbDevice.getInterface(i); - if (UsbUtils.usbInterfaceLooksLikeU2fHid(usbInterface)) { + if (UsbUtils.usbInterfaceLooksLikeCtapHid(usbInterface)) { return true; } if (UsbUtils.usbInterfaceLooksLikeCcid(usbInterface)) { @@ -228,7 +228,7 @@ synchronized void createNewActiveUsbTransport(UsbInterface usbInterface) { usbTransport = UsbCcidTransport.createUsbTransport( usbManager, usbDevice, usbConnection, usbInterface, enableDebugLogging); } else if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) { - usbTransport = UsbU2fHidTransport.createUsbTransport( + usbTransport = UsbCtapHidTransport.createUsbTransport( usbManager, usbDevice, usbConnection, usbInterface, enableDebugLogging); } else { throw new RuntimeException("unsupported USB class"); diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbSecurityKeyTypes.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbSecurityKeyTypes.java index 8622485..0af69a2 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbSecurityKeyTypes.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbSecurityKeyTypes.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbTransportException.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbTransportException.java index 810bdc4..ba8f807 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbTransportException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbTransportException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbUtils.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbUtils.java index c72eebb..6928071 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbUtils.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/UsbUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -93,20 +93,20 @@ private static boolean checkHasIoInterruptEndpoints(UsbInterface usbInterface) { return hasIn && hasOut; } - static List getCcidAndU2fHidInterfaces(UsbDevice device) { + static List getCcidAndCtapHidInterfaces(UsbDevice device) { List result = new ArrayList<>(); for (int i = 0; i < device.getInterfaceCount(); i++) { UsbInterface usbInterface = device.getInterface(i); if (usbInterfaceLooksLikeCcid(usbInterface)) { result.add(usbInterface); - } else if (usbInterfaceLooksLikeU2fHid(usbInterface)) { + } else if (usbInterfaceLooksLikeCtapHid(usbInterface)) { result.add(usbInterface); } } return result; } - static boolean usbInterfaceLooksLikeU2fHid(UsbInterface usbInterface) { + static boolean usbInterfaceLooksLikeCtapHid(UsbInterface usbInterface) { return usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID && UsbUtils.checkHasIoInterruptEndpoints(usbInterface); } @@ -131,7 +131,7 @@ public static byte[] requestHidReportDescriptor(UsbDeviceConnection usbConnectio buf.length, 50); if (bytesRead < 0) { - throw new IOException("Unable to retrieve U2FHID Report data"); + throw new IOException("Unable to retrieve CTAPHID Report data"); } return Arrays.copyOf(buf, bytesRead); } diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/CcidDescriptor.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/CcidDescriptor.java index 8e0ac31..cb85415 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/CcidDescriptor.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/CcidDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/CcidTransceiver.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/CcidTransceiver.java index 71a47d8..2b87f62 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/CcidTransceiver.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/CcidTransceiver.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/CcidTransportProtocol.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/CcidTransportProtocol.java index d446751..36e4b15 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/CcidTransportProtocol.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/CcidTransportProtocol.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/UsbCcidErrorException.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/UsbCcidErrorException.java index 4e9d881..7969239 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/UsbCcidErrorException.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/UsbCcidErrorException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/UsbCcidTransport.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/UsbCcidTransport.java index 2e5f504..88c7763 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/UsbCcidTransport.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/UsbCcidTransport.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -38,7 +38,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; -import de.cotech.hw.exceptions.SecurityKeyLostException; +import de.cotech.hw.exceptions.SecurityKeyDisconnectedException; import de.cotech.hw.internal.iso7816.CommandApdu; import de.cotech.hw.internal.iso7816.ResponseApdu; import de.cotech.hw.internal.transport.SecurityKeyInfo.SecurityKeyType; @@ -152,7 +152,7 @@ public void connect() throws IOException { @Override public ResponseApdu transceive(CommandApdu commandApdu) throws IOException { if (released) { - throw new SecurityKeyLostException(); + throw new SecurityKeyDisconnectedException(); } byte[] rawCommand = commandApdu.toBytes(); if (enableDebugLogging) { @@ -171,7 +171,7 @@ public ResponseApdu transceive(CommandApdu commandApdu) throws IOException { } catch (UsbTransportException e) { if (!UsbUtils.isDeviceStillConnected(usbManager, usbDevice)) { release(); - throw new SecurityKeyLostException(e); + throw new SecurityKeyDisconnectedException(e); } throw e; } diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/Block.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/Block.java index 2c8598d..fbd8aa5 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/Block.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/Block.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/BlockChecksumAlgorithm.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/BlockChecksumAlgorithm.java index 1405823..4dd250e 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/BlockChecksumAlgorithm.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/BlockChecksumAlgorithm.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/IBlock.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/IBlock.java index e5cd753..32eba22 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/IBlock.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/IBlock.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/RBlock.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/RBlock.java index dc87fac..7019000 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/RBlock.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/RBlock.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/SBlock.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/SBlock.java index bf3dbea..e358daf 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/SBlock.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/SBlock.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T0ShortApduProtocol.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T0ShortApduProtocol.java index 6300ce6..c7030ac 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T0ShortApduProtocol.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T0ShortApduProtocol.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T1ShortApduProtocol.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T1ShortApduProtocol.java index 0b06f52..24b24e7 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T1ShortApduProtocol.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T1ShortApduProtocol.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T1TpduBlockFactory.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T1TpduBlockFactory.java index f182cbb..6aee348 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T1TpduBlockFactory.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T1TpduBlockFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T1TpduProtocol.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T1TpduProtocol.java index 17963b2..d3c0308 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T1TpduProtocol.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ccid/tpdu/T1TpduProtocol.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidChangedChannelException.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidChangedChannelException.java new file mode 100644 index 0000000..74fe883 --- /dev/null +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidChangedChannelException.java @@ -0,0 +1,11 @@ +package de.cotech.hw.internal.transport.usb.ctaphid; + + +import de.cotech.hw.internal.transport.usb.UsbTransportException; + + +class CtapHidChangedChannelException extends UsbTransportException { + CtapHidChangedChannelException(int expectedChannelId, int actualChannelId) { + super("Channel changed during transaction, " + expectedChannelId + " to " + actualChannelId); + } +} diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidFailedEnqueueException.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidFailedEnqueueException.java new file mode 100644 index 0000000..0c75ae4 --- /dev/null +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidFailedEnqueueException.java @@ -0,0 +1,11 @@ +package de.cotech.hw.internal.transport.usb.ctaphid; + + +import de.cotech.hw.internal.transport.usb.UsbTransportException; + + +class CtapHidFailedEnqueueException extends UsbTransportException { + CtapHidFailedEnqueueException(String message) { + super(message); + } +} diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidFrameFactory.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidFrameFactory.java similarity index 83% rename from hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidFrameFactory.java rename to hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidFrameFactory.java index 6329219..492808a 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidFrameFactory.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidFrameFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -22,7 +22,7 @@ * along with this program. If not, see . */ -package de.cotech.hw.internal.transport.usb.u2fhid; +package de.cotech.hw.internal.transport.usb.ctaphid; import java.nio.BufferOverflowException; @@ -33,33 +33,33 @@ import androidx.annotation.VisibleForTesting; import de.cotech.hw.internal.transport.usb.UsbTransportException; -final class U2fHidFrameFactory { +final class CtapHidFrameFactory { private static final byte TYPE_INIT = (byte) 0x80; // Initial frame identifier @SuppressWarnings("unused") // public API - static final byte U2FHID_PING = (byte) (TYPE_INIT | 0x01); // Echo data through local processor only + static final byte CTAPHID_PING = (byte) (TYPE_INIT | 0x01); // Echo data through local processor only @SuppressWarnings("unused") // public API - static final byte U2FHID_MSG = (byte) (TYPE_INIT | 0x03); // Send U2F message frame + static final byte CTAPHID_MSG = (byte) (TYPE_INIT | 0x03); // Send CTAPHID message frame @SuppressWarnings("unused") // public API - static final byte U2FHID_LOCK = (byte) (TYPE_INIT | 0x04); // Send lock channel command + static final byte CTAPHID_LOCK = (byte) (TYPE_INIT | 0x04); // Send lock channel command @SuppressWarnings("unused") // public API - static final byte U2FHID_INIT = (byte) (TYPE_INIT | 0x06); // Channel initialization + static final byte CTAPHID_INIT = (byte) (TYPE_INIT | 0x06); // Channel initialization @SuppressWarnings("unused") // public API - static final byte U2FHID_WINK = (byte) (TYPE_INIT | 0x08); // Send device identification wink + static final byte CTAPHID_WINK = (byte) (TYPE_INIT | 0x08); // Send device identification wink @SuppressWarnings("unused") // public API - static final byte U2FHID_CBOR = (byte) (TYPE_INIT | 0x10); // Send U2F message frame + static final byte CTAPHID_CBOR = (byte) (TYPE_INIT | 0x10); // Send CTAPHID message frame @SuppressWarnings("unused") // public API - static final byte U2FHID_ERROR = (byte) (TYPE_INIT | 0x3f); // Error response + static final byte CTAPHID_ERROR = (byte) (TYPE_INIT | 0x3f); // Error response @SuppressWarnings({ "WeakerAccess" }) // public API - static final byte U2FHID_KEEPALIVE = (byte) (TYPE_INIT | 0x3b); // Just a keepalive response + static final byte CTAPHID_KEEPALIVE = (byte) (TYPE_INIT | 0x3b); // Just a keepalive response - static final int U2FHID_BUFFER_SIZE = 64; - static final int U2FHID_CHANNEL_ID_BROADCAST = 0xffffffff; + static final int CTAPHID_BUFFER_SIZE = 64; + static final int CTAPHID_CHANNEL_ID_BROADCAST = 0xffffffff; private static final int FRST_PKT_HDR_LEN = 7; private static final int CONT_PACKET_HEADER_LENGTH = 5; - private static final int MAX_LENGTH_INIT_PACKET = U2FHID_BUFFER_SIZE - FRST_PKT_HDR_LEN; - private static final int MAX_LENGTH_CONT_PACKET = U2FHID_BUFFER_SIZE - CONT_PACKET_HEADER_LENGTH; + private static final int MAX_LENGTH_INIT_PACKET = CTAPHID_BUFFER_SIZE - FRST_PKT_HDR_LEN; + private static final int MAX_LENGTH_CONT_PACKET = CTAPHID_BUFFER_SIZE - CONT_PACKET_HEADER_LENGTH; private static final int MAX_LENGTH_PAYLOAD = MAX_LENGTH_INIT_PACKET + 128 * MAX_LENGTH_CONT_PACKET; private static final int KEEPALIVE_TYPE_PROCESSING = 1; @@ -83,7 +83,7 @@ byte[] wrapFrame(int channelId, byte cmdId, byte[] payload) throws UsbTransportE private byte[] wrapFrameOrThrow(int channelId, byte cmdId, byte[] payload) { int packetsRequiredForPayload = calculatePacketCountForPayload(payload.length); - ByteBuffer output = ByteBuffer.allocate(packetsRequiredForPayload * U2FHID_BUFFER_SIZE).order(ByteOrder.BIG_ENDIAN); + ByteBuffer output = ByteBuffer.allocate(packetsRequiredForPayload * CTAPHID_BUFFER_SIZE).order(ByteOrder.BIG_ENDIAN); int offset = 0; int sequenceIdx = 0; @@ -101,7 +101,7 @@ private byte[] wrapFrameOrThrow(int channelId, byte cmdId, byte[] payload) { * uint32_t cid; // Channel identifier * uint8_t seq; // Sequence number - b7 cleared * uint8_t data[HID_RPT_SIZE - 5]; // Data payload - * } U2FHID_FRAME_CONT; + * } CTAPHID_FRAME_CONT; */ private int writeContPacket(int sequenceIdx, int channelId, byte[] payload, int offset, ByteBuffer output) { @@ -126,7 +126,7 @@ private int writeContPacket(int sequenceIdx, int channelId, byte[] payload, int * uint8_t bcnth; // Message byte count - high part * uint8_t bcntl; // Message byte count - low part * uint8_t data[HID_RPT_SIZE - 7]; // Data payload - * } U2FHID_FRAME_INIT; + * } CTAPHID_FRAME_INIT; */ private int writeInitPacket(byte cmdId, int channelId, byte[] payload, ByteBuffer output) { if ((cmdId & TYPE_INIT) == 0) { @@ -149,8 +149,8 @@ int findExpectedFramesFromInitPacketHeader(int expectedChannelId, ByteBuffer ini try { initPacket.mark(); FrameInitPacketHeader initPacketHeader = FrameInitPacketHeader.fromByteBuffer(initPacket); - if (expectedChannelId != U2FHID_CHANNEL_ID_BROADCAST && initPacketHeader.channelId != expectedChannelId) { - throw new U2fHidChangedChannelException(expectedChannelId, initPacketHeader.channelId); + if (expectedChannelId != CTAPHID_CHANNEL_ID_BROADCAST && initPacketHeader.channelId != expectedChannelId) { + throw new CtapHidChangedChannelException(expectedChannelId, initPacketHeader.channelId); } initPacket.reset(); return calculatePacketCountForPayload(initPacketHeader.payloadLength); @@ -162,7 +162,7 @@ int findExpectedFramesFromInitPacketHeader(int expectedChannelId, ByteBuffer ini KeepaliveType unwrapFrameAsKeepalivePacket(byte[] frameBytes) { ByteBuffer frame = ByteBuffer.wrap(frameBytes).order(ByteOrder.BIG_ENDIAN); FrameInitPacketHeader initPacket = FrameInitPacketHeader.fromByteBuffer(frame); - if (initPacket.cmdId != U2FHID_KEEPALIVE) { + if (initPacket.cmdId != CTAPHID_KEEPALIVE) { return null; } if (initPacket.payloadLength != 1) { @@ -201,12 +201,12 @@ private byte[] unwrapFrameOrThrow(int expectedChannelId, byte expectedCmdId, byt throw new UsbTransportException("Command mismatch = " + (initPacket.cmdId & 0xff) + " Tag = " + (expectedCmdId & 0xff)); } - if (expectedChannelId != U2FHID_CHANNEL_ID_BROADCAST && initPacket.channelId != expectedChannelId) { - throw new U2fHidChangedChannelException(expectedChannelId, initPacket.channelId); + if (expectedChannelId != CTAPHID_CHANNEL_ID_BROADCAST && initPacket.channelId != expectedChannelId) { + throw new CtapHidChangedChannelException(expectedChannelId, initPacket.channelId); } // check we don't have less data than claimed - int expectedBufferLength = calculatePacketCountForPayload(initPacket.payloadLength) * U2FHID_BUFFER_SIZE; + int expectedBufferLength = calculatePacketCountForPayload(initPacket.payloadLength) * CTAPHID_BUFFER_SIZE; if (frame.capacity() != expectedBufferLength) { throw new UsbTransportException( @@ -237,7 +237,7 @@ private int readContPacket(int expectedChannelId, ByteBuffer frame, byte[] paylo throws UsbTransportException { int channelId = frame.getInt(); if (channelId != expectedChannelId) { - throw new U2fHidChangedChannelException(expectedChannelId, channelId); + throw new CtapHidChangedChannelException(expectedChannelId, channelId); } byte sequenceId = frame.get(); @@ -253,7 +253,7 @@ private int readContPacket(int expectedChannelId, ByteBuffer frame, byte[] paylo @VisibleForTesting int calculatePacketCountForPayload(int length) { if (length > MAX_LENGTH_PAYLOAD) { - throw new IllegalArgumentException("Payload too large, U2fHid maximum is 7906 bytes!"); + throw new IllegalArgumentException("Payload too large, CtapHid maximum is 7906 bytes!"); } int lengthAfterFirstPacket = length - MAX_LENGTH_INIT_PACKET; diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidInitStructFactory.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidInitStructFactory.java similarity index 80% rename from hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidInitStructFactory.java rename to hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidInitStructFactory.java index d8cb38d..9e8e07a 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidInitStructFactory.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidInitStructFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -22,7 +22,7 @@ * along with this program. If not, see . */ -package de.cotech.hw.internal.transport.usb.u2fhid; +package de.cotech.hw.internal.transport.usb.ctaphid; import java.nio.BufferOverflowException; @@ -35,11 +35,11 @@ @SuppressWarnings("unused") // public API -class U2fHidInitStructFactory { +class CtapHidInitStructFactory { private static final int INIT_NONCE_SIZE = 8; private final Random random; - U2fHidInitStructFactory(Random random) { + CtapHidInitStructFactory(Random random) { this.random = random; } @@ -47,11 +47,11 @@ class U2fHidInitStructFactory { * * typedef struct { * uint8_t nonce[INIT_NONCE_SIZE]; // Client application nonce - * } U2FHID_INIT_REQ; + * } CTAPHID_INIT_REQ; * */ byte[] createInitRequest() { - byte[] nonce = new byte[U2fHidInitStructFactory.INIT_NONCE_SIZE]; + byte[] nonce = new byte[CtapHidInitStructFactory.INIT_NONCE_SIZE]; random.nextBytes(nonce); return nonce; } @@ -67,17 +67,17 @@ byte[] createInitRequest() { * uint8_t versionMinor; // Minor version number * uint8_t versionBuild; // Build version number * uint8_t capFlags; // Capabilities flags - * } U2FHID_INIT_RESP; + * } CTAPHID_INIT_RESP; */ - U2fHidInitResponse parseInitResponse(byte[] responseBytes, byte[] requestBytes) throws UsbTransportException { + CtapHidInitResponse parseInitResponse(byte[] responseBytes, byte[] requestBytes) throws UsbTransportException { try { - return getU2fHidInitResponseOrThrow(responseBytes, requestBytes); + return getCtapHidInitResponseOrThrow(responseBytes, requestBytes); } catch (BufferOverflowException | BufferUnderflowException e) { throw new UsbTransportException(e); } } - private U2fHidInitResponse getU2fHidInitResponseOrThrow(byte[] responseBytes, byte[] requestBytes) + private CtapHidInitResponse getCtapHidInitResponseOrThrow(byte[] responseBytes, byte[] requestBytes) throws UsbTransportException { ByteBuffer response = ByteBuffer.wrap(responseBytes); ByteBuffer request = ByteBuffer.wrap(requestBytes); @@ -89,11 +89,11 @@ private U2fHidInitResponse getU2fHidInitResponseOrThrow(byte[] responseBytes, by response.clear(); response.position(INIT_NONCE_SIZE); - return U2fHidInitResponse.readFromByteBuffer(response); + return CtapHidInitResponse.readFromByteBuffer(response); } @AutoValue - static abstract class U2fHidInitResponse { + static abstract class CtapHidInitResponse { private static final byte CAPFLAG_WINK = 1; private static final byte CAPFLAG_LOCK = 2; @@ -104,7 +104,7 @@ static abstract class U2fHidInitResponse { abstract byte versionBuild(); abstract byte capabilityFlags(); - static U2fHidInitResponse readFromByteBuffer(ByteBuffer buf) { + static CtapHidInitResponse readFromByteBuffer(ByteBuffer buf) { int channelId = buf.getInt(); byte versionInterface = buf.get(); byte versionMajor = buf.get(); @@ -112,7 +112,7 @@ static U2fHidInitResponse readFromByteBuffer(ByteBuffer buf) { byte versionBuild = buf.get(); byte capabilityFlags = buf.get(); - return new AutoValue_U2fHidInitStructFactory_U2fHidInitResponse( + return new AutoValue_CtapHidInitStructFactory_CtapHidInitResponse( channelId, versionInterface, versionMajor, versionMinor, versionBuild, capabilityFlags ); } diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidTransportProtocol.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidTransportProtocol.java similarity index 75% rename from hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidTransportProtocol.java rename to hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidTransportProtocol.java index 367709f..f8aa5d9 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidTransportProtocol.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidTransportProtocol.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -22,7 +22,7 @@ * along with this program. If not, see . */ -package de.cotech.hw.internal.transport.usb.u2fhid; +package de.cotech.hw.internal.transport.usb.ctaphid; import java.io.IOException; @@ -45,17 +45,16 @@ import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; import de.cotech.hw.internal.transport.usb.UsbTransportException; -import de.cotech.hw.internal.transport.usb.u2fhid.U2fHidFrameFactory.KeepaliveType; -import de.cotech.hw.internal.transport.usb.u2fhid.U2fHidInitStructFactory.U2fHidInitResponse; +import de.cotech.hw.internal.transport.usb.ctaphid.CtapHidFrameFactory.KeepaliveType; import de.cotech.hw.util.HwTimber; @RestrictTo(Scope.LIBRARY_GROUP) -public class U2fHidTransportProtocol { +public class CtapHidTransportProtocol { @NonNull - private final U2fHidInitStructFactory initStructFactory = new U2fHidInitStructFactory(new SecureRandom()); + private final CtapHidInitStructFactory initStructFactory = new CtapHidInitStructFactory(new SecureRandom()); @NonNull - private final U2fHidFrameFactory frameFactory = new U2fHidFrameFactory(); + private final CtapHidFrameFactory frameFactory = new CtapHidFrameFactory(); @NonNull private final UsbDeviceConnection usbCconnection; @NonNull @@ -67,10 +66,10 @@ public class U2fHidTransportProtocol { @NonNull private final ExecutorService executor; - private int channelId = U2fHidFrameFactory.U2FHID_CHANNEL_ID_BROADCAST; + private int channelId = CtapHidFrameFactory.CTAPHID_CHANNEL_ID_BROADCAST; - U2fHidTransportProtocol(@NonNull UsbDeviceConnection usbCconnection, - @NonNull UsbEndpoint usbEndpointIn, @NonNull UsbEndpoint usbEndpointOut) { + CtapHidTransportProtocol(@NonNull UsbDeviceConnection usbCconnection, + @NonNull UsbEndpoint usbEndpointIn, @NonNull UsbEndpoint usbEndpointOut) { // noinspection ConstantConditions, checking method contract if (usbCconnection == null) { throw new NullPointerException(); @@ -88,20 +87,20 @@ public class U2fHidTransportProtocol { this.usbEndpointIn = usbEndpointIn; this.usbEndpointOut = usbEndpointOut; // Allocating a direct buffer here *will break* on some android devices! - this.transferBuffer = ByteBuffer.allocate(U2fHidFrameFactory.U2FHID_BUFFER_SIZE); + this.transferBuffer = ByteBuffer.allocate(CtapHidFrameFactory.CTAPHID_BUFFER_SIZE); this.executor = Executors.newSingleThreadExecutor(); } @WorkerThread public void connect() throws UsbTransportException { - HwTimber.d("Initializing U2FHID transport…"); + HwTimber.d("Initializing CTAPHID transport…"); this.channelId = negotiateChannelId(); } private int negotiateChannelId() throws UsbTransportException { byte[] initRequestBytes = initStructFactory.createInitRequest(); - byte[] requestFrame = frameFactory.wrapFrame(channelId, U2fHidFrameFactory.U2FHID_INIT, initRequestBytes); + byte[] requestFrame = frameFactory.wrapFrame(channelId, CtapHidFrameFactory.CTAPHID_INIT, initRequestBytes); writeHidPacketsToUsbDevice(requestFrame); return performUsbRequestWithTimeout((thread, usbRequest) -> { @@ -113,15 +112,15 @@ private int negotiateChannelId() throws UsbTransportException { while (true) { checkInterrupt(thread); transferBuffer.clear(); - if (!usbRequest.queue(transferBuffer, U2fHidFrameFactory.U2FHID_BUFFER_SIZE)) { - throw new U2fHidFailedEnqueueException("Failed to receive data!"); + if (!usbRequest.queue(transferBuffer, CtapHidFrameFactory.CTAPHID_BUFFER_SIZE)) { + throw new CtapHidFailedEnqueueException("Failed to receive data!"); } usbCconnection.requestWait(); try { - byte[] response = frameFactory.unwrapFrame(channelId, U2fHidFrameFactory.U2FHID_INIT, transferBuffer.array()); - U2fHidInitResponse initResponse = initStructFactory.parseInitResponse(response, initRequestBytes); + byte[] response = frameFactory.unwrapFrame(channelId, CtapHidFrameFactory.CTAPHID_INIT, transferBuffer.array()); + CtapHidInitStructFactory.CtapHidInitResponse initResponse = initStructFactory.parseInitResponse(response, initRequestBytes); - HwTimber.d("U2FHID_INIT response: %s", initResponse); + HwTimber.d("CTAPHID_INIT response: %s", initResponse); return initResponse.channelId(); } catch (UsbTransportException e) { HwTimber.d("Ignoring unrelated INIT response"); @@ -132,16 +131,16 @@ private int negotiateChannelId() throws UsbTransportException { @WorkerThread byte[] transceive(byte[] payload) throws UsbTransportException { - byte[] requestFrame = frameFactory.wrapFrame(channelId, U2fHidFrameFactory.U2FHID_MSG, payload); + byte[] requestFrame = frameFactory.wrapFrame(channelId, CtapHidFrameFactory.CTAPHID_MSG, payload); writeHidPacketsToUsbDevice(requestFrame); byte[] responseFrame = readHidPacketsFromUsbDevice(); - return frameFactory.unwrapFrame(channelId, U2fHidFrameFactory.U2FHID_MSG, responseFrame); + return frameFactory.unwrapFrame(channelId, CtapHidFrameFactory.CTAPHID_MSG, responseFrame); } @WorkerThread byte[] transceiveCbor(byte[] payload) throws UsbTransportException { - byte[] requestFrame = frameFactory.wrapFrame(channelId, U2fHidFrameFactory.U2FHID_CBOR, payload); + byte[] requestFrame = frameFactory.wrapFrame(channelId, CtapHidFrameFactory.CTAPHID_CBOR, payload); writeHidPacketsToUsbDevice(requestFrame); while (true) { @@ -151,7 +150,7 @@ byte[] transceiveCbor(byte[] payload) throws UsbTransportException { HwTimber.d("Received keepalive packet (%s), waiting for response..", keepalivePacketType); continue; } - return frameFactory.unwrapFrame(channelId, U2fHidFrameFactory.U2FHID_CBOR, responseFrame); + return frameFactory.unwrapFrame(channelId, CtapHidFrameFactory.CTAPHID_CBOR, responseFrame); } } @@ -167,20 +166,20 @@ private byte[] readHidPacketsFromUsbDevice() throws UsbTransportException { checkInterrupt(thread); int expectedFrames = readUntilInitHeaderForChannel(usbRequest); - byte[] data = new byte[expectedFrames * U2fHidFrameFactory.U2FHID_BUFFER_SIZE]; + byte[] data = new byte[expectedFrames * CtapHidFrameFactory.CTAPHID_BUFFER_SIZE]; transferBuffer.clear(); - transferBuffer.get(data, 0, U2fHidFrameFactory.U2FHID_BUFFER_SIZE); + transferBuffer.get(data, 0, CtapHidFrameFactory.CTAPHID_BUFFER_SIZE); - int offset = U2fHidFrameFactory.U2FHID_BUFFER_SIZE; + int offset = CtapHidFrameFactory.CTAPHID_BUFFER_SIZE; for (int i = 1; i < expectedFrames; i++) { checkInterrupt(thread); - if (!usbRequest.queue(transferBuffer, U2fHidFrameFactory.U2FHID_BUFFER_SIZE)) { - throw new U2fHidFailedEnqueueException("Failed to receive data!"); + if (!usbRequest.queue(transferBuffer, CtapHidFrameFactory.CTAPHID_BUFFER_SIZE)) { + throw new CtapHidFailedEnqueueException("Failed to receive data!"); } usbCconnection.requestWait(); transferBuffer.clear(); - transferBuffer.get(data, offset, U2fHidFrameFactory.U2FHID_BUFFER_SIZE); - offset += U2fHidFrameFactory.U2FHID_BUFFER_SIZE; + transferBuffer.get(data, offset, CtapHidFrameFactory.CTAPHID_BUFFER_SIZE); + offset += CtapHidFrameFactory.CTAPHID_BUFFER_SIZE; } return data; @@ -189,15 +188,15 @@ private byte[] readHidPacketsFromUsbDevice() throws UsbTransportException { private int readUntilInitHeaderForChannel(UsbRequest usbRequest) throws IOException { while (true) { - if (!usbRequest.queue(transferBuffer, U2fHidFrameFactory.U2FHID_BUFFER_SIZE)) { - throw new U2fHidFailedEnqueueException("Failed to receive data!"); + if (!usbRequest.queue(transferBuffer, CtapHidFrameFactory.CTAPHID_BUFFER_SIZE)) { + throw new CtapHidFailedEnqueueException("Failed to receive data!"); } usbCconnection.requestWait(); transferBuffer.clear(); try { return frameFactory.findExpectedFramesFromInitPacketHeader(channelId, transferBuffer); - } catch (U2fHidChangedChannelException e) { + } catch (CtapHidChangedChannelException e) { HwTimber.d("Received message from wrong channel - ignoring"); } } @@ -205,7 +204,7 @@ private int readUntilInitHeaderForChannel(UsbRequest usbRequest) throws IOExcept @WorkerThread private void writeHidPacketsToUsbDevice(byte[] hidFrame) throws UsbTransportException { - if ((hidFrame.length % U2fHidFrameFactory.U2FHID_BUFFER_SIZE) != 0) { + if ((hidFrame.length % CtapHidFrameFactory.CTAPHID_BUFFER_SIZE) != 0) { throw new IllegalArgumentException("Invalid HID frame size!"); } @@ -220,12 +219,12 @@ private void writeHidPacketsToUsbDevice(byte[] hidFrame) throws UsbTransportExce while (offset < hidFrame.length) { checkInterrupt(thread); transferBuffer.clear(); - transferBuffer.put(hidFrame, offset, U2fHidFrameFactory.U2FHID_BUFFER_SIZE); - if (!usbRequest.queue(transferBuffer, U2fHidFrameFactory.U2FHID_BUFFER_SIZE)) { - throw new U2fHidFailedEnqueueException("Failed to send data!"); + transferBuffer.put(hidFrame, offset, CtapHidFrameFactory.CTAPHID_BUFFER_SIZE); + if (!usbRequest.queue(transferBuffer, CtapHidFrameFactory.CTAPHID_BUFFER_SIZE)) { + throw new CtapHidFailedEnqueueException("Failed to send data!"); } usbCconnection.requestWait(); // blocking - offset += U2fHidFrameFactory.U2FHID_BUFFER_SIZE; + offset += CtapHidFrameFactory.CTAPHID_BUFFER_SIZE; } return null; diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/UsbU2fHidTransport.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/UsbCtapHidTransport.java similarity index 76% rename from hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/UsbU2fHidTransport.java rename to hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/UsbCtapHidTransport.java index e8ac85e..4940d71 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/UsbU2fHidTransport.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/ctaphid/UsbCtapHidTransport.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -22,7 +22,7 @@ * along with this program. If not, see . */ -package de.cotech.hw.internal.transport.usb.u2fhid; +package de.cotech.hw.internal.transport.usb.ctaphid; import java.io.IOException; @@ -42,7 +42,7 @@ import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; -import de.cotech.hw.exceptions.SecurityKeyLostException; +import de.cotech.hw.exceptions.SecurityKeyDisconnectedException; import de.cotech.hw.internal.iso7816.CommandApdu; import de.cotech.hw.internal.iso7816.ResponseApdu; import de.cotech.hw.internal.transport.SecurityKeyInfo.SecurityKeyType; @@ -56,11 +56,11 @@ /** - * USB U2FHID - * https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-hid-protocol-v1.2-ps-20170411.html + * USB CTAPHID + * https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#usb */ @RestrictTo(Scope.LIBRARY_GROUP) -public class UsbU2fHidTransport implements Transport { +public class UsbCtapHidTransport implements Transport { private static final int FIDO2_CLA_PROPRIETARY = 0x80; private static final int FIDO2_INS = 0x10; private static final int FIDO2_P1 = 0x00; @@ -73,20 +73,20 @@ public class UsbU2fHidTransport implements Transport { private final UsbDeviceConnection usbConnection; private final UsbInterface usbInterface; private boolean enableDebugLogging; - private U2fHidTransportProtocol u2fHidTransportProtocol; + private CtapHidTransportProtocol ctapHidTransportProtocol; private boolean released = false; private TransportReleasedCallback transportReleasedCallback; - public static UsbU2fHidTransport createUsbTransport(UsbManager usbManager, UsbDevice usbDevice, - UsbDeviceConnection usbConnection, - UsbInterface usbInterface, boolean enableDebugLogging) { - return new UsbU2fHidTransport(usbManager, usbDevice, usbConnection, usbInterface, enableDebugLogging); + public static UsbCtapHidTransport createUsbTransport(UsbManager usbManager, UsbDevice usbDevice, + UsbDeviceConnection usbConnection, + UsbInterface usbInterface, boolean enableDebugLogging) { + return new UsbCtapHidTransport(usbManager, usbDevice, usbConnection, usbInterface, enableDebugLogging); } - private UsbU2fHidTransport(UsbManager usbManager, UsbDevice usbDevice, - UsbDeviceConnection usbConnection, UsbInterface usbInterface, - boolean enableDebugLogging) { + private UsbCtapHidTransport(UsbManager usbManager, UsbDevice usbDevice, + UsbDeviceConnection usbConnection, UsbInterface usbInterface, + boolean enableDebugLogging) { this.usbManager = usbManager; this.usbDevice = usbDevice; this.usbConnection = usbConnection; @@ -101,7 +101,7 @@ private UsbU2fHidTransport(UsbManager usbManager, UsbDevice usbDevice, */ @Override public boolean isConnected() { - return u2fHidTransportProtocol != null && !released; + return ctapHidTransportProtocol != null && !released; } @Override @@ -130,7 +130,7 @@ public boolean isPersistentConnectionAllowed() { */ @Override public void connect() throws IOException { - if (u2fHidTransportProtocol != null) { + if (ctapHidTransportProtocol != null) { throw new IllegalStateException("Already connected!"); } @@ -140,15 +140,15 @@ public void connect() throws IOException { UsbEndpoint usbIntOut = ioEndpoints.second; if (usbIntIn == null || usbIntOut == null) { - throw new UsbTransportException("USB_U2FHID error: invalid class 3 interface"); + throw new UsbTransportException("CTAPHID error: invalid class 3 interface"); } checkHidReportPrefix(); - U2fHidTransportProtocol u2fHidTransportProtocol = - new U2fHidTransportProtocol(usbConnection, usbIntIn, usbIntOut); - u2fHidTransportProtocol.connect(); - this.u2fHidTransportProtocol = u2fHidTransportProtocol; + CtapHidTransportProtocol ctapHidTransportProtocol = + new CtapHidTransportProtocol(usbConnection, usbIntIn, usbIntOut); + ctapHidTransportProtocol.connect(); + this.ctapHidTransportProtocol = ctapHidTransportProtocol; } private void checkHidReportPrefix() throws IOException { @@ -171,7 +171,7 @@ private void checkHidReportPrefix() throws IOException { @Override public ResponseApdu transceive(CommandApdu commandApdu) throws IOException { if (released) { - throw new SecurityKeyLostException(); + throw new SecurityKeyDisconnectedException(); } try { @@ -179,7 +179,7 @@ public ResponseApdu transceive(CommandApdu commandApdu) throws IOException { } catch (UsbTransportException e) { if (!UsbUtils.isDeviceStillConnected(usbManager, usbDevice)) { release(); - throw new SecurityKeyLostException(e); + throw new SecurityKeyDisconnectedException(e); } throw e; } @@ -188,28 +188,27 @@ public ResponseApdu transceive(CommandApdu commandApdu) throws IOException { private ResponseApdu transceiveInternal(CommandApdu commandApdu) throws IOException { // "For the U2FHID protocol, all raw U2F messages are encoded using extended length APDU encoding." // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-hid-protocol-v1.2-ps-20170411.html - // This will already be the case for some APDUs, see FidoU2fAppletConnection - CommandApdu extendedCommandApdu = commandApdu.withNe(65536); + CommandApdu extendedCommandApdu = commandApdu.forceExtendedApduNe(); if (enableDebugLogging) { - HwTimber.d("U2FHID out: %s", extendedCommandApdu); + HwTimber.d("CTAPHID out: %s", extendedCommandApdu); } long startRealtime = SystemClock.elapsedRealtime(); ResponseApdu responseApdu; if (isCtap2Apdu(commandApdu)) { HwTimber.d("Using CTAP2 CBOR"); - byte[] rawResponse = u2fHidTransportProtocol.transceiveCbor(extendedCommandApdu.getData()); + byte[] rawResponse = ctapHidTransportProtocol.transceiveCbor(extendedCommandApdu.getData()); responseApdu = ResponseApdu.create(0x9000, rawResponse); } else { - byte[] rawResponse = u2fHidTransportProtocol.transceive(extendedCommandApdu.toBytes()); + byte[] rawResponse = ctapHidTransportProtocol.transceive(extendedCommandApdu.toBytes()); responseApdu = ResponseApdu.fromBytes(rawResponse); } if (enableDebugLogging) { long totalTime = SystemClock.elapsedRealtime() - startRealtime; - HwTimber.d("U2FHID in: %s", responseApdu); - HwTimber.d("U2FHID communication took %dms", totalTime); + HwTimber.d("CTAPHID in: %s", responseApdu); + HwTimber.d("CTAPHID communication took %dms", totalTime); } return responseApdu; } @@ -238,7 +237,7 @@ public void release() { @Override public TransportType getTransportType() { - return TransportType.USB_U2FHID; + return TransportType.USB_CTAPHID; } @Override diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidChangedChannelException.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidChangedChannelException.java deleted file mode 100644 index 93b8f52..0000000 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidChangedChannelException.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.cotech.hw.internal.transport.usb.u2fhid; - - -import de.cotech.hw.internal.transport.usb.UsbTransportException; - - -class U2fHidChangedChannelException extends UsbTransportException { - U2fHidChangedChannelException(int expectedChannelId, int actualChannelId) { - super("Channel changed during transaction, " + expectedChannelId + " to " + actualChannelId); - } -} diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidFailedEnqueueException.java b/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidFailedEnqueueException.java deleted file mode 100644 index 183d9ec..0000000 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidFailedEnqueueException.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.cotech.hw.internal.transport.usb.u2fhid; - - -import de.cotech.hw.internal.transport.usb.UsbTransportException; - - -class U2fHidFailedEnqueueException extends UsbTransportException { - U2fHidFailedEnqueueException(String message) { - super(message); - } -} diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/raw/RawSecurityKey.java b/hwsecurity/core/src/main/java/de/cotech/hw/raw/RawSecurityKey.java index 8d37dab..44ec121 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/raw/RawSecurityKey.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/raw/RawSecurityKey.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/raw/RawSecurityKeyConnectionMode.java b/hwsecurity/core/src/main/java/de/cotech/hw/raw/RawSecurityKeyConnectionMode.java index 21da956..c116fc8 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/raw/RawSecurityKeyConnectionMode.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/raw/RawSecurityKeyConnectionMode.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/secrets/AndroidPreferenceSimplePinProvider.java b/hwsecurity/core/src/main/java/de/cotech/hw/secrets/AndroidPreferenceSimplePinProvider.java similarity index 96% rename from hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/secrets/AndroidPreferenceSimplePinProvider.java rename to hwsecurity/core/src/main/java/de/cotech/hw/secrets/AndroidPreferenceSimplePinProvider.java index 18b0f59..4aebf2a 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/secrets/AndroidPreferenceSimplePinProvider.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/secrets/AndroidPreferenceSimplePinProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -22,15 +22,13 @@ * along with this program. If not, see . */ -package de.cotech.hw.openpgp.secrets; +package de.cotech.hw.secrets; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; -import de.cotech.hw.secrets.ByteSecret; -import de.cotech.hw.secrets.PinProvider; import de.cotech.hw.util.Hex; diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/secrets/ByteSecret.java b/hwsecurity/core/src/main/java/de/cotech/hw/secrets/ByteSecret.java index be71edb..9b44626 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/secrets/ByteSecret.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/secrets/ByteSecret.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/secrets/ByteSecretGenerator.java b/hwsecurity/core/src/main/java/de/cotech/hw/secrets/ByteSecretGenerator.java similarity index 66% rename from hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/secrets/ByteSecretGenerator.java rename to hwsecurity/core/src/main/java/de/cotech/hw/secrets/ByteSecretGenerator.java index 08e46f0..ad926dc 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/secrets/ByteSecretGenerator.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/secrets/ByteSecretGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -22,17 +22,12 @@ * along with this program. If not, see . */ -package de.cotech.hw.openpgp.secrets; +package de.cotech.hw.secrets; import androidx.annotation.NonNull; -import de.cotech.hw.secrets.ByteSecret; -import org.bouncycastle.crypto.digests.SHA256Digest; -import org.bouncycastle.crypto.generators.HKDFBytesGenerator; -import org.bouncycastle.crypto.params.HKDFParameters; import java.security.SecureRandom; -import java.util.Arrays; /** @@ -90,39 +85,4 @@ public ByteSecret createRandomNumeric(int numChars) { return ByteSecret.fromCharArrayAsUtf8TakeOwnership(secret); } - /** - * Derives a ByteSecret using SHA256, compatible to RFC 5869 - */ - @Deprecated - public ByteSecret deriveWithSaltAndConsume(ByteSecret secret, String salt, int length) { - byte[] secretBytes = null; - try { - secretBytes = secret.getByteCopyAndClear(); - - SHA256Digest digest = new SHA256Digest(); - HKDFBytesGenerator kDF1BytesGenerator = new HKDFBytesGenerator(digest); - - kDF1BytesGenerator.init(new HKDFParameters(secretBytes, salt.getBytes(), null)); - - byte[] derivedSecret = new byte[length]; - kDF1BytesGenerator.generateBytes(derivedSecret, 0, length); - - return ByteSecret.fromByteArrayAndClear(derivedSecret); - } finally { - zeroArrayQuietly(secretBytes); - } - } - - private void zeroArrayQuietly(byte[] secretBytes) { - if (secretBytes != null) { - Arrays.fill(secretBytes, (byte) 0); - } - } - - private void zeroArrayQuietly(char[] secretChars) { - if (secretChars != null) { - Arrays.fill(secretChars, '\0'); - } - } - } diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/secrets/PinProvider.java b/hwsecurity/core/src/main/java/de/cotech/hw/secrets/PinProvider.java index 02aff7c..d95eb20 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/secrets/PinProvider.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/secrets/PinProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/secrets/StaticPinProvider.java b/hwsecurity/core/src/main/java/de/cotech/hw/secrets/StaticPinProvider.java index c20db0a..eeb2081 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/secrets/StaticPinProvider.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/secrets/StaticPinProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/util/HashUtil.java b/hwsecurity/core/src/main/java/de/cotech/hw/util/HashUtil.java index ae31a83..8c81ae2 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/util/HashUtil.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/util/HashUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -35,7 +35,10 @@ private HashUtil() { } public static byte[] sha256(String data) { byte[] dataBytes = data.getBytes(Charset.forName("UTF-8")); + return sha256(dataBytes); + } + public static byte[] sha256(byte[] dataBytes) { try { return MessageDigest.getInstance("SHA-256").digest(dataBytes); } catch (final NoSuchAlgorithmException e) { diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/util/NfcStatusObserver.java b/hwsecurity/core/src/main/java/de/cotech/hw/util/NfcStatusObserver.java index c2eb034..08b677e 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/util/NfcStatusObserver.java +++ b/hwsecurity/core/src/main/java/de/cotech/hw/util/NfcStatusObserver.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/ccid/CcidTransceiverTest.java b/hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/ccid/CcidTransceiverTest.java index ce66205..ce9e21a 100644 --- a/hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/ccid/CcidTransceiverTest.java +++ b/hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/ccid/CcidTransceiverTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidFrameFactoryTest.java b/hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidFrameFactoryTest.java similarity index 61% rename from hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidFrameFactoryTest.java rename to hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidFrameFactoryTest.java index 86ce4a5..5fb0193 100644 --- a/hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidFrameFactoryTest.java +++ b/hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidFrameFactoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -22,7 +22,7 @@ * along with this program. If not, see . */ -package de.cotech.hw.internal.transport.usb.u2fhid; +package de.cotech.hw.internal.transport.usb.ctaphid; import de.cotech.hw.internal.transport.usb.UsbTransportException; @@ -35,22 +35,22 @@ @SuppressWarnings("WeakerAccess") -public class U2fHidFrameFactoryTest { +public class CtapHidFrameFactoryTest { // message max length = 64 - 7 + 128 * (64 - 5) = 7609 bytes - static final int U2FHID_MAX_SIZE = 7609; + static final int CTAPHID_MAX_SIZE = 7609; static final int CHANNEL_ID = 12345678; static final byte[] MESSAGE_SHORT = Hex.decodeHexOrFail("1a2b3d4f5a6b7c"); static final byte[] MESSAGE_LONG = repeat(MESSAGE_SHORT, 128); - U2fHidFrameFactory factory = new U2fHidFrameFactory(); + CtapHidFrameFactory factory = new CtapHidFrameFactory(); @Test public void wrapUnwrap_short() throws Exception { - byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, MESSAGE_SHORT); - byte[] unwrappedCommand = factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, wrappedCommand); + byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, MESSAGE_SHORT); + byte[] unwrappedCommand = factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, wrappedCommand); - assertEquals(U2fHidFrameFactory.U2FHID_PING, wrappedCommand[4]); + assertEquals(CtapHidFrameFactory.CTAPHID_PING, wrappedCommand[4]); assertArrayEquals(MESSAGE_SHORT, unwrappedCommand); } @@ -63,10 +63,10 @@ public void wrapUnwrap_variableShort() throws Exception { public void wrapUnwrap_variableLength(int len) throws Exception { byte[] payload = new byte[len]; - byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, payload); - byte[] unwrappedCommand = factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, wrappedCommand); + byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, payload); + byte[] unwrappedCommand = factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, wrappedCommand); - assertEquals(U2fHidFrameFactory.U2FHID_PING, wrappedCommand[4]); + assertEquals(CtapHidFrameFactory.CTAPHID_PING, wrappedCommand[4]); assertArrayEquals(payload, unwrappedCommand); } @@ -85,82 +85,82 @@ public void packetNumber() { @Test public void wrapUnwrap_long() throws Exception { - byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, MESSAGE_LONG); - byte[] unwrappedCommand = factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, wrappedCommand); + byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, MESSAGE_LONG); + byte[] unwrappedCommand = factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, wrappedCommand); - assertEquals(U2fHidFrameFactory.U2FHID_PING, wrappedCommand[4]); + assertEquals(CtapHidFrameFactory.CTAPHID_PING, wrappedCommand[4]); assertArrayEquals(MESSAGE_LONG, unwrappedCommand); } @Test public void wrapUnwrap_max() throws Exception { - byte[] MESSAGE_MAX = new byte[U2FHID_MAX_SIZE]; + byte[] MESSAGE_MAX = new byte[CTAPHID_MAX_SIZE]; - byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, MESSAGE_MAX); - byte[] unwrappedCommand = factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, wrappedCommand); + byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, MESSAGE_MAX); + byte[] unwrappedCommand = factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, wrappedCommand); - assertEquals(U2fHidFrameFactory.U2FHID_PING, wrappedCommand[4]); + assertEquals(CtapHidFrameFactory.CTAPHID_PING, wrappedCommand[4]); assertArrayEquals(MESSAGE_MAX, unwrappedCommand); } @Test(expected = UsbTransportException.class) public void wrapUnwrap_badExpectedChannel() throws UsbTransportException { - byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, MESSAGE_SHORT); - factory.unwrapFrame(CHANNEL_ID + 1, U2fHidFrameFactory.U2FHID_PING, wrappedCommand); + byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, MESSAGE_SHORT); + factory.unwrapFrame(CHANNEL_ID + 1, CtapHidFrameFactory.CTAPHID_PING, wrappedCommand); } @Test(expected = UsbTransportException.class) public void wrapUnwrap_badExpectedCommand() throws UsbTransportException { - byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, MESSAGE_SHORT); - factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_MSG, wrappedCommand); + byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, MESSAGE_SHORT); + factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_MSG, wrappedCommand); } @Test(expected = UsbTransportException.class) public void wrapUnwrap_truncated() throws UsbTransportException { - byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, MESSAGE_SHORT); + byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, MESSAGE_SHORT); byte[] truncatedCommand = Arrays.copyOf(wrappedCommand, wrappedCommand.length - 1); - factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, wrappedCommand); - factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, truncatedCommand); + factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, wrappedCommand); + factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, truncatedCommand); } @Test(expected = UsbTransportException.class) public void wrapUnwrap_missingPacket() throws UsbTransportException { - byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, MESSAGE_LONG); - byte[] truncatedCommand = Arrays.copyOf(wrappedCommand, wrappedCommand.length - U2fHidFrameFactory.U2FHID_BUFFER_SIZE); + byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, MESSAGE_LONG); + byte[] truncatedCommand = Arrays.copyOf(wrappedCommand, wrappedCommand.length - CtapHidFrameFactory.CTAPHID_BUFFER_SIZE); - factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, wrappedCommand); - factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, truncatedCommand); + factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, wrappedCommand); + factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, truncatedCommand); } @Test(expected = UsbTransportException.class) public void wrapUnwrap_trailing() throws UsbTransportException { - byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, MESSAGE_SHORT); + byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, MESSAGE_SHORT); byte[] trailingDataCommand = Arrays.append(wrappedCommand, (byte) 0x50); - factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, wrappedCommand); - factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, trailingDataCommand); + factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, wrappedCommand); + factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, trailingDataCommand); } @Test(expected = UsbTransportException.class) public void wrapUnwrap_incorrectLength() throws UsbTransportException { - byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, MESSAGE_SHORT); + byte[] wrappedCommand = factory.wrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, MESSAGE_SHORT); byte[] incorrectLengthCommand = Arrays.clone(wrappedCommand); incorrectLengthCommand[5] += 1; - factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, wrappedCommand); - factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, incorrectLengthCommand); + factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, wrappedCommand); + factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, incorrectLengthCommand); } @Test(expected = UsbTransportException.class) public void unwrap_empty() throws UsbTransportException { - factory.unwrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, new byte[0]); + factory.unwrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, new byte[0]); } @Test(expected = IllegalArgumentException.class) public void wrap_maxPlusOne() throws Exception { - byte[] oversizedMessage = new byte[U2FHID_MAX_SIZE + 1]; - factory.wrapFrame(CHANNEL_ID, U2fHidFrameFactory.U2FHID_PING, oversizedMessage); + byte[] oversizedMessage = new byte[CTAPHID_MAX_SIZE + 1]; + factory.wrapFrame(CHANNEL_ID, CtapHidFrameFactory.CTAPHID_PING, oversizedMessage); } @Test(expected = IllegalArgumentException.class) diff --git a/hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidTransportProtocolTest.java b/hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidTransportProtocolTest.java similarity index 81% rename from hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidTransportProtocolTest.java rename to hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidTransportProtocolTest.java index 4983d7c..157c6e5 100644 --- a/hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/u2fhid/U2fHidTransportProtocolTest.java +++ b/hwsecurity/core/src/test/java/de/cotech/hw/internal/transport/usb/ctaphid/CtapHidTransportProtocolTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -22,7 +22,7 @@ * along with this program. If not, see . */ -package de.cotech.hw.internal.transport.usb.u2fhid; +package de.cotech.hw.internal.transport.usb.ctaphid; import java.io.ByteArrayOutputStream; @@ -58,7 +58,7 @@ @TargetApi(VERSION_CODES.JELLY_BEAN_MR2) @RunWith(RobolectricTestRunner.class) @Config(sdk = 24) -public class U2fHidTransportProtocolTest { +public class CtapHidTransportProtocolTest { static final int CHANNEL_ID = 12345678; static final byte[] DATA_IN = Hex.decodeHexOrFail("1a2b3d4e5f"); static final byte[] DATA_OUT = Hex.decodeHexOrFail("5f4e3d2c1b"); @@ -71,8 +71,8 @@ public class U2fHidTransportProtocolTest { LinkedList requestQueue; - U2fHidTransportProtocol protocol; - U2fHidFrameFactory frameFactory = new U2fHidFrameFactory(); + CtapHidTransportProtocol protocol; + CtapHidFrameFactory frameFactory = new CtapHidFrameFactory(); @Before public void setUp() { @@ -82,7 +82,7 @@ public void setUp() { requestQueue = new LinkedList<>(); - protocol = new U2fHidTransportProtocol(usbConnection, usbIntIn, usbIntOut) { + protocol = new CtapHidTransportProtocol(usbConnection, usbIntIn, usbIntOut) { @Override UsbRequest newUsbRequest() { return requestQueue.poll(); @@ -92,7 +92,7 @@ UsbRequest newUsbRequest() { @Test public void connect() throws Exception { - expect(U2fHidFrameFactory.U2FHID_CHANNEL_ID_BROADCAST, CHANNEL_ID, U2fHidFrameFactory.U2FHID_INIT, nonce -> + expect(CtapHidFrameFactory.CTAPHID_CHANNEL_ID_BROADCAST, CHANNEL_ID, CtapHidFrameFactory.CTAPHID_INIT, nonce -> ByteBuffer .allocate(17) .order(ByteOrder.BIG_ENDIAN) @@ -113,7 +113,7 @@ public void connect() throws Exception { @Test(expected = UsbTransportException.class) public void connect_badNonce() throws Exception { - expect(U2fHidFrameFactory.U2FHID_CHANNEL_ID_BROADCAST, CHANNEL_ID, U2fHidFrameFactory.U2FHID_INIT, nonce -> { + expect(CtapHidFrameFactory.CTAPHID_CHANNEL_ID_BROADCAST, CHANNEL_ID, CtapHidFrameFactory.CTAPHID_INIT, nonce -> { nonce[0] ^= (byte) 0x25; return ByteBuffer .allocate(17) @@ -133,11 +133,11 @@ public void connect_badNonce() throws Exception { @Test(expected = UsbTransportException.class) public void connect_leadingGarbage() throws Exception { - expect(U2fHidFrameFactory.U2FHID_CHANNEL_ID_BROADCAST, CHANNEL_ID, U2fHidFrameFactory.U2FHID_INIT, + expect(CtapHidFrameFactory.CTAPHID_CHANNEL_ID_BROADCAST, CHANNEL_ID, CtapHidFrameFactory.CTAPHID_INIT, nonce -> Hex.decodeHexOrFail("0102030405060708")); - expect(U2fHidFrameFactory.U2FHID_CHANNEL_ID_BROADCAST, CHANNEL_ID, U2fHidFrameFactory.U2FHID_INIT, + expect(CtapHidFrameFactory.CTAPHID_CHANNEL_ID_BROADCAST, CHANNEL_ID, CtapHidFrameFactory.CTAPHID_INIT, nonce -> Hex.decodeHexOrFail("0807060504030201")); - expect(U2fHidFrameFactory.U2FHID_CHANNEL_ID_BROADCAST, CHANNEL_ID, U2fHidFrameFactory.U2FHID_INIT, nonce -> + expect(CtapHidFrameFactory.CTAPHID_CHANNEL_ID_BROADCAST, CHANNEL_ID, CtapHidFrameFactory.CTAPHID_INIT, nonce -> ByteBuffer .allocate(17) .order(ByteOrder.BIG_ENDIAN) @@ -160,7 +160,7 @@ public void connect_leadingGarbage() throws Exception { public void transceive_short() throws Exception { connect(); - expect(CHANNEL_ID, CHANNEL_ID, U2fHidFrameFactory.U2FHID_MSG, data -> { + expect(CHANNEL_ID, CHANNEL_ID, CtapHidFrameFactory.CTAPHID_MSG, data -> { assertArrayEquals(DATA_IN, data); return DATA_OUT; }); @@ -175,7 +175,7 @@ public void transceive_short() throws Exception { public void transceive_long() throws Exception { connect(); - expect(CHANNEL_ID, CHANNEL_ID, U2fHidFrameFactory.U2FHID_MSG, data -> { + expect(CHANNEL_ID, CHANNEL_ID, CtapHidFrameFactory.CTAPHID_MSG, data -> { assertArrayEquals(DATA_IN_LONG, data); return DATA_OUT_LONG; }); @@ -190,12 +190,12 @@ private void verifyDialog() { assertTrue(requestQueue.isEmpty()); } - private void expect(int inputChannelId, int outputChannelId, byte cmdId, U2fCommunicationCallback callback) { + private void expect(int inputChannelId, int outputChannelId, byte cmdId, CtapCommunicationCallback callback) { RequestState state = new RequestState(); UsbRequest usbRequestOut = mock(UsbRequest.class); when(usbRequestOut.initialize(usbConnection, usbIntOut)).thenReturn(true); - when(usbRequestOut.queue(any(ByteBuffer.class), eq(U2fHidFrameFactory.U2FHID_BUFFER_SIZE))).thenAnswer( + when(usbRequestOut.queue(any(ByteBuffer.class), eq(CtapHidFrameFactory.CTAPHID_BUFFER_SIZE))).thenAnswer( (Answer) invocation -> { state.inputAccumulator.write(invocation.getArgument(0).array()); return true; @@ -204,7 +204,7 @@ private void expect(int inputChannelId, int outputChannelId, byte cmdId, U2fComm UsbRequest usbRequestIn = mock(UsbRequest.class); when(usbRequestIn.initialize(usbConnection, usbIntIn)).thenReturn(true); - when(usbRequestIn.queue(any(ByteBuffer.class), eq(U2fHidFrameFactory.U2FHID_BUFFER_SIZE))).thenAnswer( + when(usbRequestIn.queue(any(ByteBuffer.class), eq(CtapHidFrameFactory.CTAPHID_BUFFER_SIZE))).thenAnswer( (Answer) invocation -> { if (!state.inputFinished) { state.inputFinished = true; @@ -215,10 +215,10 @@ private void expect(int inputChannelId, int outputChannelId, byte cmdId, U2fComm state.outputOffset = 0; } ByteBuffer buf = invocation.getArgument(0); - assertEquals(U2fHidFrameFactory.U2FHID_BUFFER_SIZE, buf.capacity()); + assertEquals(CtapHidFrameFactory.CTAPHID_BUFFER_SIZE, buf.capacity()); buf.clear(); - buf.put(state.output, state.outputOffset, U2fHidFrameFactory.U2FHID_BUFFER_SIZE); - state.outputOffset += U2fHidFrameFactory.U2FHID_BUFFER_SIZE; + buf.put(state.output, state.outputOffset, CtapHidFrameFactory.CTAPHID_BUFFER_SIZE); + state.outputOffset += CtapHidFrameFactory.CTAPHID_BUFFER_SIZE; return true; }); requestQueue.add(usbRequestIn); @@ -231,7 +231,7 @@ static class RequestState { int outputOffset; } - interface U2fCommunicationCallback { + interface CtapCommunicationCallback { byte[] communicate(byte[] payload); } } diff --git a/hwsecurity/core/src/test/java/de/cotech/hw/secrets/ByteSecretTest.java b/hwsecurity/core/src/test/java/de/cotech/hw/secrets/ByteSecretTest.java index 023206d..dd50e5d 100644 --- a/hwsecurity/core/src/test/java/de/cotech/hw/secrets/ByteSecretTest.java +++ b/hwsecurity/core/src/test/java/de/cotech/hw/secrets/ByteSecretTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/build.gradle b/hwsecurity/fido/build.gradle index 296a694..55fda7b 100644 --- a/hwsecurity/fido/build.gradle +++ b/hwsecurity/fido/build.gradle @@ -1,14 +1,14 @@ apply plugin: 'com.android.library' -apply plugin: 'digital.wup.android-maven-publish' +apply plugin: 'maven-publish' apply plugin: 'org.jetbrains.dokka-android' dependencies { api project(':hwsecurity:core') + api project(':hwsecurity:ui') - implementation 'com.google.android.material:material:1.0.0' - + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'androidx.appcompat:appcompat:1.0.2' api 'com.google.auto.value:auto-value-annotations:1.6.2' annotationProcessor 'com.google.auto.value:auto-value:1.6.2' @@ -26,7 +26,6 @@ android { minSdkVersion 14 versionName rootProject.ext.hwSdkVersionName vectorDrawables.useSupportLibrary = true - consumerProguardFiles 'hwsecurity-fido.pro' } compileOptions { @@ -40,59 +39,63 @@ android { } } -publishing { - publications { - mavenAar(MavenPublication) { - groupId = 'de.cotech' - artifactId = 'hwsecurity-fido' - version = android.defaultConfig.versionName +// https://developer.android.com/studio/build/maven-publish-plugin +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release - from components.android + groupId = 'de.cotech' + artifactId = 'hwsecurity-fido' + version = android.defaultConfig.versionName - pom { - url = 'https://hwsecurity.dev' - licenses { - license { - name = 'Commercial' - url = 'https://hwsecurity.dev/sales/' - distribution = 'repo' + pom { + url = 'https://hwsecurity.dev' + licenses { + license { + name = 'Commercial' + url = 'https://hwsecurity.dev/sales/' + distribution = 'repo' + } + license { + name = 'GNU General Public License, version 3' + url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + } } - license { - name = 'GNU General Public License, version 3' - url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + organization { + name = 'Confidential Technologies GmbH' + url = 'https://www.cotech.de' } } - organization { - name = 'Confidential Technologies GmbH' - url = 'https://www.cotech.de' - } } } - } - /* - * To upload release, create file gradle.properties in ~/.gradle/ with this content: - * - * cotechMavenName=xxx - * cotechMavenPassword=xxx - */ - if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { - println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" + /* + * To upload release, create file gradle.properties in ~/.gradle/ with this content: + * + * cotechMavenName=xxx + * cotechMavenPassword=xxx + */ + if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { + println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" - repositories { - maven { - credentials { - username cotechMavenName - password cotechMavenPassword + repositories { + maven { + credentials { + username cotechMavenName + password cotechMavenPassword + } + url = "https://maven.cotech.de" } - url = "https://maven.cotech.de" } } } } dokka { + moduleName = 'hwsecurity-fido' outputFormat = "hugo" - outputDirectory = "$buildDir/dokka/reference" + outputDirectory = "$projectDir/../../hwsecurity.dev/content/reference" sourceDirs = files('src/main/java') packageOptions { diff --git a/hwsecurity/fido/hwsecurity-fido.pro b/hwsecurity/fido/hwsecurity-fido.pro deleted file mode 100644 index 3e21df1..0000000 --- a/hwsecurity/fido/hwsecurity-fido.pro +++ /dev/null @@ -1,5 +0,0 @@ -# keep Javascript interfaces (WebViewFidoBridge) --keepclassmembers class * { - @android.webkit.JavascriptInterface ; -} --keepattributes JavascriptInterface diff --git a/hwsecurity/fido/src/main/assets/fidobridge.js b/hwsecurity/fido/src/main/assets/fidobridge.js index 707a472..ce77fce 100644 --- a/hwsecurity/fido/src/main/assets/fidobridge.js +++ b/hwsecurity/fido/src/main/assets/fidobridge.js @@ -72,11 +72,6 @@ function () { fidobridgejava.sign(jsonMessage); }; - // TODO: WebAuthn test -// navigator.credentials.create = function(options) { -// u2fbridge.register("TODO"); -// }; - console.log("fidobridge end execution"); } diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoAuthenticateCallback.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoAuthenticateCallback.java index d398614..95f492e 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoAuthenticateCallback.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoAuthenticateCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoAuthenticateRequest.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoAuthenticateRequest.java index 768a92c..5eb4ec4 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoAuthenticateRequest.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoAuthenticateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoAuthenticateResponse.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoAuthenticateResponse.java index 7172c59..d8defa2 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoAuthenticateResponse.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoAuthenticateResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoFacetIdUtil.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoFacetIdUtil.java index 652f4f9..d8b9cc8 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoFacetIdUtil.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoFacetIdUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoRegisterCallback.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoRegisterCallback.java index 0d3d06b..fec3cf7 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoRegisterCallback.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoRegisterCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoRegisterRequest.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoRegisterRequest.java index 2b48f5d..06b3c41 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoRegisterRequest.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoRegisterRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoRegisterResponse.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoRegisterResponse.java index fbedf84..541a2b8 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoRegisterResponse.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoRegisterResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoSecurityKey.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoSecurityKey.java index b136e25..7202e75 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoSecurityKey.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoSecurityKey.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoSecurityKeyConnectionMode.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoSecurityKeyConnectionMode.java index f0bd54a..97d2192 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoSecurityKeyConnectionMode.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/FidoSecurityKeyConnectionMode.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -52,7 +52,7 @@ public FidoSecurityKey establishSecurityKeyConnection(SecurityKeyManagerConfig c @Override protected boolean isRelevantTransport(Transport transport) { - return transport.getTransportType() == TransportType.USB_U2FHID + return transport.getTransportType() == TransportType.USB_CTAPHID // || transport.getTransportType() == TransportType.USB_CCID || transport.getTransportType() == TransportType.NFC; } diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/WebViewFidoBridge.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/WebViewFidoBridge.java index d9b97c3..129086e 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/WebViewFidoBridge.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/WebViewFidoBridge.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -35,7 +35,6 @@ import android.content.Context; import android.graphics.Bitmap; import android.net.Uri; -import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.Handler; import android.os.Parcelable; @@ -43,6 +42,7 @@ import android.webkit.WebResourceRequest; import android.webkit.WebView; +import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; @@ -64,6 +64,8 @@ import de.cotech.hw.fido.ui.FidoDialogOptions; import de.cotech.hw.util.HwTimber; +import de.cotech.hw.ui.R; + /** * If you are using a WebView for your login flow, you can use this WebViewFidoBridge @@ -124,17 +126,22 @@ private WebViewFidoBridge(Context context, FragmentManager fragmentManager, WebV } private void addJavascriptInterfaceToWebView() { - webView.addJavascriptInterface(new Object() { - @JavascriptInterface - public void register(String requestJson) { - handleRegisterRequest(requestJson); - } - - @JavascriptInterface - public void sign(String requestJson) { - handleSignRequest(requestJson); - } - }, FIDO_BRIDGE_INTERFACE); + webView.addJavascriptInterface(new JsInterface(), FIDO_BRIDGE_INTERFACE); + } + + @Keep + class JsInterface { + @Keep + @JavascriptInterface + public void register(String requestJson) { + handleRegisterRequest(requestJson); + } + + @Keep + @JavascriptInterface + public void sign(String requestJson) { + handleSignRequest(requestJson); + } } // region delegate @@ -185,11 +192,11 @@ private void injectOnInterceptRequest() { loadingNewPage = false; HwTimber.d("Scheduling fido bridge injection!"); Handler handler = new Handler(context.getMainLooper()); - handler.postAtFrontOfQueue(this::injectJavascriptFidoBridge); + handler.postAtFrontOfQueue(this::injectJavascriptBridge); } } - private void injectJavascriptFidoBridge() { + private void injectJavascriptBridge() { try { String jsContent = AndroidUtils.loadTextFromAssets(context, ASSETS_BRIDGE_JS, Charset.defaultCharset()); webView.evaluateJavascript("javascript:(" + jsContent + ")()", null); @@ -232,7 +239,7 @@ private void showRegisterFragment(RequestData requestData, String appId, String FidoDialogOptions.Builder opsBuilder = optionsBuilder != null ? optionsBuilder : FidoDialogOptions.builder(); opsBuilder.setTimeoutSeconds(timeoutSeconds); - opsBuilder.setTitle(context.getString(R.string.hwsecurity_title_default_register_app_id, getDisplayAppId(appId))); + opsBuilder.setTitle(context.getString(R.string.hwsecurity_fido_title_default_register_app_id, getDisplayAppId(appId))); FidoDialogFragment fidoDialogFragment = FidoDialogFragment.newInstance(registerRequest, opsBuilder.build()); fidoDialogFragment.setFidoRegisterCallback(fidoRegisterCallback); @@ -295,7 +302,7 @@ private void showSignFragment( FidoDialogOptions.Builder opsBuilder = optionsBuilder != null ? optionsBuilder : FidoDialogOptions.builder(); opsBuilder.setTimeoutSeconds(timeoutSeconds); - opsBuilder.setTitle(context.getString(R.string.hwsecurity_title_default_authenticate_app_id, getDisplayAppId(appId))); + opsBuilder.setTitle(context.getString(R.string.hwsecurity_fido_title_default_authenticate_app_id, getDisplayAppId(appId))); FidoDialogFragment fidoDialogFragment = FidoDialogFragment.newInstance(authenticateRequest, opsBuilder.build()); fidoDialogFragment.setFidoAuthenticateCallback(fidoAuthenticateCallback); diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/WebsafeBase64.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/WebsafeBase64.java index dada624..f42b0d7 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/WebsafeBase64.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/WebsafeBase64.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/exceptions/FidoPresenceRequiredException.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/exceptions/FidoPresenceRequiredException.java index cdcab1c..e1b92b7 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/exceptions/FidoPresenceRequiredException.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/exceptions/FidoPresenceRequiredException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/exceptions/FidoWrongKeyHandleException.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/exceptions/FidoWrongKeyHandleException.java index b3abb48..e1539ce 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/exceptions/FidoWrongKeyHandleException.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/exceptions/FidoWrongKeyHandleException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/FidoCommandApduDescriber.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/FidoCommandApduDescriber.java index 5ab2b3f..0b05949 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/FidoCommandApduDescriber.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/FidoCommandApduDescriber.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/FidoU2fAppletConnection.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/FidoU2fAppletConnection.java index f7fb72c..416fd7c 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/FidoU2fAppletConnection.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/FidoU2fAppletConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -34,6 +34,7 @@ import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; + import de.cotech.hw.SecurityKeyException; import de.cotech.hw.exceptions.*; import de.cotech.hw.fido.exceptions.FidoPresenceRequiredException; @@ -89,7 +90,7 @@ public void connectIfNecessary() throws IOException { private void connectToDevice() throws IOException { try { - if (transport.getTransportType() == TransportType.USB_U2FHID) { + if (transport.getTransportType() == TransportType.USB_CTAPHID) { HwTimber.d("Using USB U2F HID as a transport. No need to select AID."); byte[] versionBytes = readVersion(); checkVersionOrThrow(versionBytes); @@ -145,20 +146,6 @@ private byte[] selectFileOrFail(byte[] fileAid) throws IOException { // region communication - // ISO/IEC 7816-4 - private ResponseApdu communicate(CommandApdu commandApdu) throws IOException { - ResponseApdu lastResponse; - - lastResponse = sendWithChaining(commandApdu); - if (lastResponse.getSw1() == RESPONSE_SW1_INCORRECT_LENGTH && lastResponse.getSw2() != 0) { - commandApdu = commandApdu.withNe(lastResponse.getSw2()); - lastResponse = sendWithChaining(commandApdu); - } - lastResponse = readChainedResponseIfAvailable(lastResponse); - - return lastResponse; - } - // see "FIDO U2F Raw Message Formats", Section 3.3 Status Codes // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html public ResponseApdu communicateOrThrow(CommandApdu commandApdu) throws IOException { @@ -187,41 +174,54 @@ public ResponseApdu communicateOrThrow(CommandApdu commandApdu) throws IOExcepti } // ISO/IEC 7816-4 + private ResponseApdu communicate(CommandApdu commandApdu) throws IOException { + ResponseApdu lastResponse; + + lastResponse = sendWithChaining(commandApdu); + if (lastResponse.getSw1() == RESPONSE_SW1_INCORRECT_LENGTH && lastResponse.getSw2() != 0) { + commandApdu = commandApdu.withNe(lastResponse.getSw2()); + lastResponse = sendWithChaining(commandApdu); + } + lastResponse = readChainedResponseIfAvailable(lastResponse); + + return lastResponse; + } + @NonNull private ResponseApdu sendWithChaining(CommandApdu commandApdu) throws IOException { - /* We use an Lc of 65536 to enforce extended length APDUs for the register and authenticate commands. + /* U2F Spec: + * "If the request was encoded using extended length APDU encoding, + * the authenticator MUST respond using the extended length APDU response format." + * + * "If the request was encoded using short APDU encoding, + * the authenticator MUST respond using ISO 7816-4 APDU chaining." * - * This forces APDU case 4e in CommandApdu: - * apdu[apdu.length - 2] = 0; - * apdu[apdu.length - 1] = 0; + * https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-nfc-protocol-v1.2-ps-20170411.html * - * see also: - * https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-hid-protocol-v1.2-ps-20170411.html - * https://docs.oracle.com/javacard/3.0.5/prognotes/extended_apdu_format.htm + * In the best case, extended length is supported by device and authenticator, so we don't need to + * parse chained APDUs coming from the authenticator. * - * Note: * We do *not* check for `transport.isExtendedLengthSupported()` here! There are phones (including - * the Nexus 5X) that return "false" to this, but some Security Keys (like Yubikey Neo) still + * the Nexus 5X, Nexus 6P) that return "false" to this, but some Security Keys (like Yubikey Neo) still * require us to send extended APDUs. So what we do is, send an extended APDU, and if that doesn't * work, fall back to a short one. */ - if (commandFactory.isSuitableForExtendedApdu(commandApdu)) { - CommandApdu extendedLengthApdu = commandApdu.withNe(65536); - ResponseApdu response = transport.transceive(extendedLengthApdu); - if (response.getSw() != WrongRequestLengthException.SW_WRONG_REQUEST_LENGTH) { - return response; - } else { - HwTimber.d("Received WRONG_REQUEST_LENGTH error. Retrying with compatibility workaround"); - } + if (!transport.isExtendedLengthSupported()) { + HwTimber.w("Transport protocol does not support extended length. Probably an old device with NFC, such as Nexus 5X, Nexus 6P. We still try sending extended length!"); } - if (commandFactory.isSuitableForShortApdu(commandApdu)) { - CommandApdu shortApdu = commandFactory.createShortApdu(commandApdu); - return transport.transceive(shortApdu); + ResponseApdu response = transport.transceive(commandApdu.withExtendedApduNe()); + if (response.getSw() != WrongRequestLengthException.SW_WRONG_REQUEST_LENGTH) { + return response; + } else { + HwTimber.d("Received WRONG_REQUEST_LENGTH error. Retrying with short APDU Ne."); } - ResponseApdu lastResponse = null; + if (commandFactory.isSuitableForSingleShortApdu(commandApdu)) { + return transport.transceive(commandApdu.withShortApduNe()); + } + ResponseApdu lastResponse = null; List chainedApdus = commandFactory.createChainedApdus(commandApdu); for (int i = 0, totalCommands = chainedApdus.size(); i < totalCommands; i++) { CommandApdu chainedApdu = chainedApdus.get(i); diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/FidoU2fCommandApduFactory.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/FidoU2fCommandApduFactory.java index b2a2995..648a554 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/FidoU2fCommandApduFactory.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/FidoU2fCommandApduFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -31,6 +31,7 @@ import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; + import de.cotech.hw.internal.iso7816.CommandApdu; @@ -42,12 +43,6 @@ public class FidoU2fCommandApduFactory { private static final FidoCommandApduDescriber DESCRIBER = new FidoCommandApduDescriber(); - // ISO/IEC 7816-4 - private static final int MAX_APDU_NC = 255; - private static final int MAX_APDU_NC_EXT = 65535; - private static final int MAX_APDU_NE = 256; - private static final int MAX_APDU_NE_EXT = 65536; - private static final int MASK_CLA_CHAINING = 1 << 4; private static final int CLA = 0x00; @@ -85,9 +80,10 @@ public CommandApdu createVersionCommand() { } // ISO/IEC 7816-4 + // SELECT command always as short APDU @NonNull public CommandApdu createSelectFileCommand(byte[] fileAid) { - return CommandApdu.create(CLA, INS_SELECT_FILE, P1_SELECT_FILE, P2_EMPTY, fileAid, MAX_APDU_NE).withDescriber(DESCRIBER); + return CommandApdu.create(CLA, INS_SELECT_FILE, P1_SELECT_FILE, P2_EMPTY, fileAid, CommandApdu.MAX_APDU_NE_SHORT).withDescriber(DESCRIBER); } // GET RESPONSE ISO/IEC 7816-4 par.7.6.1 @@ -96,13 +92,6 @@ public CommandApdu createGetResponseCommand(int lastResponseSw2) { return CommandApdu.create(CLA, INS_GET_RESPONSE, P1_EMPTY, P2_EMPTY, lastResponseSw2).withDescriber(DESCRIBER); } - // ISO/IEC 7816-4 - @NonNull - public CommandApdu createShortApdu(CommandApdu apdu) { - int ne = Math.min(apdu.getNe(), MAX_APDU_NE); - return CommandApdu.create(apdu.getCLA(), apdu.getINS(), apdu.getP1(), apdu.getP2(), apdu.getData(), ne).withDescriber(DESCRIBER); - } - // ISO/IEC 7816-4 @NonNull public List createChainedApdus(CommandApdu apdu) { @@ -111,13 +100,14 @@ public List createChainedApdus(CommandApdu apdu) { int offset = 0; byte[] data = apdu.getData(); while (offset < data.length) { - int curLen = Math.min(MAX_APDU_NC, data.length - offset); + int curLen = Math.min(CommandApdu.MAX_APDU_NC_SHORT, data.length - offset); boolean last = offset + curLen >= data.length; int cla = apdu.getCLA() + (last ? 0 : MASK_CLA_CHAINING); CommandApdu cmd; if (last) { - int ne = Math.min(apdu.getNe(), MAX_APDU_NE); + // TODO: Check this! + int ne = Math.min(apdu.getNe(), CommandApdu.MAX_APDU_NE_SHORT); cmd = CommandApdu.create(cla, apdu.getINS(), apdu.getP1(), apdu.getP2(), data, offset, curLen, ne, DESCRIBER); } else { cmd = CommandApdu.create(cla, apdu.getINS(), apdu.getP1(), apdu.getP2(), data, offset, curLen, 0, DESCRIBER); @@ -130,11 +120,8 @@ public List createChainedApdus(CommandApdu apdu) { return result; } - public boolean isSuitableForShortApdu(CommandApdu apdu) { - return apdu.getData().length <= MAX_APDU_NC; + public boolean isSuitableForSingleShortApdu(CommandApdu apdu) { + return apdu.getNc() <= CommandApdu.MAX_APDU_NC_SHORT; } - public boolean isSuitableForExtendedApdu(CommandApdu commandApdu) { - return commandApdu.getCLA() == CLA && commandApdu.getINS() != INS_SELECT_FILE; - } } diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoAsyncOperationManager.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoAsyncOperationManager.java index 0f48dc5..919f569 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoAsyncOperationManager.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoAsyncOperationManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoAuthenticateOperationThread.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoAuthenticateOperationThread.java index 7a86251..c8c22ae 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoAuthenticateOperationThread.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoAuthenticateOperationThread.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoOperationThread.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoOperationThread.java index c83ffd3..27b4045 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoOperationThread.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoOperationThread.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -37,7 +37,7 @@ import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.OnLifecycleEvent; -import de.cotech.hw.exceptions.SecurityKeyLostException; +import de.cotech.hw.exceptions.SecurityKeyDisconnectedException; import de.cotech.hw.fido.exceptions.FidoPresenceRequiredException; import de.cotech.hw.fido.internal.FidoU2fAppletConnection; import de.cotech.hw.util.HwTimber; @@ -85,7 +85,7 @@ public void run() { } catch (InterruptedException e) { HwTimber.e("Fido operation was interrupted"); break; - } catch (SecurityKeyLostException e) { + } catch (SecurityKeyDisconnectedException e) { HwTimber.e("Transport gone during fido operation"); break; } catch (FidoPresenceRequiredException e) { diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoRegisterOperationThread.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoRegisterOperationThread.java index 395fc23..9489ab5 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoRegisterOperationThread.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/async/FidoRegisterOperationThread.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fApiUtils.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fApiUtils.java index 762d700..23c96ad 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fApiUtils.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fApiUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fAuthenticateRequest.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fAuthenticateRequest.java index ef16337..926bd8a 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fAuthenticateRequest.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fAuthenticateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fJsonParser.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fJsonParser.java index 97daca2..8309f6b 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fJsonParser.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fJsonParser.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fJsonSerializer.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fJsonSerializer.java index d41387e..eb94dc5 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fJsonSerializer.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fJsonSerializer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fRegisterRequest.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fRegisterRequest.java index 4a04fe5..c7c3328 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fRegisterRequest.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fRegisterRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fRequest.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fRequest.java index 12eea78..5c01a58 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fRequest.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fResponse.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fResponse.java index 00365ac..5cce78c 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fResponse.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/jsapi/U2fResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/operations/AuthenticateOp.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/operations/AuthenticateOp.java index 8d3e704..e86f704 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/operations/AuthenticateOp.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/operations/AuthenticateOp.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/operations/RegisterOp.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/operations/RegisterOp.java index 91d09d2..61d4d80 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/operations/RegisterOp.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/operations/RegisterOp.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/utils/AndroidUtils.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/utils/AndroidUtils.java index 40227a8..dfe6f0d 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/utils/AndroidUtils.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/utils/AndroidUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/utils/AnimatedVectorDrawableHelper.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/utils/AnimatedVectorDrawableHelper.java index b36029a..e51fd3e 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/utils/AnimatedVectorDrawableHelper.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/internal/utils/AnimatedVectorDrawableHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/ui/FidoDialogFragment.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/ui/FidoDialogFragment.java index 62e0f7b..bb01aae 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/ui/FidoDialogFragment.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/ui/FidoDialogFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -25,68 +25,42 @@ package de.cotech.hw.fido.ui; -import android.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ArgbEvaluator; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; import android.content.pm.ActivityInfo; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.provider.Settings; -import android.util.DisplayMetrics; -import android.util.Pair; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.Window; import android.view.WindowManager; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.widget.Button; import android.widget.FrameLayout; -import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.AttrRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.annotation.UiThread; import androidx.appcompat.app.AppCompatDelegate; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.Guideline; import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.view.ViewCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; -import androidx.transition.AutoTransition; -import androidx.transition.Scene; -import androidx.transition.Transition; import androidx.transition.TransitionManager; -import androidx.vectordrawable.graphics.drawable.Animatable2Compat; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.button.MaterialButton; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; import de.cotech.hw.SecurityKeyCallback; import de.cotech.hw.SecurityKeyException; import de.cotech.hw.SecurityKeyManager; -import de.cotech.hw.exceptions.SecurityKeyLostException; +import de.cotech.hw.exceptions.SecurityKeyDisconnectedException; import de.cotech.hw.fido.FidoAuthenticateCallback; import de.cotech.hw.fido.FidoAuthenticateRequest; import de.cotech.hw.fido.FidoAuthenticateResponse; @@ -95,21 +69,22 @@ import de.cotech.hw.fido.FidoRegisterResponse; import de.cotech.hw.fido.FidoSecurityKey; import de.cotech.hw.fido.FidoSecurityKeyConnectionMode; -import de.cotech.hw.fido.R; +import de.cotech.hw.ui.R; import de.cotech.hw.fido.exceptions.FidoWrongKeyHandleException; -import de.cotech.hw.fido.internal.utils.AnimatedVectorDrawableHelper; -import de.cotech.hw.internal.NfcSweetspotData; -import de.cotech.hw.util.NfcStatusObserver; +import de.cotech.hw.ui.internal.ErrorView; +import de.cotech.hw.ui.internal.NfcFullscreenView; +import de.cotech.hw.ui.internal.SecurityKeyFormFactor; +import de.cotech.hw.ui.internal.SmartcardFormFactor; import de.cotech.hw.util.HwTimber; -public class FidoDialogFragment extends BottomSheetDialogFragment implements SecurityKeyCallback { +public class FidoDialogFragment extends BottomSheetDialogFragment implements SecurityKeyCallback, SecurityKeyFormFactor.SelectTransportCallback { private static final String FRAGMENT_TAG = "hwsecurity-fido-fragment"; private static final String ARG_FIDO_REGISTER_REQUEST = "ARG_FIDO_REGISTER_REQUEST"; private static final String ARG_FIDO_AUTHENTICATE_REQUEST = "ARG_FIDO_AUTHENTICATE_REQUEST"; private static final String ARG_FIDO_OPTIONS = "de.cotech.hw.fido.ARG_FIDO_OPTIONS"; - private static final long TIME_DELAYED_STATE_CHANGE = 3000; + private static final long TIME_DELAYED_SCREEN_CHANGE = 3000; static { AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); @@ -123,45 +98,34 @@ public class FidoDialogFragment extends BottomSheetDialogFragment implements Sec private ConstraintLayout innerBottomSheet; private Guideline guidelineForceHeight; - private Button buttonCancel; + private MaterialButton buttonLeft; + private MaterialButton buttonRight; private TextView textTitle; private TextView textDescription; - private TextView textNfc; - private TextView textUsb; - private ImageView imageNfc; - private ImageView imageNfcFullscreen; - private ImageView imageUsb; - private TextView textViewNfcDisabled; - private Button buttonNfcDisabled; + private SecurityKeyFormFactor securityKeyFormFactor; + private SmartcardFormFactor smartcardFormFactor; - private TextView textError; - private ImageView imageError; - - private ImageView sweetspotIndicator; - private TextView textNfcFullscreen; - - private NfcStatusObserver nfcStatusObserver; + private ErrorView errorView; private FidoDialogOptions options; private FidoRegisterRequest fidoRegisterRequest; private FidoAuthenticateRequest fidoAuthenticateRequest; - private NfcSweetspotData nfcSweetspotData; + private NfcFullscreenView nfcFullscreenView; - private enum State { + private enum Screen { START, NFC_FULLSCREEN, - NFC_SWEETSPOT, USB_INSERT, USB_PRESS_BUTTON, USB_SELECT_AND_PRESS_BUTTON, ERROR, } - private State currentState; - private State stateBeforeError; + private Screen currentScreen; + private Screen screenBeforeError; public void setFidoRegisterCallback(OnFidoRegisterCallback fidoRegisterCallback) { this.fidoRegisterCallback = fidoRegisterCallback; @@ -293,15 +257,13 @@ public void onCreate(@Nullable Bundle savedInstanceState) { "or FidoDialogFragment.OnFidoRegisterCallback!"); } - nfcSweetspotData = NfcSweetspotData.getInstance(getContext()); - SecurityKeyManager.getInstance().registerCallback(new FidoSecurityKeyConnectionMode(), this, this); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.hwsecurity_fido_bottomsheet, container, false); + return inflater.inflate(R.layout.hwsecurity_security_key_dialog, container, false); } @Override @@ -325,20 +287,20 @@ private void initTimeout(long timeoutSeconds) { handler.postDelayed(() -> { HwTimber.d("Timeout after %s seconds.", timeoutSeconds); - textError.setText(R.string.hwsecurity_error_timeout); - gotoState(State.ERROR); + errorView.setText(R.string.hwsecurity_fido_error_timeout); + gotoScreen(Screen.ERROR); bottomSheet.postDelayed(() -> { if (!isAdded()) { return; } - dismiss(); + dismissAllowingStateLoss(); if (fidoAuthenticateRequest != null) { fidoAuthenticateCallback.onFidoAuthenticateTimeout(fidoAuthenticateRequest); } else if (fidoRegisterRequest != null) { fidoRegisterCallback.onFidoRegisterTimeout(fidoRegisterRequest); } - }, TIME_DELAYED_STATE_CHANGE); + }, TIME_DELAYED_SCREEN_CHANGE); }, timeoutSeconds * 1000); } @@ -371,69 +333,24 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat initTimeout(options.getTimeoutSeconds()); } - innerBottomSheet = view.findViewById(R.id.hwSecurityFidoBottomSheet); + innerBottomSheet = view.findViewById(R.id.hwSecurityDialogBottomSheet); guidelineForceHeight = view.findViewById(R.id.guidelineForceHeight); - buttonCancel = view.findViewById(R.id.buttonCancel); + buttonLeft = view.findViewById(R.id.buttonLeft); + buttonRight = view.findViewById(R.id.buttonRight); textTitle = view.findViewById(R.id.textTitle); textDescription = view.findViewById(R.id.textDescription); - textNfc = view.findViewById(R.id.textNfc); - textNfcFullscreen = view.findViewById(R.id.textNfcFullscreen); - textUsb = view.findViewById(R.id.textUsb); - imageNfc = view.findViewById(R.id.imageNfc); - imageNfcFullscreen = view.findViewById(R.id.imageNfcFullscreen); - sweetspotIndicator = view.findViewById(R.id.imageNfcSweetspot); - imageUsb = view.findViewById(R.id.imageUsb); - imageError = view.findViewById(R.id.imageError); - textError = view.findViewById(R.id.textError); - textViewNfcDisabled = view.findViewById(R.id.textNfcDisabled); - buttonNfcDisabled = view.findViewById(R.id.buttonNfcDisabled); - - nfcStatusObserver = new NfcStatusObserver(getContext(), this, this::showOrHideNfcDisabledView); - - buttonCancel.setOnClickListener(v -> getDialog().cancel()); - } - @Override - public void onResume() { - super.onResume(); - if (currentState == State.START) { - // re-check NFC status, maybe user is coming back from settings - showOrHideNfcView(); - } - if (currentState == null || - currentState == State.USB_INSERT || - currentState == State.USB_PRESS_BUTTON || - currentState == State.USB_SELECT_AND_PRESS_BUTTON) { - gotoState(State.START); - } - } + smartcardFormFactor = new SmartcardFormFactor(view.findViewById(R.id.includeSmartcardFormFactor), this); + securityKeyFormFactor = new SecurityKeyFormFactor(view.findViewById(R.id.includeSecurityKeyFormFactor), this, this, innerBottomSheet, options.getShowSdkLogo()); - private void showOrHideNfcView() { - boolean isNfcHardwareAvailable = SecurityKeyManager.getInstance().isNfcHardwareAvailable(); - textNfc.setVisibility(isNfcHardwareAvailable ? View.VISIBLE : View.GONE); - imageNfc.setVisibility(isNfcHardwareAvailable ? View.VISIBLE : View.GONE); + errorView = new ErrorView(view.findViewById(de.cotech.hw.ui.R.id.includeError)); - if (isNfcHardwareAvailable) { - boolean nfcEnabled = nfcStatusObserver.isNfcEnabled(); - showOrHideNfcDisabledView(nfcEnabled); - } - } + nfcFullscreenView = new NfcFullscreenView(view.findViewById(de.cotech.hw.ui.R.id.includeNfcFullscreen), innerBottomSheet); - private void showOrHideNfcDisabledView(boolean nfcEnabled) { - textViewNfcDisabled.setVisibility(nfcEnabled ? View.GONE : View.VISIBLE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - buttonNfcDisabled.setOnClickListener(v -> startAndroidNfcConfigActivityWithHint()); - buttonNfcDisabled.setVisibility(nfcEnabled ? View.GONE : View.VISIBLE); - } - textNfc.setVisibility(nfcEnabled ? View.VISIBLE : View.INVISIBLE); - imageNfc.setVisibility(nfcEnabled ? View.VISIBLE : View.INVISIBLE); - } + buttonRight.setVisibility(View.INVISIBLE); + buttonLeft.setOnClickListener(v -> getDialog().cancel()); - @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) - private void startAndroidNfcConfigActivityWithHint() { - Toast.makeText(getContext().getApplicationContext(), - R.string.hwsecurity_nfc_settings_toast, Toast.LENGTH_SHORT).show(); - startActivity(new Intent(Settings.ACTION_NFC_SETTINGS)); + gotoScreen(Screen.START); } @Override @@ -450,416 +367,108 @@ private String getStartTitle() { if (options.getTitle() != null) { return options.getTitle(); } else if (fidoRegisterRequest != null) { - return getResources().getString(R.string.hwsecurity_title_default_register); + return getResources().getString(R.string.hwsecurity_fido_title_default_register); } else { - return getResources().getString(R.string.hwsecurity_title_default_authenticate); + return getResources().getString(R.string.hwsecurity_fido_title_default_authenticate); } } - private void gotoState(State newState) { - switch (newState) { + private void gotoScreen(Screen newScreen) { + switch (newScreen) { case START: { - imageNfc.setOnClickListener(v -> gotoState(State.NFC_FULLSCREEN)); - imageUsb.setOnClickListener(v -> gotoState(State.USB_INSERT)); animateStart(); break; } case NFC_FULLSCREEN: { - removeOnClickListener(); - animateSelectNfc(); - break; - } - case NFC_SWEETSPOT: { - removeOnClickListener(); - showNfcSweetSpot(); + securityKeyFormFactor.animateSelectNfc(); break; } case USB_INSERT: { - removeOnClickListener(); - animateSelectUsb(); + securityKeyFormFactor.animateSelectUsb(); break; } case USB_PRESS_BUTTON: { - removeOnClickListener(); - animateUsbPressButton(); + securityKeyFormFactor.animateUsbPressButton(); break; } case USB_SELECT_AND_PRESS_BUTTON: { - removeOnClickListener(); - animateSelectUsbAndPressButton(); + securityKeyFormFactor.animateSelectUsbAndPressButton(); break; } case ERROR: { - removeOnClickListener(); animateError(); break; } } - currentState = newState; - } - - private void removeOnClickListener() { - imageNfc.setOnClickListener(null); - imageUsb.setOnClickListener(null); + currentScreen = newScreen; } - private void animateStart() { - imageNfc.setImageResource(R.drawable.hwsecurity_nfc_start); - imageUsb.setImageResource(R.drawable.hwsecurity_usb_start); - buttonCancel.setText(R.string.hwsecurity_cancel); - textTitle.setText(getStartTitle()); - textDescription.setText(R.string.hwsecurity_description_start); - - AutoTransition selectModeTransition = new AutoTransition(); - selectModeTransition.setDuration(150); - - TransitionManager.go(new Scene(innerBottomSheet), selectModeTransition); - showOrHideNfcView(); - imageUsb.setVisibility(View.VISIBLE); - textUsb.setVisibility(View.VISIBLE); - textTitle.setVisibility(View.VISIBLE); - textDescription.setVisibility(View.VISIBLE); - textError.setVisibility(View.GONE); - imageError.setVisibility(View.GONE); - } - - private void animateSelectNfc() { - AutoTransition selectModeTransition = new AutoTransition(); - selectModeTransition.setDuration(150); - selectModeTransition.addListener(new Transition.TransitionListener() { - @Override - public void onTransitionStart(@NonNull Transition transition) { - } - - @Override - public void onTransitionEnd(@NonNull Transition transition) { - animateNfcFullscreen(); - } - - @Override - public void onTransitionCancel(@NonNull Transition transition) { - } - - @Override - public void onTransitionPause(@NonNull Transition transition) { - } - - @Override - public void onTransitionResume(@NonNull Transition transition) { - } - }); - - TransitionManager.go(new Scene(innerBottomSheet), selectModeTransition); - imageUsb.setVisibility(View.GONE); - imageNfc.setVisibility(View.GONE); - textNfc.setVisibility(View.GONE); - textViewNfcDisabled.setVisibility(View.GONE); - buttonNfcDisabled.setVisibility(View.GONE); - textUsb.setVisibility(View.GONE); + @Override + public void screeFullscreenNfc() { textTitle.setVisibility(View.GONE); textDescription.setVisibility(View.GONE); - textError.setVisibility(View.GONE); - imageError.setVisibility(View.GONE); - } - - private void animateNfcFullscreen() { - ValueAnimator bottomSheetFullscreenAnimator = ValueAnimator - .ofInt(bottomSheet.getHeight(), coordinator.getHeight()) - .setDuration(250); - - bottomSheetFullscreenAnimator.addUpdateListener(animation -> { - bottomSheet.getLayoutParams().height = (int) animation.getAnimatedValue(); - bottomSheet.requestLayout(); - guidelineForceHeight.setGuidelineEnd((int) animation.getAnimatedValue() - 100); - guidelineForceHeight.requestLayout(); - }); - - int colorFrom = getResources().getColor(R.color.hwSecurityWhite); - int colorTo = resolveColorFromAttr(R.attr.hwSecuritySurfaceColor); - ValueAnimator colorChange = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); - colorChange.setDuration(100); - colorChange.addUpdateListener(animator -> { - innerBottomSheet.setBackgroundColor((int) animator.getAnimatedValue()); - }); - - ObjectAnimator fadeInImageNfcFullscreen = ObjectAnimator - .ofFloat(imageNfcFullscreen, View.ALPHA, 0, 1) - .setDuration(150); - fadeInImageNfcFullscreen.setStartDelay(50); - fadeInImageNfcFullscreen.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - imageNfcFullscreen.setVisibility(View.VISIBLE); - } - - @Override - public void onAnimationEnd(Animator animation) { - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - }); - - bottomSheetFullscreenAnimator.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - } - - @Override - public void onAnimationEnd(Animator animation) { - animateNfcFinal(); - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - }); - - List items = new ArrayList<>(); - items.add(bottomSheetFullscreenAnimator); - items.add(fadeInImageNfcFullscreen); - items.add(colorChange); - - AnimatorSet set = new AnimatorSet(); - set.playTogether(items); - set.setInterpolator(new AccelerateDecelerateInterpolator()); - set.start(); - } - - private void animateNfcFinal() { - textNfcFullscreen.setText(R.string.hwsecurity_title_nfc_fullscreen); - textNfcFullscreen.setVisibility(View.VISIBLE); - - Animatable2Compat.AnimationCallback animationCallback = new Animatable2Compat.AnimationCallback() { - @Override - public void onAnimationEnd(Drawable drawable) { - if (!ViewCompat.isAttachedToWindow(imageNfcFullscreen)) { - return; - } - - fadeToNfcSweetSpot(); - } - }; - - AnimatedVectorDrawableHelper.startAnimation(imageNfcFullscreen, R.drawable.hwsecurity_nfc_handling, animationCallback); - } - - private void fadeToNfcSweetSpot() { - Pair nfcPosition = nfcSweetspotData.getSweetspotForBuildModel(); - if (nfcPosition == null) { - HwTimber.d("No NFC sweetspot data available for this model."); - return; - } - - int colorFrom = resolveColorFromAttr(R.attr.hwSecuritySurfaceColor); - int colorTo = getResources().getColor(R.color.hwSecurityWhite); - ValueAnimator colorChange = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); - colorChange.setDuration(150); - colorChange.addUpdateListener(animator -> { - innerBottomSheet.setBackgroundColor((int) animator.getAnimatedValue()); - }); - - ObjectAnimator fadeOutImageNfcFullscreen = ObjectAnimator - .ofFloat(imageNfcFullscreen, "alpha", 1, 0) - .setDuration(150); - - fadeOutImageNfcFullscreen.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - } - - @Override - public void onAnimationEnd(Animator animation) { - gotoState(State.NFC_SWEETSPOT); - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - }); - - List items = new ArrayList<>(); - items.add(colorChange); - items.add(fadeOutImageNfcFullscreen); + errorView.setVisibility(View.GONE); + smartcardFormFactor.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.GONE); - AnimatorSet set = new AnimatorSet(); - set.playTogether(items); - set.start(); + nfcFullscreenView.setVisibility(View.VISIBLE); + nfcFullscreenView.animateNfcFullscreen(getDialog()); } - private void showNfcSweetSpot() { - Pair nfcPosition = nfcSweetspotData.getSweetspotForBuildModel(); - - Dialog dialog = getDialog(); - if (dialog == null) { - return; - } - - DisplayMetrics metrics = new DisplayMetrics(); - dialog.getWindow().getWindowManager().getDefaultDisplay().getMetrics(metrics); - - float statusBarHeight = getStatusbarHeight(dialog.getWindow()); - - final float translationX = (float) (metrics.widthPixels * nfcPosition.first); - final float translationY = (float) (metrics.heightPixels * nfcPosition.second) + statusBarHeight; - - sweetspotIndicator.post(() -> { - sweetspotIndicator.setTranslationX(translationX - sweetspotIndicator.getWidth() / 2); - sweetspotIndicator.setTranslationY(translationY - sweetspotIndicator.getHeight() / 2); - - TransitionManager.beginDelayedTransition(innerBottomSheet); - sweetspotIndicator.setVisibility(View.VISIBLE); - textNfcFullscreen.setVisibility(View.VISIBLE); - imageNfcFullscreen.setVisibility(View.GONE); - imageError.setVisibility(View.GONE); - textError.setVisibility(View.GONE); - }); - - AnimatedVectorDrawableHelper.startAndLoopAnimation(sweetspotIndicator, R.drawable.hwsecurity_nfc_sweet_spot_a); - } - - private static int getStatusbarHeight(Window window) { - Rect rectangle = new Rect(); - window.getDecorView().getWindowVisibleDisplayFrame(rectangle); - int statusBarHeight = rectangle.top; - int contentViewTop = window.findViewById(Window.ID_ANDROID_CONTENT).getTop(); - return contentViewTop - statusBarHeight; - } - - private void animateSelectUsb() { - AutoTransition selectModeTransition = new AutoTransition(); - selectModeTransition.setDuration(150); - selectModeTransition.addListener(new Transition.TransitionListener() { - @Override - public void onTransitionStart(@NonNull Transition transition) { - } - - @Override - public void onTransitionEnd(@NonNull Transition transition) { - AnimatedVectorDrawableHelper.startAndLoopAnimation(imageUsb, R.drawable.hwsecurity_usb_handling_a); - } - - @Override - public void onTransitionCancel(@NonNull Transition transition) { - } - - @Override - public void onTransitionPause(@NonNull Transition transition) { - } - - @Override - public void onTransitionResume(@NonNull Transition transition) { - } - }); - - TransitionManager.go(new Scene(innerBottomSheet), selectModeTransition); - textTitle.setText(R.string.hwsecurity_title_usb_selected); - imageNfc.setVisibility(View.GONE); - textViewNfcDisabled.setVisibility(View.GONE); - buttonNfcDisabled.setVisibility(View.GONE); - textDescription.setVisibility(View.GONE); - textNfc.setVisibility(View.GONE); - textUsb.setVisibility(View.GONE); - textError.setVisibility(View.GONE); - imageError.setVisibility(View.GONE); + @Override + public void onSecurityKeyFormFactorClickUsb() { + currentScreen = Screen.USB_INSERT; } - private void animateSelectUsbAndPressButton() { - AutoTransition selectModeTransition = new AutoTransition(); - selectModeTransition.setDuration(150); - selectModeTransition.addListener(new Transition.TransitionListener() { - @Override - public void onTransitionStart(@NonNull Transition transition) { - } - - @Override - public void onTransitionEnd(@NonNull Transition transition) { - AnimatedVectorDrawableHelper.startAndLoopAnimation(imageUsb, R.drawable.hwsecurity_usb_handling_b); - } - - @Override - public void onTransitionCancel(@NonNull Transition transition) { - } - - @Override - public void onTransitionPause(@NonNull Transition transition) { - } - - @Override - public void onTransitionResume(@NonNull Transition transition) { - } - }); + private void animateStart() { + buttonLeft.setText(R.string.hwsecurity_ui_button_cancel); + textTitle.setText(getStartTitle()); + textDescription.setText(R.string.hwsecurity_ui_description_start); - TransitionManager.go(new Scene(innerBottomSheet), selectModeTransition); - textTitle.setText(R.string.hwsecurity_title_usb_button); - imageNfc.setVisibility(View.GONE); - textViewNfcDisabled.setVisibility(View.GONE); - buttonNfcDisabled.setVisibility(View.GONE); - textDescription.setVisibility(View.GONE); - textNfc.setVisibility(View.GONE); - textUsb.setVisibility(View.GONE); - textError.setVisibility(View.GONE); - imageError.setVisibility(View.GONE); - } + textTitle.setVisibility(View.VISIBLE); + textDescription.setVisibility(View.VISIBLE); + errorView.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.VISIBLE); - private void animateUsbPressButton() { - TransitionManager.beginDelayedTransition(innerBottomSheet); - textTitle.setText(R.string.hwsecurity_title_usb_button); - AnimatedVectorDrawableHelper.startAndLoopAnimation(imageUsb, R.drawable.hwsecurity_usb_handling_b); + securityKeyFormFactor.resetAnimation(); } private void animateError() { TransitionManager.beginDelayedTransition(innerBottomSheet); textTitle.setVisibility(View.GONE); textDescription.setVisibility(View.GONE); - imageNfc.setVisibility(View.GONE); - textViewNfcDisabled.setVisibility(View.GONE); - buttonNfcDisabled.setVisibility(View.GONE); - imageUsb.setVisibility(View.GONE); - textNfc.setVisibility(View.GONE); - textUsb.setVisibility(View.GONE); - textNfcFullscreen.setVisibility(View.GONE); - imageNfcFullscreen.setVisibility(View.GONE); - textError.setVisibility(View.VISIBLE); - imageError.setVisibility(View.VISIBLE); - - AnimatedVectorDrawableHelper.startAnimation(imageError, R.drawable.hwsecurity_error); + securityKeyFormFactor.setVisibility(View.GONE); + smartcardFormFactor.setVisibility(View.GONE); + nfcFullscreenView.setVisibility(View.GONE); + errorView.setVisibility(View.VISIBLE); } @UiThread @Override public void onSecurityKeyDiscovered(@NonNull FidoSecurityKey securityKey) { - switch (currentState) { + switch (currentScreen) { case START: { if (securityKey.isTransportUsb()) { - gotoState(State.USB_SELECT_AND_PRESS_BUTTON); + gotoScreen(Screen.USB_SELECT_AND_PRESS_BUTTON); } + sendFidoCommand(securityKey); break; } case USB_INSERT: { if (securityKey.isTransportUsb()) { - gotoState(State.USB_PRESS_BUTTON); + gotoScreen(Screen.USB_PRESS_BUTTON); } + sendFidoCommand(securityKey); break; } default: { - HwTimber.d("onSecurityKeyDiscovered unhandled state: %s", currentState.name()); + HwTimber.d("onSecurityKeyDiscovered unhandled screen: %s", currentScreen.name()); } } + } + + private void sendFidoCommand(@NonNull FidoSecurityKey securityKey) { if (fidoRegisterRequest != null) { securityKey.registerAsync(fidoRegisterRequest, new FidoRegisterCallback() { @@ -903,12 +512,12 @@ public void onIoException(IOException e) { public void onSecurityKeyDisconnected(@NonNull FidoSecurityKey securityKey) { HwTimber.d("onSecurityKeyDisconnected"); - switch (currentState) { + switch (currentScreen) { case USB_PRESS_BUTTON: case USB_SELECT_AND_PRESS_BUTTON: - gotoState(State.START); + gotoScreen(Screen.START); default: - HwTimber.d("onSecurityKeyDisconnected unhandled state: %s", currentState.name()); + HwTimber.d("onSecurityKeyDisconnected unhandled screen: %s", currentScreen.name()); } } @@ -920,43 +529,36 @@ public void onSecurityKeyDiscoveryFailed(@NonNull IOException exception) { private void handleError(IOException exception) { HwTimber.d(exception); - if (currentState == State.ERROR) { - // keep stateBeforeError - } else if (currentState == State.NFC_FULLSCREEN || currentState == State.NFC_SWEETSPOT) { - stateBeforeError = State.NFC_SWEETSPOT; + if (currentScreen == Screen.ERROR) { + // keep current screenBeforeError + } else if (currentScreen == Screen.NFC_FULLSCREEN) { +// screenBeforeError = Screen.NFC_SWEETSPOT; } else { - stateBeforeError = State.START; + screenBeforeError = Screen.START; } try { throw exception; } catch (FidoWrongKeyHandleException e) { - showError(getString(R.string.hwsecurity_error_wrong_key_handle)); + showError(getString(R.string.hwsecurity_fido_error_wrong_security_key)); } catch (SecurityKeyException e) { - showError(getString(R.string.hwsecurity_error_internal, e.getShortErrorName())); - } catch (SecurityKeyLostException e) { + showError(getString(R.string.hwsecurity_fido_error_internal, e.getShortErrorName())); + } catch (SecurityKeyDisconnectedException e) { // not handled } catch (IOException e) { - showError(getString(R.string.hwsecurity_error_internal, e.getMessage())); + showError(getString(R.string.hwsecurity_fido_error_internal, e.getMessage())); } } private void showError(String text) { - textError.setText(text); - gotoState(State.ERROR); + errorView.setText(text); + gotoScreen(Screen.ERROR); bottomSheet.postDelayed(() -> { if (!isAdded()) { return; } - gotoState(stateBeforeError); - }, TIME_DELAYED_STATE_CHANGE); - } - - private int resolveColorFromAttr(@AttrRes int resId) { - TypedValue outValue = new TypedValue(); - // must be the themed context to work correctly on Android < 5 - innerBottomSheet.getContext().getTheme().resolveAttribute(resId, outValue, true); - return outValue.data; + gotoScreen(screenBeforeError); + }, TIME_DELAYED_SCREEN_CHANGE); } } diff --git a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/ui/FidoDialogOptions.java b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/ui/FidoDialogOptions.java index ceb79ea..eed7ccd 100644 --- a/hwsecurity/fido/src/main/java/de/cotech/hw/fido/ui/FidoDialogOptions.java +++ b/hwsecurity/fido/src/main/java/de/cotech/hw/fido/ui/FidoDialogOptions.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -31,7 +31,7 @@ import com.google.auto.value.AutoValue; -import de.cotech.hw.fido.R; +import de.cotech.hw.ui.R; @AutoValue public abstract class FidoDialogOptions implements Parcelable { @@ -47,11 +47,14 @@ public abstract class FidoDialogOptions implements Parcelable { @StyleRes public abstract int getTheme(); + public abstract boolean getShowSdkLogo(); + // default values public static Builder builder() { return new AutoValue_FidoDialogOptions.Builder() .setPreventScreenshots(false) - .setTheme(R.style.HwSecurity_Fido_Dialog); + .setShowSdkLogo(false) + .setTheme(R.style.HwSecurity_Dialog); } @AutoValue.Builder @@ -84,7 +87,7 @@ public abstract static class Builder { /** * Set your own custom theme for the dialog to change colors: *

{@code
-         * 
-
-    
-
-    
-
-    
-
\ No newline at end of file
diff --git a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/FidoSecurityKeyTest.java b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/FidoSecurityKeyTest.java
index 10edea9..c7bc253 100644
--- a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/FidoSecurityKeyTest.java
+++ b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/FidoSecurityKeyTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2019 Confidential Technologies GmbH
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
  *
  * You can purchase a commercial license at https://hwsecurity.dev.
  * Buying such a license is mandatory as soon as you develop commercial
diff --git a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/FakeU2fFidoAppletConnection.java b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/FakeU2fFidoAppletConnection.java
index b1770d8..503f850 100644
--- a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/FakeU2fFidoAppletConnection.java
+++ b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/FakeU2fFidoAppletConnection.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2019 Confidential Technologies GmbH
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
  *
  * You can purchase a commercial license at https://hwsecurity.dev.
  * Buying such a license is mandatory as soon as you develop commercial
diff --git a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/FidoU2fAppletConnectionTest.java b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/FidoU2fAppletConnectionTest.java
index b14ffad..9c5f38a 100644
--- a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/FidoU2fAppletConnectionTest.java
+++ b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/FidoU2fAppletConnectionTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2019 Confidential Technologies GmbH
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
  *
  * You can purchase a commercial license at https://hwsecurity.dev.
  * Buying such a license is mandatory as soon as you develop commercial
@@ -41,7 +41,8 @@
 @SuppressWarnings("WeakerAccess")
 public class FidoU2fAppletConnectionTest {
     static final CommandApdu PING_APDU = CommandApdu.create(0x00, 0xc0, 0x00, 0x00);
-    static final CommandApdu PING_APDU_EXTENDED = CommandApdu.create(0x00, 0xc0, 0x00, 0x00).withNe(65536);
+    static final CommandApdu PING_APDU_SHORT = CommandApdu.create(0x00, 0xc0, 0x00, 0x00).withShortApduNe();
+    static final CommandApdu PING_APDU_EXTENDED = CommandApdu.create(0x00, 0xc0, 0x00, 0x00).withExtendedApduNe();
 
     FidoU2fAppletConnection connection;
     FakeTransport transport;
@@ -60,8 +61,9 @@ public void communicate_ping() throws Exception {
 
     @Test(expected = WrongRequestLengthException.class)
     public void communicateOrThrow_wrongLength() throws Exception {
+        // On wrong length, we try again with short APDU encoding
         transport.expect(PING_APDU_EXTENDED, ResponseApduUtils.createError(0x6700));
-        transport.expect(PING_APDU, ResponseApduUtils.createError(0x6700));
+        transport.expect(PING_APDU_SHORT, ResponseApduUtils.createError(0x6700));
         connection.communicateOrThrow(PING_APDU);
     }
 
diff --git a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/FidoAsyncOperationManagerTest.java b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/FidoAsyncOperationManagerTest.java
index 7d5c0d9..5fd8155 100644
--- a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/FidoAsyncOperationManagerTest.java
+++ b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/FidoAsyncOperationManagerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2019 Confidential Technologies GmbH
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
  *
  * You can purchase a commercial license at https://hwsecurity.dev.
  * Buying such a license is mandatory as soon as you develop commercial
diff --git a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/FidoAsyncOperationManagerUtil.java b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/FidoAsyncOperationManagerUtil.java
index 32c3df0..3a39a96 100644
--- a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/FidoAsyncOperationManagerUtil.java
+++ b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/FidoAsyncOperationManagerUtil.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2019 Confidential Technologies GmbH
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
  *
  * You can purchase a commercial license at https://hwsecurity.dev.
  * Buying such a license is mandatory as soon as you develop commercial
diff --git a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/FidoOperationThreadTest.java b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/FidoOperationThreadTest.java
index 83ec6cb..5cc9796 100644
--- a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/FidoOperationThreadTest.java
+++ b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/FidoOperationThreadTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2019 Confidential Technologies GmbH
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
  *
  * You can purchase a commercial license at https://hwsecurity.dev.
  * Buying such a license is mandatory as soon as you develop commercial
diff --git a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/TestFidoOperationThread.java b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/TestFidoOperationThread.java
index ce5d4c5..c9183a3 100644
--- a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/TestFidoOperationThread.java
+++ b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/async/TestFidoOperationThread.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2019 Confidential Technologies GmbH
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
  *
  * You can purchase a commercial license at https://hwsecurity.dev.
  * Buying such a license is mandatory as soon as you develop commercial
diff --git a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/operations/AuthenticateOpTest.java b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/operations/AuthenticateOpTest.java
index 62ff970..2c929aa 100644
--- a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/operations/AuthenticateOpTest.java
+++ b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/operations/AuthenticateOpTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2019 Confidential Technologies GmbH
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
  *
  * You can purchase a commercial license at https://hwsecurity.dev.
  * Buying such a license is mandatory as soon as you develop commercial
diff --git a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/operations/RegisterOpTest.java b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/operations/RegisterOpTest.java
index 5548b13..c5a5365 100644
--- a/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/operations/RegisterOpTest.java
+++ b/hwsecurity/fido/src/test/java/de/cotech/hw/fido/internal/operations/RegisterOpTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2019 Confidential Technologies GmbH
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
  *
  * You can purchase a commercial license at https://hwsecurity.dev.
  * Buying such a license is mandatory as soon as you develop commercial
diff --git a/hwsecurity/fido/src/test/java/de/cotech/hw/internal/iso7816/ResponseApduUtils.java b/hwsecurity/fido/src/test/java/de/cotech/hw/internal/iso7816/ResponseApduUtils.java
index 3e551a7..081c6b1 100644
--- a/hwsecurity/fido/src/test/java/de/cotech/hw/internal/iso7816/ResponseApduUtils.java
+++ b/hwsecurity/fido/src/test/java/de/cotech/hw/internal/iso7816/ResponseApduUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2019 Confidential Technologies GmbH
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
  *
  * You can purchase a commercial license at https://hwsecurity.dev.
  * Buying such a license is mandatory as soon as you develop commercial
diff --git a/hwsecurity/fido/src/test/java/de/cotech/hw/internal/transport/FakeTransport.java b/hwsecurity/fido/src/test/java/de/cotech/hw/internal/transport/FakeTransport.java
index a9ed583..7db1fe5 100644
--- a/hwsecurity/fido/src/test/java/de/cotech/hw/internal/transport/FakeTransport.java
+++ b/hwsecurity/fido/src/test/java/de/cotech/hw/internal/transport/FakeTransport.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2019 Confidential Technologies GmbH
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
  *
  * You can purchase a commercial license at https://hwsecurity.dev.
  * Buying such a license is mandatory as soon as you develop commercial
@@ -89,7 +89,7 @@ public boolean ping() {
 
     @Override
     public TransportType getTransportType() {
-        return TransportType.USB_U2FHID;
+        return TransportType.USB_CTAPHID;
     }
 
     @Nullable
@@ -108,7 +108,7 @@ public void setExtendedLengthSupported(boolean extendedLengthSupported) {
     }
 
     public void expect(String commandBytesHex, String responseBytesHex) throws IOException {
-        expectCommands.add(CommandApdu.fromBytes(Hex.decodeHexOrFail(commandBytesHex)).withNe(65536));
+        expectCommands.add(CommandApdu.fromBytes(Hex.decodeHexOrFail(commandBytesHex)).withExtendedApduNe());
         expectResponses.add(ResponseApdu.fromBytes(Hex.decodeHexOrFail(responseBytesHex)));
     }
 
diff --git a/hwsecurity/fido2/build.gradle b/hwsecurity/fido2/build.gradle
new file mode 100644
index 0000000..1ddc23d
--- /dev/null
+++ b/hwsecurity/fido2/build.gradle
@@ -0,0 +1,105 @@
+apply plugin: 'com.android.library'
+apply plugin: 'maven-publish'
+apply plugin: 'org.jetbrains.dokka-android'
+
+dependencies {
+    api project(':hwsecurity:core')
+    api project(':hwsecurity:ui')
+
+    implementation 'androidx.appcompat:appcompat:1.1.0'
+    implementation 'com.google.android.material:material:1.1.0'
+    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+
+    api 'com.google.auto.value:auto-value-annotations:1.6.2'
+    annotationProcessor 'com.google.auto.value:auto-value:1.6.2'
+    annotationProcessor 'com.ryanharter.auto.value:auto-value-parcel:0.2.6'
+
+    testImplementation 'junit:junit:4.12'
+    testImplementation 'org.robolectric:robolectric:3.8'
+    testImplementation 'org.mockito:mockito-core:2.18.0'
+}
+
+android {
+    compileSdkVersion rootProject.ext.compileSdkVersion
+
+    defaultConfig {
+        minSdkVersion 14
+        versionName rootProject.ext.hwSdkVersionName
+        vectorDrawables.useSupportLibrary = true
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    // Do not abort build if lint finds errors
+    lintOptions {
+        abortOnError false
+    }
+}
+
+// https://developer.android.com/studio/build/maven-publish-plugin
+afterEvaluate {
+    publishing {
+        publications {
+            release(MavenPublication) {
+                from components.release
+
+                groupId = 'de.cotech'
+                artifactId = 'hwsecurity-fido2'
+                version = android.defaultConfig.versionName
+
+                pom {
+                    url = 'https://hwsecurity.dev'
+                    licenses {
+                        license {
+                            name = 'Commercial'
+                            url = 'https://hwsecurity.dev/sales/'
+                            distribution = 'repo'
+                        }
+                        license {
+                            name = 'GNU General Public License, version 3'
+                            url = 'https://www.gnu.org/licenses/gpl-3.0.txt'
+                        }
+                    }
+                    organization {
+                        name = 'Confidential Technologies GmbH'
+                        url = 'https://www.cotech.de'
+                    }
+                }
+            }
+        }
+        /*
+         * To upload release, create file gradle.properties in ~/.gradle/ with this content:
+         *
+         * cotechMavenName=xxx
+         * cotechMavenPassword=xxx
+         */
+        if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) {
+            println "Found cotechMavenName, cotechMavenPassword in gradle.properties!"
+
+            repositories {
+                maven {
+                    credentials {
+                        username cotechMavenName
+                        password cotechMavenPassword
+                    }
+                    url = "https://maven.cotech.de"
+                }
+            }
+        }
+    }
+}
+
+dokka {
+    moduleName = 'hwsecurity-fido2'
+    outputFormat = "hugo"
+    outputDirectory = "$projectDir/../../hwsecurity.dev/content/reference"
+    sourceDirs = files('src/main/java')
+
+    packageOptions {
+        prefix = "de.cotech.hw.fido2.internal"
+        suppress = true
+    }
+}
diff --git a/hwsecurity/fido2/src/main/AndroidManifest.xml b/hwsecurity/fido2/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..091276c
--- /dev/null
+++ b/hwsecurity/fido2/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+    
+
diff --git a/hwsecurity/fido2/src/main/assets/webauthnbridge.js b/hwsecurity/fido2/src/main/assets/webauthnbridge.js
new file mode 100644
index 0000000..3c60ad6
--- /dev/null
+++ b/hwsecurity/fido2/src/main/assets/webauthnbridge.js
@@ -0,0 +1,124 @@
+function () {
+
+    console.log("webauthnbridge start execution");
+
+    const webauthnbridge = window.webauthnbridge = {};
+
+    webauthnbridge.reqCounter = 0;
+
+    // Is called from Java
+    webauthnbridge.responseHandler = function(message) {
+        const response = message;
+        const reqId = response['requestId'];
+        if (!reqId || !webauthnbridge.callbackMap[reqId]) {
+            console.error('Unknown or missing requestId in response.');
+            return;
+        }
+        const cb = webauthnbridge.callbackMap[reqId];
+        delete webauthnbridge.callbackMap[reqId];
+        cb(response['responseData']);
+    };
+
+    const jsonArrayFix = function(k, v) {
+        if (v instanceof ArrayBuffer) {
+            return new Uint8Array(v);
+        }
+        return v;
+    };
+
+    const decodeWebsafeBase64 = function(websafeBase64) {
+        const base64 = websafeBase64.replace(/_/g, '/').replace(/-/g, '+');
+        return Uint8Array.from(atob(base64), c=>c.charCodeAt(0))
+    };
+
+    navigator.credentials = function() {
+    }
+
+    // overrides Browser API: https://www.w3.org/TR/credential-management-1
+    navigator.credentials.get = function(opt_options) {
+        return new Promise((resolve, reject) => {
+            webauthnbridge.current_resolve = resolve;
+            webauthnbridge.current_reject = reject;
+            const jsonMessage = JSON.stringify(opt_options, jsonArrayFix);
+            webauthnbridgejava.get(jsonMessage);
+        });
+    };
+
+    navigator.credentials.store = function(credential) {
+        return new Promise((resolve, reject) => {
+            const jsonMessage = JSON.stringify(credential);
+            webauthnbridgejava.store(jsonMessage);
+        });
+    };
+
+    navigator.credentials.create = function(opt_options) {
+        return new Promise((resolve, reject) => {
+            webauthnbridge.current_resolve = resolve;
+            webauthnbridge.current_reject = reject;
+            const jsonMessage = JSON.stringify(opt_options, jsonArrayFix);
+            webauthnbridgejava.create(jsonMessage);
+        });
+    };
+
+    navigator.credentials.preventSilentAccess = function() {
+        // calls to Java
+        webauthnbridgejava.preventSilentAccess();
+    };
+
+    window.PublicKeyCredential = function() {
+    }
+
+    // Fakes Windows Hello support
+    // https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/isUserVerifyingPlatformAuthenticatorAvailable
+    window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = function(opt_options) {
+       return Promise.resolve(true);
+    };
+
+    // Fakes Windows Hello support
+    // https://github.com/w3c/webauthn/issues/1173
+    window.PublicKeyCredential.isExternalCTAP2SecurityKeySupported = function(opt_options) {
+       return Promise.resolve(true);
+    };
+
+    webauthnbridge.handleReject = function(error) {
+        webauthnbridge.current_reject(error);
+    }
+
+    webauthnbridge.handleResolve = function(obj) {
+        console.log(JSON.stringify(obj));
+
+        obj.rawId = decodeWebsafeBase64(obj.id);
+
+        obj.response.clientDataJSON = decodeWebsafeBase64(obj.response.clientDataJsonB64);
+        delete obj.response.clientDataJsonB64;
+
+        if (obj.response.attestationObjectB64) {
+            obj.response.attestationObject = decodeWebsafeBase64(obj.response.attestationObjectB64);
+            delete obj.response.attestationObjectB64;
+        }
+
+        if (obj.response.signatureB64) {
+            obj.response.signature = decodeWebsafeBase64(obj.response.signatureB64);
+            delete obj.response.signatureB64;
+        }
+
+        if (obj.response.authenticatorDataB64) {
+            obj.response.authenticatorData = decodeWebsafeBase64(obj.response.authenticatorDataB64);
+            delete obj.response.authenticatorDataB64;
+        }
+
+        if (obj.response.userHandleB64) {
+            obj.response.userHandle = decodeWebsafeBase64(obj.response.userHandleB64);
+            delete obj.response.userHandleB64;
+        } else {
+            obj.response.userHandle = null;
+        }
+
+        obj.getClientExtensionResults = function () { return {}; };
+
+        console.log(JSON.stringify(obj));
+        webauthnbridge.current_resolve(obj);
+    }
+
+    console.log("webauthnbridge end execution");
+}
diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Credential.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Credential.java
new file mode 100644
index 0000000..d7d1596
--- /dev/null
+++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Credential.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
+ *
+ * You can purchase a commercial license at https://hwsecurity.dev.
+ * Buying such a license is mandatory as soon as you develop commercial
+ * activities involving this program without disclosing the source code
+ * of your own applications.
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package de.cotech.hw.fido2;
+
+
+abstract class Credential {
+    public abstract String id();
+    public abstract String type();
+}
diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Ctap2Callback.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Ctap2Callback.java
new file mode 100644
index 0000000..fbc20b9
--- /dev/null
+++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Ctap2Callback.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
+ *
+ * You can purchase a commercial license at https://hwsecurity.dev.
+ * Buying such a license is mandatory as soon as you develop commercial
+ * activities involving this program without disclosing the source code
+ * of your own applications.
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package de.cotech.hw.fido2;
+
+
+import java.io.IOException;
+
+import de.cotech.hw.fido2.internal.ctap2.Ctap2Response;
+
+
+public interface Ctap2Callback {
+    void onResponse(CR response);
+    void onIoException(IOException e);
+}
diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Fido2SecurityKey.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Fido2SecurityKey.java
new file mode 100644
index 0000000..9de4e0e
--- /dev/null
+++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Fido2SecurityKey.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
+ *
+ * You can purchase a commercial license at https://hwsecurity.dev.
+ * Buying such a license is mandatory as soon as you develop commercial
+ * activities involving this program without disclosing the source code
+ * of your own applications.
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package de.cotech.hw.fido2;
+
+
+import java.io.IOException;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.WorkerThread;
+import androidx.lifecycle.LifecycleOwner;
+import de.cotech.hw.SecurityKey;
+import de.cotech.hw.SecurityKeyManagerConfig;
+import de.cotech.hw.fido2.domain.create.PublicKeyCredentialCreationOptions;
+import de.cotech.hw.fido2.domain.get.PublicKeyCredentialRequestOptions;
+import de.cotech.hw.fido2.internal.Fido2AppletConnection;
+import de.cotech.hw.fido2.internal.async.Ctap2Fido2OperationThread;
+import de.cotech.hw.fido2.internal.async.Fido2AsyncOperationManager;
+import de.cotech.hw.fido2.internal.async.WebauthnFido2OperationThread;
+import de.cotech.hw.fido2.internal.ctap2.Ctap2Command;
+import de.cotech.hw.fido2.internal.ctap2.Ctap2Response;
+import de.cotech.hw.fido2.internal.json.JsonPublicKeyCredentialSerializer;
+import de.cotech.hw.fido2.internal.json.JsonWebauthnOptionsParser;
+import de.cotech.hw.fido2.internal.operations.WebauthnSecurityKeyOperation;
+import de.cotech.hw.fido2.internal.operations.WebauthnSecurityKeyOperationFactory;
+import de.cotech.hw.fido2.internal.webauthn.WebauthnCommand;
+import de.cotech.hw.fido2.internal.webauthn.WebauthnResponse;
+import de.cotech.hw.internal.transport.Transport;
+import org.json.JSONException;
+
+
+@SuppressWarnings({ "unused", "WeakerAccess" }) // All methods are public API
+public class Fido2SecurityKey extends SecurityKey {
+    private static final int USER_PRESENCE_CHECK_DELAY_MS = 250;
+
+    private final Fido2AppletConnection fido2AppletConnection;
+    private final Fido2AsyncOperationManager fido2AsyncOperationManager;
+    private final WebauthnSecurityKeyOperationFactory operationFactory;
+    private final JsonWebauthnOptionsParser jsonOptionsParser = new JsonWebauthnOptionsParser();
+    private final JsonPublicKeyCredentialSerializer jsonPublicKeyCredentialSerializer =
+            new JsonPublicKeyCredentialSerializer();
+
+    Fido2SecurityKey(SecurityKeyManagerConfig config, Fido2AppletConnection fido2AppletConnection,
+            Transport transport, Fido2AsyncOperationManager fido2AsyncOperationManager,
+            WebauthnSecurityKeyOperationFactory operationFactory) {
+        super(config, transport);
+        this.fido2AppletConnection = fido2AppletConnection;
+        this.fido2AsyncOperationManager = fido2AsyncOperationManager;
+        this.operationFactory = operationFactory;
+    }
+
+    @WorkerThread
+    String webauthnPublicKeyCredentialGet(String jsonOptions, String origin) throws IOException {
+        PublicKeyCredentialRequestOptions options;
+        try {
+            options = jsonOptionsParser.fromOptionsJsonGetAssertion(jsonOptions);
+        } catch (JSONException e) {
+            throw new IOException("Invalid input parameters!", e);
+        }
+        PublicKeyCredentialGet request = PublicKeyCredentialGet.create(origin, options);
+        PublicKeyCredential publicKeyCredential = webauthnCommand(request);
+        return jsonPublicKeyCredentialSerializer.publicKeyCredentialToJsonString(publicKeyCredential);
+    }
+
+    void webauthnPublicKeyCredentialGetAsync(String jsonOptions, String origin,
+            WebauthnJsonCallback callback, LifecycleOwner lifecycleOwner) throws IOException {
+        Handler mainThreadHandler = new Handler(Looper.getMainLooper());
+        webauthnPublicKeyCredentialGetAsync(jsonOptions, origin, callback, mainThreadHandler, lifecycleOwner);
+    }
+
+    void webauthnPublicKeyCredentialGetAsync(String jsonOptions, String origin,
+            WebauthnJsonCallback callback, Handler handler, LifecycleOwner lifecycleOwner) throws IOException {
+        PublicKeyCredentialRequestOptions options;
+        try {
+            options = jsonOptionsParser.fromOptionsJsonGetAssertion(jsonOptions);
+        } catch (JSONException e) {
+            throw new IOException("Invalid input parameters!", e);
+        }
+        PublicKeyCredentialGet request = PublicKeyCredentialGet.create(origin, options);
+        webauthnCommandAsync(request, webauthnResponseToJsonCallback(callback), handler, lifecycleOwner);
+    }
+
+    @WorkerThread
+    public String webauthnPublicKeyCredentialCreate(String jsonOptions, String origin) throws IOException {
+        PublicKeyCredentialCreationOptions options;
+        try {
+            options = jsonOptionsParser.fromOptionsJsonMakeCredential(jsonOptions);
+        } catch (JSONException e) {
+            throw new IOException("Invalid input parameters!", e);
+        }
+        PublicKeyCredentialCreate create = PublicKeyCredentialCreate.create(origin, options);
+        PublicKeyCredential publicKeyCredential = webauthnCommand(create);
+        return jsonPublicKeyCredentialSerializer.publicKeyCredentialToJsonString(publicKeyCredential);
+    }
+
+    @AnyThread
+    void webauthnPublicKeyCredentialCreateAsync(String jsonOptions, String origin,
+            WebauthnJsonCallback callback, LifecycleOwner lifecycleOwner)
+            throws IOException {
+        Handler mainThreadHandler = new Handler(Looper.getMainLooper());
+        webauthnPublicKeyCredentialCreateAsync(jsonOptions, origin, callback, mainThreadHandler, lifecycleOwner);
+    }
+
+    @AnyThread
+    void webauthnPublicKeyCredentialCreateAsync(String jsonOptions, String origin,
+            WebauthnJsonCallback callback, Handler handler, LifecycleOwner lifecycleOwner)
+            throws IOException {
+        PublicKeyCredentialCreationOptions options;
+        try {
+            options = jsonOptionsParser.fromOptionsJsonMakeCredential(jsonOptions);
+        } catch (JSONException e) {
+            throw new IOException("Invalid input parameters!", e);
+        }
+        PublicKeyCredentialCreate create = PublicKeyCredentialCreate.create(origin, options);
+        webauthnCommandAsync(create, webauthnResponseToJsonCallback(callback), handler, lifecycleOwner);
+    }
+
+    @WorkerThread
+    public 
+    WR webauthnCommand(WC command) throws IOException {
+        WebauthnSecurityKeyOperation operation = operationFactory.getOperation(
+                command, fido2AppletConnection.isCtap2Capable());
+        return operation.performWebauthnSecurityKeyOperation(fido2AppletConnection, command);
+    }
+
+    @AnyThread
+    public 
+    void webauthnCommandAsync(WebauthnCommand command, WebauthnCallback callback,
+            LifecycleOwner lifecycleOwner) {
+        Handler mainThreadHandler = new Handler(Looper.getMainLooper());
+        webauthnCommandAsync(command, callback, mainThreadHandler, lifecycleOwner);
+    }
+
+    @AnyThread
+    public 
+    void webauthnCommandAsync(
+            WC command, WebauthnCallback callback, Handler handler,
+            LifecycleOwner lifecycleOwner) {
+        WebauthnFido2OperationThread
+                fidoOperationThread = new WebauthnFido2OperationThread<>(
+                fido2AppletConnection, operationFactory, handler, callback, command, USER_PRESENCE_CHECK_DELAY_MS);
+        fido2AsyncOperationManager.startAsyncOperation(lifecycleOwner, fidoOperationThread);
+    }
+
+    @WorkerThread
+    public  CR ctap2RawCommand(Ctap2Command ctap2Command)
+            throws IOException {
+        return fido2AppletConnection.ctap2CommunicateOrThrow(ctap2Command);
+    }
+
+    @AnyThread
+    public  void ctap2RawCommandAsync(Ctap2Command command,
+            Ctap2Callback callback, LifecycleOwner lifecycleOwner) {
+        Handler mainThreadHandler = new Handler(Looper.getMainLooper());
+        ctap2RawCommandAsync(command, callback, mainThreadHandler, lifecycleOwner);
+    }
+
+    @AnyThread
+    public  void ctap2RawCommandAsync(Ctap2Command command,
+            Ctap2Callback callback, Handler handler, LifecycleOwner lifecycleOwner) {
+        Ctap2Fido2OperationThread ctap2Fido2OperationThread = new Ctap2Fido2OperationThread<>(
+                fido2AppletConnection, handler, command, callback, USER_PRESENCE_CHECK_DELAY_MS);
+        fido2AsyncOperationManager.startAsyncOperation(lifecycleOwner, ctap2Fido2OperationThread);
+    }
+
+    @AnyThread
+    public void clearAsyncOperation() {
+        fido2AsyncOperationManager.clearAsyncOperation();
+    }
+
+    private WebauthnCallback webauthnResponseToJsonCallback(
+            WebauthnJsonCallback callback) {
+        return new WebauthnCallback() {
+            @Override
+            public void onResponse(PublicKeyCredential jsonResponse) {
+                jsonPublicKeyCredentialSerializer.publicKeyCredentialToJsonString(jsonResponse);
+            }
+
+            @Override
+            public void onIoException(IOException e) {
+                callback.onIoException(e);
+            }
+        };
+    }
+}
diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Fido2SecurityKeyConnectionMode.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Fido2SecurityKeyConnectionMode.java
new file mode 100644
index 0000000..6bbb788
--- /dev/null
+++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Fido2SecurityKeyConnectionMode.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
+ *
+ * You can purchase a commercial license at https://hwsecurity.dev.
+ * Buying such a license is mandatory as soon as you develop commercial
+ * activities involving this program without disclosing the source code
+ * of your own applications.
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package de.cotech.hw.fido2;
+
+
+import java.io.IOException;
+
+import de.cotech.hw.SecurityKey;
+import de.cotech.hw.SecurityKeyConnectionMode;
+import de.cotech.hw.SecurityKeyManagerConfig;
+import de.cotech.hw.fido2.internal.Fido2AppletConnection;
+import de.cotech.hw.fido2.internal.async.Fido2AsyncOperationManager;
+import de.cotech.hw.fido2.internal.operations.WebauthnSecurityKeyOperationFactory;
+import de.cotech.hw.fido2.internal.pinauth.PinAuthCryptoUtil;
+import de.cotech.hw.fido2.internal.pinauth.PinProtocolV1;
+import de.cotech.hw.internal.transport.SecurityKeyInfo.TransportType;
+import de.cotech.hw.internal.transport.Transport;
+
+
+public class Fido2SecurityKeyConnectionMode extends SecurityKeyConnectionMode {
+    private static Fido2SecurityKeyConnectionMode INSTANCE;
+    private final Fido2SecurityKeyConnectionModeConfig fido2Config;
+
+    public static Fido2SecurityKeyConnectionMode getInstance() {
+        if (INSTANCE == null) {
+            Fido2SecurityKeyConnectionModeConfig defaultConfig =
+                    Fido2SecurityKeyConnectionModeConfig.getDefaultConfig();
+            INSTANCE = new Fido2SecurityKeyConnectionMode(
+                    defaultConfig);
+        }
+        return INSTANCE;
+    }
+
+    public static Fido2SecurityKeyConnectionMode getInstance(Fido2SecurityKeyConnectionModeConfig  config) {
+        return new Fido2SecurityKeyConnectionMode(config);
+    }
+
+    public Fido2SecurityKeyConnectionMode(Fido2SecurityKeyConnectionModeConfig fido2Config) {
+        this.fido2Config = fido2Config;
+    }
+
+    @Override
+    public Fido2SecurityKey establishSecurityKeyConnection(SecurityKeyManagerConfig config, Transport transport) throws IOException {
+        if (!isRelevantTransport(transport)) {
+            throw new IllegalArgumentException("Received incompatible transport!");
+        }
+
+        Fido2AppletConnection fido2AppletConnection = Fido2AppletConnection.getInstanceForTransport(transport);
+        fido2AppletConnection.connectIfNecessary();
+        fido2AppletConnection.setForceCtap1(fido2Config.isForceU2f());
+
+        WebauthnSecurityKeyOperationFactory operationFactory =
+                new WebauthnSecurityKeyOperationFactory(new PinProtocolV1(new PinAuthCryptoUtil()));
+        return new Fido2SecurityKey(config, fido2AppletConnection, transport, new Fido2AsyncOperationManager(),
+                operationFactory);
+    }
+
+    @Override
+    protected boolean isRelevantTransport(Transport transport) {
+        return transport.getTransportType() == TransportType.USB_CTAPHID
+                // || transport.getTransportType() == TransportType.USB_CCID
+                || transport.getTransportType() == TransportType.NFC;
+    }
+
+    @Override
+    protected boolean isRelevantSecurityKey(SecurityKey securityKey) {
+        return securityKey instanceof Fido2SecurityKey;
+    }
+}
diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Fido2SecurityKeyConnectionModeConfig.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Fido2SecurityKeyConnectionModeConfig.java
new file mode 100644
index 0000000..6543060
--- /dev/null
+++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/Fido2SecurityKeyConnectionModeConfig.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
+ *
+ * You can purchase a commercial license at https://hwsecurity.dev.
+ * Buying such a license is mandatory as soon as you develop commercial
+ * activities involving this program without disclosing the source code
+ * of your own applications.
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package de.cotech.hw.fido2;
+
+
+import android.os.Parcelable;
+
+import com.google.auto.value.AutoValue;
+
+
+@AutoValue
+public abstract class Fido2SecurityKeyConnectionModeConfig  implements Parcelable {
+    public abstract boolean isForceU2f();
+
+    public static Fido2SecurityKeyConnectionModeConfig getDefaultConfig() {
+        return builder().build();
+    }
+
+    public static Fido2SecurityKeyConnectionModeConfig.Builder builder() {
+        return new AutoValue_Fido2SecurityKeyConnectionModeConfig.Builder()
+                .setForceU2f(false);
+    }
+
+    @AutoValue.Builder
+    public static abstract class Builder {
+        public abstract Builder setForceU2f(boolean forceU2f);
+
+        public abstract Fido2SecurityKeyConnectionModeConfig build();
+    }
+}
diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/PublicKeyCredential.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/PublicKeyCredential.java
new file mode 100644
index 0000000..6141fed
--- /dev/null
+++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/PublicKeyCredential.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
+ *
+ * You can purchase a commercial license at https://hwsecurity.dev.
+ * Buying such a license is mandatory as soon as you develop commercial
+ * activities involving this program without disclosing the source code
+ * of your own applications.
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package de.cotech.hw.fido2;
+
+
+import com.google.auto.value.AutoValue;
+import de.cotech.hw.fido2.domain.AuthenticatorResponse;
+import de.cotech.hw.fido2.internal.utils.WebsafeBase64;
+import de.cotech.hw.fido2.internal.webauthn.WebauthnResponse;
+
+
+@AutoValue
+public abstract class PublicKeyCredential extends Credential implements WebauthnResponse {
+    public abstract byte[] rawId();
+    public abstract AuthenticatorResponse response();
+
+    public String id() {
+        return WebsafeBase64.encodeToString(rawId());
+    }
+
+    @Override
+    public String type() {
+        return "public-key";
+    }
+
+    public static PublicKeyCredential create(byte[] rawId, AuthenticatorResponse response) {
+        return new AutoValue_PublicKeyCredential(rawId, response);
+    }
+}
diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/PublicKeyCredentialCreate.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/PublicKeyCredentialCreate.java
new file mode 100644
index 0000000..2db17fc
--- /dev/null
+++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/PublicKeyCredentialCreate.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
+ *
+ * You can purchase a commercial license at https://hwsecurity.dev.
+ * Buying such a license is mandatory as soon as you develop commercial
+ * activities involving this program without disclosing the source code
+ * of your own applications.
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package de.cotech.hw.fido2;
+
+
+import androidx.annotation.Nullable;
+import com.google.auto.value.AutoValue;
+import de.cotech.hw.fido2.domain.create.PublicKeyCredentialCreationOptions;
+import de.cotech.hw.fido2.internal.webauthn.WebauthnCommand;
+
+
+@AutoValue
+public abstract class PublicKeyCredentialCreate extends WebauthnCommand {
+    public abstract String origin();
+    public abstract PublicKeyCredentialCreationOptions options();
+    @Nullable
+    public abstract String clientPin();
+    public abstract boolean lastAttemptOk();
+
+    public static PublicKeyCredentialCreate create(String origin, PublicKeyCredentialCreationOptions options) {
+        return new AutoValue_PublicKeyCredentialCreate(origin, options, null, false);
+    }
+
+    @Override
+    public PublicKeyCredentialCreate withClientPin(String clientPin, boolean lastAttemptOk) {
+        return new AutoValue_PublicKeyCredentialCreate(origin(), options(), clientPin, lastAttemptOk);
+    }
+}
diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/PublicKeyCredentialGet.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/PublicKeyCredentialGet.java
new file mode 100644
index 0000000..83fdd07
--- /dev/null
+++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/PublicKeyCredentialGet.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
+ *
+ * You can purchase a commercial license at https://hwsecurity.dev.
+ * Buying such a license is mandatory as soon as you develop commercial
+ * activities involving this program without disclosing the source code
+ * of your own applications.
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package de.cotech.hw.fido2;
+
+
+import androidx.annotation.Nullable;
+import com.google.auto.value.AutoValue;
+import de.cotech.hw.fido2.domain.get.PublicKeyCredentialRequestOptions;
+import de.cotech.hw.fido2.internal.webauthn.WebauthnCommand;
+
+
+@AutoValue
+public abstract class PublicKeyCredentialGet extends WebauthnCommand {
+    public abstract String origin();
+    public abstract PublicKeyCredentialRequestOptions options();
+    @Nullable
+    public abstract String clientPin();
+    public abstract boolean lastAttemptOk();
+
+    public static PublicKeyCredentialGet create(String origin, PublicKeyCredentialRequestOptions options) {
+        return new AutoValue_PublicKeyCredentialGet(origin, options, null, false);
+    }
+
+    @Override
+    public PublicKeyCredentialGet withClientPin(String clientPin, boolean lastAttemptOk) {
+        return new AutoValue_PublicKeyCredentialGet(origin(), options(), clientPin, lastAttemptOk);
+    }
+}
diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/WebViewWebauthnBridge.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/WebViewWebauthnBridge.java
new file mode 100644
index 0000000..1248fb6
--- /dev/null
+++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/WebViewWebauthnBridge.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2018-2020 Confidential Technologies GmbH
+ *
+ * You can purchase a commercial license at https://hwsecurity.dev.
+ * Buying such a license is mandatory as soon as you develop commercial
+ * activities involving this program without disclosing the source code
+ * of your own applications.
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package de.cotech.hw.fido2;
+
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.Charset;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.webkit.JavascriptInterface;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebView;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.FragmentManager;
+import de.cotech.hw.fido2.domain.create.PublicKeyCredentialCreationOptions;
+import de.cotech.hw.fido2.domain.get.PublicKeyCredentialRequestOptions;
+import de.cotech.hw.fido2.internal.json.JsonPublicKeyCredentialSerializer;
+import de.cotech.hw.fido2.internal.json.JsonWebauthnOptionsParser;
+import de.cotech.hw.fido2.internal.utils.AndroidUtils;
+import de.cotech.hw.fido2.ui.WebauthnDialogFragment;
+import de.cotech.hw.fido2.ui.WebauthnDialogFragment.OnGetAssertionCallback;
+import de.cotech.hw.fido2.ui.WebauthnDialogFragment.OnMakeCredentialCallback;
+import de.cotech.hw.fido2.ui.WebauthnDialogOptions;
+import de.cotech.hw.util.HwTimber;
+import org.json.JSONException;
+
+import de.cotech.hw.ui.R;
+
+/**
+ * If you are using a WebView for your login flow, you can use this WebViewWebauthnBridge
+ * for extending the WebView's Javascript API with the WebAuthn APIs.
+ * 

+ * Note: Currently only compatible and tested with Android SDK >= 19 due to evaluateJavascript() calls. + */ +@TargetApi(VERSION_CODES.KITKAT) +public class WebViewWebauthnBridge { + private static final String WEBAUTHN_BRIDGE_INTERFACE = "webauthnbridgejava"; + private static final String ASSETS_BRIDGE_JS = "webauthnbridge.js"; + + private final Context context; + private final FragmentManager fragmentManager; + private final WebView webView; + private final WebauthnDialogOptions.Builder optionsBuilder; + + private String currentOrigin; + private boolean loadingNewPage; + private JsonWebauthnOptionsParser jsonWebauthnOptionsParser = + new JsonWebauthnOptionsParser(); + private JsonPublicKeyCredentialSerializer jsonPublicKeyCredentialSerializer = + new JsonPublicKeyCredentialSerializer(); + + @SuppressWarnings("unused") // public API + public static WebViewWebauthnBridge createInstanceForWebView(AppCompatActivity activity, WebView webView) { + return createInstanceForWebView(activity.getApplicationContext(), activity.getSupportFragmentManager(), webView, null); + } + + /** + * Same as createInstanceForWebView, but allows to set WebauthnDialogOptions.Builder. + *

+ * Note: Timeout and Title will be overwritten. + */ + @SuppressWarnings("unused") // public API + public static WebViewWebauthnBridge createInstanceForWebView(AppCompatActivity activity, WebView webView, WebauthnDialogOptions.Builder optionsBuilder) { + return createInstanceForWebView(activity.getApplicationContext(), activity.getSupportFragmentManager(), webView, optionsBuilder); + } + + public static WebViewWebauthnBridge createInstanceForWebView(Context context, FragmentManager fragmentManager, WebView webView) { + return createInstanceForWebView(context, fragmentManager, webView, null); + } + + @SuppressWarnings("WeakerAccess") // public API + public static WebViewWebauthnBridge createInstanceForWebView(Context context, FragmentManager fragmentManager, WebView webView, WebauthnDialogOptions.Builder optionsBuilder) { + Context applicationContext = context.getApplicationContext(); + + WebauthnDialogOptions.Builder opsBuilder = optionsBuilder != null ? optionsBuilder : WebauthnDialogOptions.builder(); + WebViewWebauthnBridge webViewWebauthnBridge = new WebViewWebauthnBridge(applicationContext, fragmentManager, webView, opsBuilder); + webViewWebauthnBridge.addJavascriptInterfaceToWebView(); + + return webViewWebauthnBridge; + } + + private WebViewWebauthnBridge(Context context, FragmentManager fragmentManager, WebView webView, WebauthnDialogOptions.Builder optionsBuilder) { + this.context = context; + this.fragmentManager = fragmentManager; + this.webView = webView; + this.optionsBuilder = optionsBuilder; + } + + private void addJavascriptInterfaceToWebView() { + webView.addJavascriptInterface(new JsInterface(), WEBAUTHN_BRIDGE_INTERFACE); + } + + @Keep + class JsInterface { + @Keep + @JavascriptInterface + public void get(String options) { + javascriptPublicKeyCredentialGet(options); + } + + @Keep + @JavascriptInterface + public void store(String credential) { + javascriptPublicKeyCredentialStore(credential); + } + + @Keep + @JavascriptInterface + public void create(String options) { + javascriptPublicKeyCredentialCreate(options); + } + + @Keep + @JavascriptInterface + public void preventSilentAccess() { + javascriptPublicKeyCredentialPreventSilentAccess(); + } + } + + // region delegate + + /** + * Call this in your WebViewClient.shouldInterceptRequest(WebView view, WebResourceRequest request) + */ + @TargetApi(VERSION_CODES.LOLLIPOP) + @SuppressWarnings("unused") + // parity with WebViewClient.shouldInterceptRequest(WebView view, WebResourceRequest request) + public void delegateShouldInterceptRequest(WebView view, WebResourceRequest request) { + HwTimber.d("shouldInterceptRequest(WebView view, WebResourceRequest request) %s", request.getUrl()); + injectOnInterceptRequest(); + } + + /** + * Call this in your WebViewClient.shouldInterceptRequest(WebView view, String url) + */ + @TargetApi(VERSION_CODES.KITKAT) + @SuppressWarnings("unused") + // parity with WebViewClient.shouldInterceptRequest(WebView view, String url) + public void delegateShouldInterceptRequest(WebView view, String url) { + HwTimber.d("shouldInterceptRequest(WebView view, String url): %s", url); + injectOnInterceptRequest(); + } + + @SuppressWarnings("unused") // parity with WebViewClient.onPageStarted + public void delegateOnPageStarted(WebView view, String url, Bitmap favicon) { + this.currentOrigin = null; + this.loadingNewPage = false; + + if (url == null) { + return; + } + Uri uri = Uri.parse(url); + + if (!"https".equalsIgnoreCase(uri.getScheme())) { + HwTimber.e("WebAuthn only supported for HTTPS websites!"); + return; + } + + this.currentOrigin = "https://" + uri.getAuthority(); + this.loadingNewPage = true; + } + + private void injectOnInterceptRequest() { + if (loadingNewPage) { + loadingNewPage = false; + HwTimber.d("Scheduling WebAuthn bridge injection!"); + Handler handler = new Handler(context.getMainLooper()); + handler.postAtFrontOfQueue(this::injectJavascriptBridge); + } + } + + private void injectJavascriptBridge() { + try { + String jsContent = AndroidUtils.loadTextFromAssets(context, ASSETS_BRIDGE_JS, Charset.defaultCharset()); + webView.evaluateJavascript("javascript:(" + jsContent + ")()", null); + } catch (IOException e) { + HwTimber.e(e); + throw new IllegalStateException(); + } + } + + // endregion + + private void javascriptPublicKeyCredentialGet(String optionsJsonString) { + HwTimber.d("javascriptPublicKeyCredentialGet: %s", optionsJsonString); + try { + PublicKeyCredentialRequestOptions options = jsonWebauthnOptionsParser.fromOptionsJsonGetAssertion(optionsJsonString); + javascriptPublicKeyCredentialGet(options); + } catch (JSONException e) { + HwTimber.e(e); + } + } + + private void javascriptPublicKeyCredentialGet(PublicKeyCredentialRequestOptions options) { + PublicKeyCredentialGet + credentialGetCommand = PublicKeyCredentialGet.create(currentOrigin, options); + + OnGetAssertionCallback onGetCredentialCallback = new OnGetAssertionCallback() { + @Override + public void onGetAssertionResponse(@NonNull PublicKeyCredential publicKeyCredential) { + callJavascriptResolve(publicKeyCredential); + HwTimber.d("response: %s", publicKeyCredential); + } + + @Override + public void onGetAssertionCancel() { + HwTimber.d("operation cancelled."); + callJavascriptCancel(); + } + + @Override + public void onGetAssertionTimeout() { + HwTimber.d("timeout: %s", options); + callJavascriptTimeout(); + } + }; + + optionsBuilder.setTimeoutMs(options.timeout()); + optionsBuilder.setTitle(context.getString(R.string.hwsecurity_fido_title_default_authenticate_app_id, getDisplayOrigin(currentOrigin))); + + WebauthnDialogFragment webauthnDialogFragment = WebauthnDialogFragment.newInstance( + credentialGetCommand, optionsBuilder.build()); + webauthnDialogFragment.setOnGetAssertionCallback(onGetCredentialCallback); + webauthnDialogFragment.show(fragmentManager); + } + + private void javascriptPublicKeyCredentialStore(String optionsJsonString) { + HwTimber.e("store: Not implemented"); + } + + private void javascriptPublicKeyCredentialCreate(String optionsJsonString) { + HwTimber.d("javascriptPublicKeyCredentialCreate: %s", optionsJsonString); + try { + PublicKeyCredentialCreationOptions options = jsonWebauthnOptionsParser.fromOptionsJsonMakeCredential(optionsJsonString); + javascriptPublicKeyCredentialCreate(options); + } catch (JSONException e) { + HwTimber.e(e); + } + } + + private void javascriptPublicKeyCredentialCreate(PublicKeyCredentialCreationOptions options) { + PublicKeyCredentialCreate + credentialCreateCommand = PublicKeyCredentialCreate.create(currentOrigin, options); + + OnMakeCredentialCallback onMakeCredentialCallback = new OnMakeCredentialCallback() { + @Override + public void onMakeCredentialResponse(@NonNull PublicKeyCredential publicKeyCredential) { + callJavascriptResolve(publicKeyCredential); + HwTimber.d("response: %s", publicKeyCredential); + } + + @Override + public void onMakeCredentialCancel() { + HwTimber.d("operation cancelled."); + callJavascriptCancel(); + } + + @Override + public void onMakeCredentialTimeout() { + HwTimber.d("timeout: %s", options); + callJavascriptTimeout(); + } + }; + + optionsBuilder.setTimeoutMs(options.timeout()); + optionsBuilder.setTitle(context.getString(R.string.hwsecurity_fido_title_default_register_app_id, getDisplayOrigin(currentOrigin))); + + WebauthnDialogFragment webauthnDialogFragment = WebauthnDialogFragment.newInstance( + credentialCreateCommand, optionsBuilder.build()); + webauthnDialogFragment.setOnMakeCredentialCallback(onMakeCredentialCallback); + webauthnDialogFragment.show(fragmentManager); + } + + private void javascriptPublicKeyCredentialPreventSilentAccess() { + HwTimber.e("preventSilentAccess: Not implemented"); + } + + private void callJavascriptCancel() { + String javascript = "javascript:webauthnbridge.handleReject(new Error('User cancelled operation.'))"; + webView.evaluateJavascript(javascript, null); + } + + private void callJavascriptTimeout() { + String javascript = "javascript:webauthnbridge.handleReject(new Error('Operation timed out.'))"; + webView.evaluateJavascript(javascript, null); + } + + private void callJavascriptResolve(PublicKeyCredential publicKeyCredential) { + String publicKeyCredentialJson = + jsonPublicKeyCredentialSerializer.publicKeyCredentialToJsonString(publicKeyCredential); + String javascript = "javascript:webauthnbridge.handleResolve(" + publicKeyCredentialJson + ")"; + webView.evaluateJavascript(javascript, null); + } + + public void setForceU2f(boolean forceU2f) { + optionsBuilder.setForceU2f(forceU2f); + } + + private String getDisplayOrigin(String origin) { + try { + URI appIdUri = new URI(origin); + return appIdUri.getHost(); + } catch (URISyntaxException e) { + throw new IllegalStateException("Invalid URI used for origin"); + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/WebauthnCallback.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/WebauthnCallback.java new file mode 100644 index 0000000..5424bbc --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/WebauthnCallback.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2; + + +import java.io.IOException; + +import de.cotech.hw.fido2.internal.ctap2.Ctap2Response; +import de.cotech.hw.fido2.internal.webauthn.WebauthnResponse; + + +public interface WebauthnCallback { + void onResponse(R response); + void onIoException(IOException e); +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/WebauthnJsonCallback.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/WebauthnJsonCallback.java new file mode 100644 index 0000000..a4b39e9 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/WebauthnJsonCallback.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2; + + +import java.io.IOException; + + +public interface WebauthnJsonCallback { + void onResponse(String jsonResponse); + void onIoException(IOException e); +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/AuthenticatorResponse.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/AuthenticatorResponse.java new file mode 100644 index 0000000..9e22123 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/AuthenticatorResponse.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; + + +public abstract class AuthenticatorResponse { + public abstract byte[] clientDataJson(); +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/AuthenticatorTransport.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/AuthenticatorTransport.java new file mode 100644 index 0000000..f55839c --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/AuthenticatorTransport.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; + + +import androidx.annotation.CheckResult; +import androidx.annotation.Nullable; + + +// https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport +public enum AuthenticatorTransport { + USB ("usb"), + NFC ("nfc"), + BLE ("ble"), + INTERNAL ("internal"); + + public final String transport; + + AuthenticatorTransport(String transport) { + this.transport = transport; + } + + @Nullable + @CheckResult + public static AuthenticatorTransport fromString(String transport) { + switch (transport.toLowerCase()) { + case "usb": return USB; + case "nfc": return NFC; + case "ble": return BLE; + case "internal": return INTERNAL; + default: return null; + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/CollectedClientData.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/CollectedClientData.java new file mode 100644 index 0000000..1cc4c9e --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/CollectedClientData.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; + + +import com.google.auto.value.AutoValue; + + +@AutoValue +public abstract class CollectedClientData { + public abstract String type(); + public abstract byte[] challenge(); + public abstract String origin(); + public abstract String hashAlgorithm(); + + public static CollectedClientData create(String type, byte[] challenge, String origin, String hashAlgorithm) { + return new AutoValue_CollectedClientData(type, challenge, origin, hashAlgorithm); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialDescriptor.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialDescriptor.java new file mode 100644 index 0000000..4547a2f --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialDescriptor.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; + + +import android.os.Parcelable; + +import java.util.List; + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; + + +// https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor +@AutoValue +public abstract class PublicKeyCredentialDescriptor implements Parcelable { + public abstract PublicKeyCredentialType type(); + public abstract byte[] id(); + @Nullable + public abstract List transports(); + + public static PublicKeyCredentialDescriptor create(PublicKeyCredentialType type, byte[] id, List transports) { + return new AutoValue_PublicKeyCredentialDescriptor(type, id, transports); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialEntity.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialEntity.java new file mode 100644 index 0000000..035574e --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialEntity.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; + + +import androidx.annotation.Nullable; + + +public abstract class PublicKeyCredentialEntity { + @Nullable + public abstract String name(); + + @Nullable + public abstract String icon(); +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialParameters.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialParameters.java new file mode 100644 index 0000000..2156af5 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialParameters.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; + + +import android.os.Parcelable; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.internal.cose.CoseIdentifiers.CoseAlg; + + +@AutoValue +public abstract class PublicKeyCredentialParameters implements Parcelable { + abstract Map rawParameters(); + + // Pretty ugly, but we can only parcel String keys in a Map type. + public Map parameters() { + HashMap result = new HashMap<>(); + for (Map.Entry entry : rawParameters().entrySet()) { + result.put(PublicKeyCredentialType.fromString(entry.getKey()), entry.getValue()); + } + return Collections.unmodifiableMap(result); + } + + public static PublicKeyCredentialParameters createSingle(PublicKeyCredentialType type, CoseAlg algorithm) { + Map parameters = Collections.singletonMap(type.type, algorithm); + return new AutoValue_PublicKeyCredentialParameters(parameters); + } + + public static PublicKeyCredentialParameters createDefaultEs256() { + return createSingle(PublicKeyCredentialType.PUBLIC_KEY, CoseAlg.ES256); + } + + public static List createDefaultEs256List() { + return Collections.singletonList(createDefaultEs256()); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialRpEntity.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialRpEntity.java new file mode 100644 index 0000000..accd3fe --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialRpEntity.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; + + +import android.os.Parcelable; + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; + + +@AutoValue +public abstract class PublicKeyCredentialRpEntity extends PublicKeyCredentialEntity implements Parcelable { + @Nullable + public abstract String id(); + + public static PublicKeyCredentialRpEntity create(String id, String name, @Nullable String icon) { + return new AutoValue_PublicKeyCredentialRpEntity(name, icon, id); + } + + public PublicKeyCredentialRpEntity withId(String id) { + return create(id, name(), icon()); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialType.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialType.java new file mode 100644 index 0000000..68f6602 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialType.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; + + +// https://www.w3.org/TR/webauthn/#enumdef-publickeycredentialtype +public enum PublicKeyCredentialType { + PUBLIC_KEY("public-key"); + + public final String type; + + PublicKeyCredentialType(String type) { + this.type = type; + } + + public static PublicKeyCredentialType fromString(String type) { + if ("public-key".equals(type)) { + return PUBLIC_KEY; + } + return null; + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialUserEntity.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialUserEntity.java new file mode 100644 index 0000000..c30d54b --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/PublicKeyCredentialUserEntity.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; + + +import android.os.Parcelable; + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; + + +@AutoValue +public abstract class PublicKeyCredentialUserEntity extends PublicKeyCredentialEntity implements + Parcelable { + public abstract byte[] id(); + @Nullable + public abstract String displayName(); + + public static PublicKeyCredentialUserEntity create(byte[] id, @Nullable String name, + @Nullable String displayName, @Nullable String icon) { + return new AutoValue_PublicKeyCredentialUserEntity(name, icon, id, displayName); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/UserVerificationRequirement.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/UserVerificationRequirement.java new file mode 100644 index 0000000..16976cd --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/UserVerificationRequirement.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; + + +import androidx.annotation.CheckResult; +import androidx.annotation.NonNull; + + +public enum UserVerificationRequirement { + REQUIRED, PREFERRED, DISCOURAGED; + + @NonNull + @CheckResult + public static UserVerificationRequirement fromString(String userVerification) { + if (userVerification == null) { + return PREFERRED; + } + switch (userVerification.toLowerCase()) { + case "required": return REQUIRED; + case "discouraged": return DISCOURAGED; + default: return PREFERRED; + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AttestationConveyancePreference.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AttestationConveyancePreference.java new file mode 100644 index 0000000..780b8f0 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AttestationConveyancePreference.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain.create; + + +public enum AttestationConveyancePreference { + NONE, INDIRECT, DIRECT; + + public static AttestationConveyancePreference fromString(String userVerification) { + if (userVerification == null) { + return NONE; + } + switch (userVerification.toLowerCase()) { + case "direct": return DIRECT; + case "indirect": return INDIRECT; + default: return NONE; + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AttestationObject.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AttestationObject.java new file mode 100644 index 0000000..ad359e4 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AttestationObject.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain.create; + + +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Response; + + +@AutoValue +public abstract class AttestationObject extends Ctap2Response { + // fmt String Required The attestation statement format identifier. + public abstract String fmt(); + // authData Byte Array Required The authenticator data object. + public abstract byte[] authData(); + // attStmt Byte Array, the structure of which depends on the attestation statement format identifier Required The attestation statement, whose format is identified by the "fmt" object member. The client treats it as an opaque object. abstract List versions(); + public abstract byte[] attStmt(); + + + public static AttestationObject create(String fmt, byte[] authData, byte[] attStmt) { + return new AutoValue_AttestationObject(fmt, authData, attStmt); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AttestedCredentialData.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AttestedCredentialData.java new file mode 100644 index 0000000..9d496f4 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AttestedCredentialData.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain.create; + + +import com.google.auto.value.AutoValue; + + +@AutoValue +public abstract class AttestedCredentialData { + private static final byte[] LENGTH_AAGUID = new byte[16]; + + public abstract byte[] aaguid(); + public abstract byte[] credentialId(); + public abstract byte[] credentialPublicKey(); + + public static AttestedCredentialData create(byte[] aaguid, byte[] credentialId, byte[] credentialPublicKey) { + return new AutoValue_AttestedCredentialData(aaguid, credentialId, credentialPublicKey); + } + + AttestedCredentialData withEmptyAaguid() { + return create(LENGTH_AAGUID, credentialId(), credentialPublicKey()); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorAttachment.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorAttachment.java new file mode 100644 index 0000000..5343765 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorAttachment.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain.create; + + +public enum AuthenticatorAttachment { + PLATFORM, CROSS_PLATFORM; + + public static AuthenticatorAttachment fromString(String authenticatorAttachment) { + if (authenticatorAttachment == null) { + return null; + } + switch (authenticatorAttachment.toLowerCase()) { + case "platform": return PLATFORM; + case "cross-platform": return CROSS_PLATFORM; + } + return null; + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorAttestationResponse.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorAttestationResponse.java new file mode 100644 index 0000000..4c67faa --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorAttestationResponse.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain.create; + + +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.domain.AuthenticatorResponse; + + +@AutoValue +public abstract class AuthenticatorAttestationResponse extends AuthenticatorResponse { + public abstract byte[] attestationObject(); + + public static AuthenticatorAttestationResponse create( + byte[] clientData, byte[] attestationObject) { + return new AutoValue_AuthenticatorAttestationResponse(clientData, attestationObject); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorData.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorData.java new file mode 100644 index 0000000..736ba3c --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorData.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain.create; + + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; + + +@AutoValue +public abstract class AuthenticatorData { + // Bit 0: User Present (UP) result. + // 1 means the user is present. + // 0 means the user is not present. + public static final byte FLAG_USER_PRESENT = 1; + // Bit 1: Reserved for future use (RFU1). + private static final byte FLAG_RFU_1 = 1<<1; + // Bit 2: User Verified (UV) result. + // 1 means the user is verified. + // 0 means the user is not verified. + private static final byte FLAG_USER_VERIFIED = 1<<2; + // Bits 3-5: Reserved for future use (RFU2). + private static final byte FLAG_RFU_2 = 1<<3; + private static final byte FLAG_RFU_3 = 1<<4; + private static final byte FLAG_RFU_4 = 1<<5; + // Bit 6: Attested credential data included (AT). + // Indicates whether the authenticator added attested credential data. + public static final byte FLAG_ATTESTED_CREDENTIAL_DATA = 1<<6; + // Bit 7: Extension data included (ED). + // Indicates if the authenticator data has extensions. + public static final byte FLAG_EXTENSION_DATA = (byte) (1<<7); + + // rpIdHash 32 SHA-256 hash of the RP ID the credential is scoped to. + public abstract byte[] rpIdHash(); + + // flags 1 Flags (bit 0 is the least significant bit): + public abstract byte flags(); + + // signCount 4 Signature counter, 32-bit unsigned big-endian integer. + public abstract int sigCounter(); + + // attestedCredentialData variable (if present) attested credential data (if present). See §6.4.1 Attested Credential Data for details. Its length depends on the length of the credential ID and credential public key being attested. + @Nullable + public abstract AttestedCredentialData attestedCredentialData(); + + // extensions variable (if present) Extension-defined authenticator data. This is a CBOR [RFC7049] map with extension identifiers as keys, and authenticator extension outputs as values. See §9 WebAuthn Extensions for details. + @Nullable + public abstract byte[] extensions(); + + public boolean hasAttestedCredentialData() { + return (flags() & FLAG_ATTESTED_CREDENTIAL_DATA) != 0; + } + + public boolean hasExtensionData() { + return (flags() & FLAG_EXTENSION_DATA) != 0; + } + + public static AuthenticatorData create(byte[] rpIdHash, byte flags, int sigCounter, + AttestedCredentialData credentialData, byte[] extensions) { + return new AutoValue_AuthenticatorData(rpIdHash, flags, sigCounter, credentialData, extensions); + } + + public AuthenticatorData withEmptyAaguid() { + AttestedCredentialData attestedCredentialData = attestedCredentialData(); + if (attestedCredentialData != null) { + attestedCredentialData = attestedCredentialData.withEmptyAaguid(); + } + return new AutoValue_AuthenticatorData(rpIdHash(), flags(), sigCounter(), attestedCredentialData, extensions()); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorSelectionCriteria.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorSelectionCriteria.java new file mode 100644 index 0000000..f518c11 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorSelectionCriteria.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain.create; + + +import android.os.Parcelable; + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.domain.UserVerificationRequirement; + + +@AutoValue +public abstract class AuthenticatorSelectionCriteria implements Parcelable { + @Nullable + public abstract AuthenticatorAttachment authenticatorAttachment(); + public abstract boolean requireResidentKey(); + public abstract UserVerificationRequirement userVerification(); + + public static AuthenticatorSelectionCriteria create( + @Nullable AuthenticatorAttachment authenticatorAttachment, boolean requireResidentKey, + UserVerificationRequirement userVerification) { + return new AutoValue_AuthenticatorSelectionCriteria(authenticatorAttachment, + requireResidentKey, userVerification); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/CredentialCreationData.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/CredentialCreationData.java new file mode 100644 index 0000000..298f352 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/CredentialCreationData.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain.create; + + +import com.google.auto.value.AutoValue; + + +@AutoValue +public abstract class CredentialCreationData { + public abstract AttestationObject attestationObjectResult(); + public abstract byte[] clientDataJSONResult(); + public abstract AttestationConveyancePreference attestationConveyancePreferenceOption(); + + public static CredentialCreationData create( + AttestationObject attestationObjectResult, + byte[] clientDataJSONResult, + AttestationConveyancePreference attestationConveyancePreferenceOption) { + return new AutoValue_CredentialCreationData( + attestationObjectResult, clientDataJSONResult, attestationConveyancePreferenceOption); + } + + // Missing: + // public abstract AuthenticationExtensionsClientOutputs clientExtensionResults(); +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/PublicKeyCredentialCreationOptions.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/PublicKeyCredentialCreationOptions.java new file mode 100644 index 0000000..539c08a --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/PublicKeyCredentialCreationOptions.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain.create; + + +import java.util.List; + +import android.os.Parcelable; + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; +import de.cotech.hw.fido2.domain.PublicKeyCredentialParameters; +import de.cotech.hw.fido2.domain.PublicKeyCredentialRpEntity; +import de.cotech.hw.fido2.domain.PublicKeyCredentialUserEntity; + + +@AutoValue +public abstract class PublicKeyCredentialCreationOptions implements Parcelable { + public abstract PublicKeyCredentialRpEntity rp(); + public abstract PublicKeyCredentialUserEntity user(); + public abstract byte[] challenge(); + public abstract List pubKeyCredParams(); + @Nullable + public abstract Long timeout(); + public abstract AuthenticatorSelectionCriteria authenticatorSelection(); + @Nullable + public abstract List excludeCredentials(); + public abstract AttestationConveyancePreference attestation(); + + public static PublicKeyCredentialCreationOptions create( + PublicKeyCredentialRpEntity rp, + PublicKeyCredentialUserEntity user, + byte[] challenge, + List pubKeyCredParams, + @Nullable Long timeout, + AuthenticatorSelectionCriteria authenticatorSelection, + @Nullable List excludeCredentials, + AttestationConveyancePreference attestation + ) { + return new AutoValue_PublicKeyCredentialCreationOptions( + rp, user, challenge, pubKeyCredParams, timeout, + authenticatorSelection, excludeCredentials, attestation); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/AssertionCreationData.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/AssertionCreationData.java new file mode 100644 index 0000000..df7ae2f --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/AssertionCreationData.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain.get; + + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; + + +@AutoValue +public abstract class AssertionCreationData { + public abstract byte[] credentialIdResult(); + public abstract byte[] clientDataJSONResult(); + public abstract byte[] authenticatorDataResult(); + public abstract byte[] signatureResult(); + @Nullable + public abstract byte[] userHandleResult(); + + public static AssertionCreationData create(byte[] credentialId, byte[] clientDataJSON, byte[] authenticatorData, byte[] signature, @Nullable byte[] userHandle) { + return new AutoValue_AssertionCreationData(credentialId, clientDataJSON, authenticatorData, signature, userHandle); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/AuthenticatorAssertionResponse.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/AuthenticatorAssertionResponse.java new file mode 100644 index 0000000..1a12217 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/AuthenticatorAssertionResponse.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain.get; + + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.domain.AuthenticatorResponse; + + +@AutoValue +public abstract class AuthenticatorAssertionResponse extends AuthenticatorResponse { + public abstract byte[] authenticatorData(); + public abstract byte[] signature(); + @Nullable + public abstract byte[] userHandle(); + + public static AuthenticatorAssertionResponse create(byte[] clientDataJson, byte[] authenticatorData, byte[] signature, @Nullable byte[] userHandle) { + return new AutoValue_AuthenticatorAssertionResponse(clientDataJson, authenticatorData, signature, userHandle); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/PublicKeyCredentialRequestOptions.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/PublicKeyCredentialRequestOptions.java new file mode 100644 index 0000000..1dd9baa --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/PublicKeyCredentialRequestOptions.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain.get; + + +import java.util.List; + +import android.os.Parcelable; + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; +import de.cotech.hw.fido2.domain.UserVerificationRequirement; + + +@AutoValue +public abstract class PublicKeyCredentialRequestOptions implements Parcelable { + public abstract byte[] challenge(); + @Nullable + public abstract Long timeout(); + @Nullable + public abstract String rpId(); + @Nullable + public abstract List allowCredentials(); + @Nullable + public abstract UserVerificationRequirement userVerification(); + + public static PublicKeyCredentialRequestOptions create( + byte[] challenge, + Long timeout, + String rpId, + List allowCredentials, + UserVerificationRequirement userVerification + ) { + return new AutoValue_PublicKeyCredentialRequestOptions( + challenge, timeout, rpId, allowCredentials, userVerification); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinBlockedException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinBlockedException.java new file mode 100644 index 0000000..0058c27 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinBlockedException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.exceptions; + + +import java.io.IOException; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + + +public class FidoClientPinBlockedException extends IOException { + @RestrictTo(Scope.LIBRARY_GROUP) + public FidoClientPinBlockedException() { + super("This authenticator is blocked, it must be reset before use."); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinInvalidException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinInvalidException.java new file mode 100644 index 0000000..13bddd8 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinInvalidException.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.exceptions; + + +import java.io.IOException; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + + +public class FidoClientPinInvalidException extends IOException { + public final int retriesLeft; + + @RestrictTo(Scope.LIBRARY_GROUP) + public FidoClientPinInvalidException(int retriesLeft) { + super("Invalid PIN. " + getRetriesText(retriesLeft)); + this.retriesLeft = retriesLeft; + } + + public int getRetriesLeft() { + return retriesLeft; + } + + private static String getRetriesText(int retriesLeft) { + if (retriesLeft == 0) { + return "Too many attempts, authenticator is now blocked."; + } + if (retriesLeft == 1) { + return "One attempt left."; + } + return retriesLeft + " attempts left."; + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinLastAttemptException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinLastAttemptException.java new file mode 100644 index 0000000..3978ffd --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinLastAttemptException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.exceptions; + + +import java.io.IOException; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + + +public class FidoClientPinLastAttemptException extends IOException { + @RestrictTo(Scope.LIBRARY_GROUP) + public FidoClientPinLastAttemptException() { + super("Only one client PIN attempt left, need explicit confirmation."); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinNotSetException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinNotSetException.java new file mode 100644 index 0000000..532b624 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinNotSetException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.exceptions; + + +import java.io.IOException; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + + +public class FidoClientPinNotSetException extends IOException { + @RestrictTo(Scope.LIBRARY_GROUP) + public FidoClientPinNotSetException() { + super("User verification is required, but not set up on authenticator!"); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinNotSupportedException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinNotSupportedException.java new file mode 100644 index 0000000..36bac61 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinNotSupportedException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.exceptions; + + +import java.io.IOException; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + + +public class FidoClientPinNotSupportedException extends IOException { + @RestrictTo(Scope.LIBRARY_GROUP) + public FidoClientPinNotSupportedException() { + super("User verification is required, but not supported by authenticator!"); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinRequiredException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinRequiredException.java new file mode 100644 index 0000000..2cccfd7 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinRequiredException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.exceptions; + + +import java.io.IOException; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + + +public class FidoClientPinRequiredException extends IOException { + @RestrictTo(Scope.LIBRARY_GROUP) + public FidoClientPinRequiredException() { + super("User verification is required, but no PIN provided!"); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinTooShortException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinTooShortException.java new file mode 100644 index 0000000..31f7923 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoClientPinTooShortException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.exceptions; + + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + +import java.io.IOException; + + +public class FidoClientPinTooShortException extends IOException { + @RestrictTo(Scope.LIBRARY_GROUP) + public FidoClientPinTooShortException() { + super("PIN must be 4 or more characters"); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoInvalidCredentialException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoInvalidCredentialException.java new file mode 100644 index 0000000..cb3e0d0 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoInvalidCredentialException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.exceptions; + + +import java.io.IOException; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + + +public class FidoInvalidCredentialException extends IOException { + @RestrictTo(Scope.LIBRARY_GROUP) + public FidoInvalidCredentialException() { + super("No valid credentials."); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoPresenceRequiredException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoPresenceRequiredException.java new file mode 100644 index 0000000..c5d993a --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoPresenceRequiredException.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.exceptions; + + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import de.cotech.hw.exceptions.ConditionsNotSatisfiedException; + + +public class FidoPresenceRequiredException extends ConditionsNotSatisfiedException { + public static final int SW_TEST_OF_USER_PRESENCE_REQUIRED = SW_CONDITIONS_NOT_SATISFIED; + + @RestrictTo(Scope.LIBRARY_GROUP) + public FidoPresenceRequiredException() { + super("Security Key returned error, user presence is required for this operation"); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoResidentKeyNoCredentialException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoResidentKeyNoCredentialException.java new file mode 100644 index 0000000..303c43b --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoResidentKeyNoCredentialException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.exceptions; + + +import java.io.IOException; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + + +public class FidoResidentKeyNoCredentialException extends IOException { + @RestrictTo(Scope.LIBRARY_GROUP) + public FidoResidentKeyNoCredentialException() { + super("No key available on authenticator for this relying party."); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoResidentKeyNotSupportedException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoResidentKeyNotSupportedException.java new file mode 100644 index 0000000..761e964 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoResidentKeyNotSupportedException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.exceptions; + + +import java.io.IOException; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + + +public class FidoResidentKeyNotSupportedException extends IOException { + @RestrictTo(Scope.LIBRARY_GROUP) + public FidoResidentKeyNotSupportedException() { + super("Requested resident key, but not supported by this authenticator."); + } +} diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SecurityKeyLostException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoSecurityError.java similarity index 75% rename from hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SecurityKeyLostException.java rename to hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoSecurityError.java index 5434d47..f90f6f2 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/exceptions/SecurityKeyLostException.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoSecurityError.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -22,18 +22,14 @@ * along with this program. If not, see . */ -package de.cotech.hw.exceptions; +package de.cotech.hw.fido2.exceptions; import java.io.IOException; -public class SecurityKeyLostException extends IOException { - public SecurityKeyLostException(Throwable cause) { - super("Security Key is no longer connected.", cause); - } - - public SecurityKeyLostException() { - this(null); +public class FidoSecurityError extends IOException { + public FidoSecurityError(String message) { + super(message); } } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoWrongKeyHandleException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoWrongKeyHandleException.java new file mode 100644 index 0000000..787afac --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/exceptions/FidoWrongKeyHandleException.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.exceptions; + + +import de.cotech.hw.exceptions.WrongDataException; + + +public class FidoWrongKeyHandleException extends WrongDataException { + public static final int SW_WRONG_KEY_HANDLE = WrongDataException.SW_WRONG_DATA; + + public FidoWrongKeyHandleException() { + super("Security Key returned error WRONG_KEY_HANDLE (0x6A80)"); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/AuthenticationPin.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/AuthenticationPin.java new file mode 100644 index 0000000..fb10e5a --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/AuthenticationPin.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal; + + +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.internal.webauthn.WebauthnResponse; + + +@AutoValue +public abstract class AuthenticationPin implements WebauthnResponse { + public abstract String pin(); + + public static AuthenticationPin create(String pin) { + return new AutoValue_AuthenticationPin(pin); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/Fido2AppletConnection.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/Fido2AppletConnection.java new file mode 100644 index 0000000..646948f --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/Fido2AppletConnection.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal; + + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + +import de.cotech.hw.SecurityKeyException; +import de.cotech.hw.exceptions.AppletFileNotFoundException; +import de.cotech.hw.exceptions.ClaNotSupportedException; +import de.cotech.hw.exceptions.InsNotSupportedException; +import de.cotech.hw.exceptions.SelectAppletException; +import de.cotech.hw.exceptions.WrongRequestLengthException; +import de.cotech.hw.fido2.exceptions.FidoPresenceRequiredException; +import de.cotech.hw.fido2.exceptions.FidoWrongKeyHandleException; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Command; +import de.cotech.hw.fido2.internal.ctap2.Ctap2CommandApduTransformer; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Exception; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Response; +import de.cotech.hw.fido2.internal.ctap2.CtapErrorResponse; +import de.cotech.hw.fido2.internal.ctap2.commands.getInfo.AuthenticatorGetInfo; +import de.cotech.hw.fido2.internal.ctap2.commands.getInfo.AuthenticatorGetInfoResponse; +import de.cotech.hw.fido2.internal.pinauth.PinToken; +import de.cotech.hw.internal.iso7816.CommandApdu; +import de.cotech.hw.internal.iso7816.ResponseApdu; +import de.cotech.hw.internal.transport.SecurityKeyInfo.TransportType; +import de.cotech.hw.internal.transport.Transport; +import de.cotech.hw.util.Hex; +import de.cotech.hw.util.HwTimber; + + +@RestrictTo(Scope.LIBRARY_GROUP) +public class Fido2AppletConnection { + private static final int APDU_SW1_RESPONSE_AVAILABLE = 0x61; + private static final int RESPONSE_SW1_INCORRECT_LENGTH = 0x6C; + + private static final List FIDO_AID_PREFIXES = Arrays.asList( + // see to "FIDO U2F NFC protocol", Section 5. Applet selection + // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-nfc-protocol-v1.2-ps-20170411.html + Hex.decodeHexOrFail("A0000006472F0001"), + // Workaround for Solokey for firmware < 2.4.0: https://github.com/solokeys/solo/issues/213 + Hex.decodeHexOrFail("A0000006472F000100"), + // old Yubico demo applet AID + Hex.decodeHexOrFail("A0000005271002") + ); + + @NonNull + private final Transport transport; + @NonNull + private final Fido2CommandApduFactory commandFactory; + @NonNull + private final Ctap2CommandApduTransformer ctap2CommandApduTransformer; + + private boolean isFidoAppletConnected; + private AuthenticatorGetInfoResponse ctap2Info; + private boolean isForceCtap1; + + private PinToken cachedPinToken; + + public static Fido2AppletConnection getInstanceForTransport(@NonNull Transport transport) { + return new Fido2AppletConnection(transport, new Fido2CommandApduFactory(), new Ctap2CommandApduTransformer()); + } + + private Fido2AppletConnection(@NonNull Transport transport, @NonNull Fido2CommandApduFactory commandFactory, + @NonNull Ctap2CommandApduTransformer ctap2CommandApduTransformer) { + this.transport = transport; + this.commandFactory = commandFactory; + this.ctap2CommandApduTransformer = ctap2CommandApduTransformer; + } + + // region connection management + + public void connectIfNecessary() throws IOException { + if (isFidoAppletConnected) { + return; + } + + connectToDevice(); + } + + private void connectToDevice() throws IOException { + try { + if (transport.getTransportType() == TransportType.USB_CTAPHID) { + HwTimber.d("Using USB U2F HID as a transport. No need to select AID."); + byte[] versionBytes = readVersion(); + checkVersionOrThrow(versionBytes); + } else { + byte[] selectedAid = selectFilesFromPrefixOrFail(); + HwTimber.d("Connected to AID %s", Hex.encodeHexString(selectedAid)); + } + + try { + ctap2Info = ctap2AuthenticatorGetInfo(); + HwTimber.d("Call to AuthenticatorGetInfo returns valid response - using CTAP2"); + HwTimber.d(ctap2Info.toString()); + } catch (IOException e) { + HwTimber.d("Call to AuthenticatorGetInfo returned no valid response - using CTAP1"); + } + + isFidoAppletConnected = true; + } catch (IOException e) { + transport.release(); + throw e; + } + } + + private AuthenticatorGetInfoResponse ctap2AuthenticatorGetInfo() throws IOException { + return ctap2CommunicateOrThrow(AuthenticatorGetInfo.create()); + } + + public T ctap2CommunicateOrThrow(Ctap2Command ctap2Command) + throws IOException { + if (!isCtap2Capable()) { + HwTimber.w("Attempting to send CTAP2 command, but CTAP2 is not supported. " + + "This will probably cause an error."); + } + CommandApdu commandApdu = ctap2CommandApduTransformer.toCommandApdu(ctap2Command); + ResponseApdu responseApdu = communicateOrThrow(commandApdu); + Ctap2Response ctap2Response = ctap2ResponseFromResponseApdu(ctap2Command, responseApdu); + // noinspection unchecked, this is ensured in Ctap2ResponseTransformer + return (T) ctap2Response; + } + + private Ctap2Response ctap2ResponseFromResponseApdu(Ctap2Command command, ResponseApdu responseApdu) + throws IOException { + byte[] data = responseApdu.getData(); + if (data[0] != CtapErrorResponse.CTAP2_OK) { + throw new Ctap2Exception(CtapErrorResponse.create(data[0])); + } + + byte[] responseData = de.cotech.hw.util.Arrays.copyOfRange(data, 1, data.length); + return command.getResponseFactory().createResponse(responseData); + } + + private byte[] selectFilesFromPrefixOrFail() throws IOException { + for (byte[] fileAid : FIDO_AID_PREFIXES) { + byte[] initializedAid = selectFileOrFail(fileAid); + if (initializedAid != null) { + return initializedAid; + } + } + throw new SelectAppletException(FIDO_AID_PREFIXES, "FIDO U2F or CTAP2"); + } + + private void checkVersionOrThrow(byte[] versionBytes) throws IOException { + String version = new String(versionBytes, Charset.forName("ASCII")); + + if ("U2F_V2".equals(version) || "FIDO_2_0".equals(version)) { + HwTimber.d("U2F applet answered correctly with version U2F_V2 or FIDO_2_0"); + } else { + HwTimber.e("Applet did NOT answer with a correct version string!"); + throw new IOException("Applet replied with incorrect version string!"); + } + } + + private byte[] selectFileOrFail(byte[] fileAid) throws IOException { + CommandApdu select = commandFactory.createSelectFileCommand(fileAid); + + try { + ResponseApdu response = communicateOrThrow(select); + + // "FIDO authenticator SHALL reply with its version string in the successful response" + // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-nfc-protocol-v1.2-ps-20170411.html + checkVersionOrThrow(response.getData()); + return fileAid; + } catch (AppletFileNotFoundException e) { + return null; + } + } + + // endregion + + // region communication + + // see "FIDO U2F Raw Message Formats", Section 3.3 Status Codes + // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html + public ResponseApdu communicateOrThrow(CommandApdu commandApdu) throws IOException { + ResponseApdu response = communicate(commandApdu); + + if (response.isSuccess()) { + return response; + } + + switch (response.getSw()) { + case FidoPresenceRequiredException.SW_TEST_OF_USER_PRESENCE_REQUIRED: + throw new FidoPresenceRequiredException(); + case FidoWrongKeyHandleException.SW_WRONG_KEY_HANDLE: + throw new FidoWrongKeyHandleException(); + case AppletFileNotFoundException.SW_FILE_NOT_FOUND: + throw new AppletFileNotFoundException(); + case ClaNotSupportedException.SW_CLA_NOT_SUPPORTED: + throw new ClaNotSupportedException(); + case InsNotSupportedException.SW_INS_NOT_SUPPORTED: + throw new InsNotSupportedException(); + case WrongRequestLengthException.SW_WRONG_REQUEST_LENGTH: + throw new WrongRequestLengthException(); + default: + throw new SecurityKeyException("UNKNOWN", response.getSw()); + } + } + + // ISO/IEC 7816-4 + private ResponseApdu communicate(CommandApdu commandApdu) throws IOException { + ResponseApdu lastResponse; + + lastResponse = sendWithChaining(commandApdu); + if (lastResponse.getSw1() == RESPONSE_SW1_INCORRECT_LENGTH && lastResponse.getSw2() != 0) { + commandApdu = commandApdu.withNe(lastResponse.getSw2()); + lastResponse = sendWithChaining(commandApdu); + } + lastResponse = readChainedResponseIfAvailable(lastResponse); + + return lastResponse; + } + + // ISO/IEC 7816-4 + @NonNull + private ResponseApdu sendWithChaining(CommandApdu commandApdu) throws IOException { + /* CTAP2 Spec: + * "If the request was encoded using extended length APDU encoding, + * the authenticator MUST respond using the extended length APDU response format." + * + * "If the request was encoded using short APDU encoding, + * the authenticator MUST respond using ISO 7816-4 APDU chaining." + * + * https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#nfc-fragmentation + * + * In the best case, extended length is supported by device and authenticator, so we don't need to + * parse chained APDUs coming from the authenticator. + * + * We do *not* check for `transport.isExtendedLengthSupported()` here! There are phones (including + * the Nexus 5X, Nexus 6P) that return "false" to this, but some Security Keys (like Yubikey Neo) still + * require us to send extended APDUs. So what we do is, send an extended APDU, and if that doesn't + * work, fall back to a short one. + */ + if (!transport.isExtendedLengthSupported()) { + HwTimber.w("Transport protocol does not support extended length. Probably an old device with NFC, such as Nexus 5X, Nexus 6P. We still try sending extended length!"); + } + + ResponseApdu response = transport.transceive(commandApdu.withExtendedApduNe()); + if (response.getSw() != WrongRequestLengthException.SW_WRONG_REQUEST_LENGTH) { + return response; + } else { + HwTimber.d("Received WRONG_REQUEST_LENGTH error. Retrying with short APDU Ne."); + } + + if (commandFactory.isSuitableForSingleShortApdu(commandApdu)) { + return transport.transceive(commandApdu.withShortApduNe()); + } + + ResponseApdu lastResponse = null; + List chainedApdus = commandFactory.createChainedApdus(commandApdu); + for (int i = 0, totalCommands = chainedApdus.size(); i < totalCommands; i++) { + CommandApdu chainedApdu = chainedApdus.get(i); + lastResponse = transport.transceive(chainedApdu); + + boolean isLastCommand = (i == totalCommands - 1); + if (!isLastCommand && !lastResponse.isSuccess()) { + throw new IOException("Failed to chain apdu " + + "(" + i + "/" + (totalCommands - 1) + ", last SW: " + Integer.toHexString(lastResponse.getSw()) + ")"); + } + } + + if (lastResponse == null) { + throw new IllegalStateException(); + } + + return lastResponse; + } + + // ISO/IEC 7816-4 + @NonNull + private ResponseApdu readChainedResponseIfAvailable(ResponseApdu lastResponse) throws + IOException { + if (lastResponse.getSw1() != APDU_SW1_RESPONSE_AVAILABLE) { + return lastResponse; + } + + ByteArrayOutputStream result = new ByteArrayOutputStream(); + result.write(lastResponse.getData()); + + do { + // GET RESPONSE ISO/IEC 7816-4 par.7.6.1 + CommandApdu getResponse = commandFactory.createGetResponseCommand(lastResponse.getSw2()); + lastResponse = transport.transceive(getResponse); + result.write(lastResponse.getData()); + } while (lastResponse.getSw1() == APDU_SW1_RESPONSE_AVAILABLE); + + result.write(lastResponse.getSw1()); + result.write(lastResponse.getSw2()); + + return ResponseApdu.fromBytes(result.toByteArray()); + } + + // endregion + + @NonNull + public Fido2CommandApduFactory getCommandFactory() { + return commandFactory; + } + + /** + * GetVersion Request + * https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html + *

+ * The FIDO Client can query the U2F token about the U2F protocol version that it implements. + * + * @return should return "U2F_V2" + */ + private byte[] readVersion() throws IOException { + CommandApdu getDataUserIdCommand = commandFactory.createVersionCommand(); + ResponseApdu responseApdu = communicateOrThrow(getDataUserIdCommand); + return responseApdu.getData(); + } + + public boolean isConnected() { + return transport.isConnected(); + } + + public boolean isSupportResidentKeys() { + return ctap2Info != null && ctap2Info.options().rk(); + } + + public boolean isSupportClientPin() { + return ctap2Info != null && ctap2Info.options().clientPin() != null; + } + + public boolean isClientPinSet() { + if (ctap2Info == null) { + return false; + } + Boolean clientPin = ctap2Info.options().clientPin(); + return clientPin != null && clientPin; + } + + public boolean isSupportUserVerification() { + return ctap2Info != null && ctap2Info.options().uv() != null; + } + + public boolean isSupportUserPresence() { + return ctap2Info != null && ctap2Info.options().up(); + } + + public boolean isCtap2Capable() { + return !isForceCtap1 && ctap2Info != null; + } + + public void setForceCtap1(boolean isForceCtap1) { + this.isForceCtap1 = isForceCtap1; + } + + @Nullable + public PinToken getCachedPinToken() { + return cachedPinToken; + } + + public void setCachedPinToken(PinToken pinToken) { + this.cachedPinToken = pinToken; + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/Fido2CommandApduDescriber.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/Fido2CommandApduDescriber.java new file mode 100644 index 0000000..0464472 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/Fido2CommandApduDescriber.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal; + + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import de.cotech.hw.internal.iso7816.CommandApdu; +import de.cotech.hw.internal.iso7816.CommandApduDescriber; +import de.cotech.hw.util.Hex; + + +@RestrictTo(Scope.LIBRARY_GROUP) +public class Fido2CommandApduDescriber implements CommandApduDescriber { + @Override + public String describe(CommandApdu commandApdu) { + return Hex.encodeHexString(commandApdu.toBytes()); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/Fido2CommandApduFactory.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/Fido2CommandApduFactory.java new file mode 100644 index 0000000..cedd366 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/Fido2CommandApduFactory.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal; + + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import de.cotech.hw.internal.iso7816.CommandApdu; + + +/** + * Follows "FIDO U2F Raw Message Formats" v1.2 + * https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html + */ +@RestrictTo(Scope.LIBRARY_GROUP) +public class Fido2CommandApduFactory { + private static final Fido2CommandApduDescriber DESCRIBER = new Fido2CommandApduDescriber(); + + private static final int MASK_CLA_CHAINING = 1 << 4; + + private static final int CLA = 0x00; + private static final int INS_SELECT_FILE = 0xA4; + private static final int P1_SELECT_FILE = 0x04; + private static final int INS_GET_RESPONSE = 0xC0; + + private static final int P1_EMPTY = 0x00; + private static final int P2_EMPTY = 0x00; + + // "FIDO U2F Raw Message Formats", Section 3.1.1 Command and parameter values + // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html + private static final int U2F_REGISTER = 0x01; + private static final int U2F_AUTHENTICATE = 0x02; + private static final int U2F_VERSION = 0x03; + + private static final int U2F_AUTHENTICATE_P1_ENFORCE_USER_PRESENCE_AND_SIGN = 0x03; + private static final int U2F_AUTHENTICATE_P1_CHECK_ONLY = 0x07; + private static final int U2F_AUTHENTICATE_P1_DONT_ENFORCE_USER_PRESENCE_AND_SIGN = 0x08; + + + @NonNull + public CommandApdu createRegistrationCommand(byte[] data) { + return CommandApdu.create(CLA, U2F_REGISTER, U2F_AUTHENTICATE_P1_ENFORCE_USER_PRESENCE_AND_SIGN, P2_EMPTY, data).withDescriber(DESCRIBER); + } + + @NonNull + public CommandApdu createAuthenticationCommand(byte[] data) { + return CommandApdu.create(CLA, U2F_AUTHENTICATE, U2F_AUTHENTICATE_P1_ENFORCE_USER_PRESENCE_AND_SIGN, P2_EMPTY, data).withDescriber(DESCRIBER); + } + + @NonNull + public CommandApdu createVersionCommand() { + return CommandApdu.create(CLA, U2F_VERSION, P1_EMPTY, P2_EMPTY).withDescriber(DESCRIBER); + } + + // ISO/IEC 7816-4 + // SELECT command always as short APDU + @NonNull + public CommandApdu createSelectFileCommand(byte[] fileAid) { + return CommandApdu.create(CLA, INS_SELECT_FILE, P1_SELECT_FILE, P2_EMPTY, fileAid, CommandApdu.MAX_APDU_NE_SHORT).withDescriber(DESCRIBER); + } + + // GET RESPONSE ISO/IEC 7816-4 par.7.6.1 + @NonNull + public CommandApdu createGetResponseCommand(int lastResponseSw2) { + return CommandApdu.create(CLA, INS_GET_RESPONSE, P1_EMPTY, P2_EMPTY, lastResponseSw2).withDescriber(DESCRIBER); + } + + // ISO/IEC 7816-4 + @NonNull + public List createChainedApdus(CommandApdu apdu) { + ArrayList result = new ArrayList<>(); + + int offset = 0; + byte[] data = apdu.getData(); + while (offset < data.length) { + int curLen = Math.min(CommandApdu.MAX_APDU_NC_SHORT, data.length - offset); + boolean last = offset + curLen >= data.length; + int cla = apdu.getCLA() + (last ? 0 : MASK_CLA_CHAINING); + + CommandApdu cmd; + if (last) { + // TODO: check this! + int ne = Math.min(apdu.getNe(), CommandApdu.MAX_APDU_NE_SHORT); + cmd = CommandApdu.create(cla, apdu.getINS(), apdu.getP1(), apdu.getP2(), data, offset, curLen, ne, DESCRIBER); + } else { + cmd = CommandApdu.create(cla, apdu.getINS(), apdu.getP1(), apdu.getP2(), data, offset, curLen, 0, DESCRIBER); + } + result.add(cmd); + + offset += curLen; + } + + return result; + } + + public boolean isSuitableForSingleShortApdu(CommandApdu apdu) { + return apdu.getNc() <= CommandApdu.MAX_APDU_NC_SHORT; + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/GenericFido2SecurityKeyDialogPresenter.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/GenericFido2SecurityKeyDialogPresenter.java new file mode 100644 index 0000000..432c770 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/GenericFido2SecurityKeyDialogPresenter.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal; + +import android.content.Context; + +import androidx.annotation.RestrictTo; + +import java.io.IOException; + +import de.cotech.hw.SecurityKey; +import de.cotech.hw.SecurityKeyException; +import de.cotech.hw.exceptions.SecurityKeyDisconnectedException; +import de.cotech.hw.ui.R; +import de.cotech.hw.secrets.ByteSecret; +import de.cotech.hw.ui.SecurityKeyDialogOptions; +import de.cotech.hw.ui.internal.SecurityKeyDialogPresenter; +import de.cotech.hw.util.HwTimber; + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class GenericFido2SecurityKeyDialogPresenter extends SecurityKeyDialogPresenter { + + public GenericFido2SecurityKeyDialogPresenter(View view, Context context, SecurityKeyDialogOptions options) { + super(view, context, options); + } + + @Override + public void handleError(IOException exception) { + HwTimber.d(exception); + + switch (currentScreen) { + case NORMAL_SECURITY_KEY: + case NORMAL_SECURITY_KEY_HOLD: { + try { + throw exception; + } catch (SecurityKeyException e) { + gotoErrorScreenAndDelayedScreen(exception.getMessage(), + Screen.NORMAL_ERROR, Screen.NORMAL_SECURITY_KEY); + } catch (SecurityKeyDisconnectedException e) { + gotoScreen(Screen.NORMAL_SECURITY_KEY); + } catch (IOException e) { + view.updateErrorViewText(context.getString(R.string.hwsecurity_fido_error_internal, e.getMessage())); + gotoScreen(Screen.NORMAL_ERROR); + } + break; + } + default: + HwTimber.d("handleError unhandled screen: %s", currentScreen.name()); + } + } + + @Override + public void updateSecurityKeyPinUsingPuk(SecurityKey securityKey, ByteSecret puk, ByteSecret newPin) throws IOException { + throw new IOException("RESET_PIN mode is not supported in FIDO2 mode"); + } + + @Override + public boolean isSecurityKeyEmpty(SecurityKey securityKey) throws IOException { + HwTimber.e("SETUP mode is not supported in FIDO2 mode"); + return true; + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/async/Ctap2Fido2OperationThread.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/async/Ctap2Fido2OperationThread.java new file mode 100644 index 0000000..113598c --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/async/Ctap2Fido2OperationThread.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.async; + + +import java.io.IOException; + +import android.os.Handler; + +import androidx.annotation.UiThread; +import androidx.annotation.WorkerThread; +import de.cotech.hw.fido2.Ctap2Callback; +import de.cotech.hw.fido2.internal.Fido2AppletConnection; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Command; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Response; + + +public class Ctap2Fido2OperationThread extends Fido2OperationThread { + private final Ctap2Command ctap2Command; + private final Ctap2Callback callback; + + public Ctap2Fido2OperationThread( + Fido2AppletConnection fido2AppletConnection, Handler handler, + Ctap2Command ctap2Command, Ctap2Callback callback, int userPresenceCheckDelayMs) { + super(fido2AppletConnection, handler, userPresenceCheckDelayMs); + this.ctap2Command = ctap2Command; + this.callback = callback; + } + + @WorkerThread + CR performOperation() throws IOException { + return fido2AppletConnection.ctap2CommunicateOrThrow(ctap2Command); + } + + @Override + @UiThread + void deliverResponse(CR response) { + callback.onResponse(response); + } + + @Override + @UiThread + void deliverIoException(IOException e) { + callback.onIoException(e); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/async/Fido2AsyncOperationManager.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/async/Fido2AsyncOperationManager.java new file mode 100644 index 0000000..5cb9330 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/async/Fido2AsyncOperationManager.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.async; + + +import androidx.annotation.AnyThread; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.LifecycleOwner; +import de.cotech.hw.fido2.internal.utils.AndroidUtils; + + +@RestrictTo(Scope.LIBRARY_GROUP) +public class Fido2AsyncOperationManager { + private final Object asyncOperationLock; + @VisibleForTesting + Fido2OperationThread asyncOperationThread; + + public Fido2AsyncOperationManager() { + asyncOperationLock = new Object(); + } + + @AnyThread + public void startAsyncOperation(LifecycleOwner lifecycleOwner, Fido2OperationThread operationThread) { + synchronized (asyncOperationLock) { + if (asyncOperationThread != null) { + asyncOperationThread.interrupt(); + asyncOperationThread = null; + } + + asyncOperationThread = operationThread; + asyncOperationThread.setFido2AsyncOperationManager(this); + asyncOperationThread.start(); + AndroidUtils.addLifecycleObserver(lifecycleOwner, asyncOperationThread); + } + } + + @AnyThread + public void clearAsyncOperation() { + clearAsyncOperation(true, null); + } + + @AnyThread + void clearAsyncOperation(boolean interrupt, Thread specificThread) { + synchronized (asyncOperationLock) { + if (specificThread != null && asyncOperationThread != specificThread) { + if (interrupt) { + specificThread.interrupt(); + } + return; + } + if (asyncOperationThread != null && asyncOperationThread.isAlive() && interrupt) { + asyncOperationThread.interrupt(); + } + asyncOperationThread = null; + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/async/Fido2OperationThread.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/async/Fido2OperationThread.java new file mode 100644 index 0000000..ec72faa --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/async/Fido2OperationThread.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.async; + + +import java.io.IOException; + +import android.os.Handler; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import androidx.annotation.UiThread; +import androidx.annotation.WorkerThread; +import androidx.lifecycle.Lifecycle.Event; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import de.cotech.hw.exceptions.SecurityKeyDisconnectedException; +import de.cotech.hw.fido2.exceptions.FidoPresenceRequiredException; +import de.cotech.hw.fido2.internal.Fido2AppletConnection; +import de.cotech.hw.util.HwTimber; + + +@RestrictTo(Scope.LIBRARY_GROUP) +abstract class Fido2OperationThread extends Thread implements LifecycleObserver { + private Fido2AsyncOperationManager fido2AsyncOperationManager; + private final Handler handler; + private final int presenceCheckDelayMs; + final Fido2AppletConnection fido2AppletConnection; + + Fido2OperationThread(Fido2AppletConnection fido2AppletConnection, Handler handler, int presenceCheckDelayMs) { + this.fido2AppletConnection = fido2AppletConnection; + this.handler = handler; + this.presenceCheckDelayMs = presenceCheckDelayMs; + } + + @WorkerThread + void prepareOperation() throws InterruptedException { + + } + + @WorkerThread + abstract T performOperation() throws IOException, InterruptedException; + @UiThread + abstract void deliverResponse(T response); + @UiThread + abstract void deliverIoException(IOException e); + + void setFido2AsyncOperationManager(Fido2AsyncOperationManager fido2AsyncOperationManager) { + this.fido2AsyncOperationManager = fido2AsyncOperationManager; + } + + @Override + public void run() { + try { + prepareOperation(); + } catch (InterruptedException e) { + fido2AsyncOperationManager.clearAsyncOperation(false, this); + return; + } + while (!isInterrupted() && fido2AppletConnection.isConnected()) { + try { + T response = performOperation(); + postToHandler(() -> deliverResponse(response)); + break; + } catch (InterruptedException e) { + HwTimber.e("Fido 2 operation was interrupted"); + break; + } catch (SecurityKeyDisconnectedException e) { + HwTimber.e("Transport gone during fido 2 operation"); + break; + } catch (FidoPresenceRequiredException e) { + try { + Thread.sleep(presenceCheckDelayMs); + } catch (InterruptedException e1) { + break; + } + } catch (IOException e) { + if (e.getCause() instanceof InterruptedException) { + HwTimber.e("Fido 2 operation was interrupted"); + break; + } + postToHandler(() -> deliverIoException(e)); + break; + } + } + fido2AsyncOperationManager.clearAsyncOperation(false, this); + } + + private void postToHandler(Runnable runnable) { + if (isInterrupted()) { + return; + } + handler.post(() -> { + if (!isInterrupted()) { + runnable.run(); + } + }); + } + + @OnLifecycleEvent(Event.ON_STOP) + public void onDestroy() { + if (isAlive() && !isInterrupted()) { + fido2AsyncOperationManager.clearAsyncOperation(true, this); + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/async/WebauthnFido2OperationThread.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/async/WebauthnFido2OperationThread.java new file mode 100644 index 0000000..37e8fa1 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/async/WebauthnFido2OperationThread.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.async; + + +import java.io.IOException; + +import android.os.Handler; + +import androidx.annotation.UiThread; +import androidx.annotation.WorkerThread; +import de.cotech.hw.fido2.WebauthnCallback; +import de.cotech.hw.fido2.internal.Fido2AppletConnection; +import de.cotech.hw.fido2.internal.operations.WebauthnSecurityKeyOperation; +import de.cotech.hw.fido2.internal.operations.WebauthnSecurityKeyOperationFactory; +import de.cotech.hw.fido2.internal.webauthn.WebauthnCommand; +import de.cotech.hw.fido2.internal.webauthn.WebauthnResponse; + + +public class WebauthnFido2OperationThread + extends Fido2OperationThread { + private final WC webauthnCommand; + private final WebauthnCallback callback; + private final WebauthnSecurityKeyOperation operation; + + public WebauthnFido2OperationThread( + Fido2AppletConnection fido2AppletConnection, + WebauthnSecurityKeyOperationFactory operationFactory, + Handler handler, WebauthnCallback callback, WC webauthnCommand, + int userPresenceCheckDelayMs + ) { + super(fido2AppletConnection, handler, userPresenceCheckDelayMs); + this.callback = callback; + this.webauthnCommand = webauthnCommand; + this.operation = operationFactory.getOperation(webauthnCommand, fido2AppletConnection.isCtap2Capable()); + } + + @WorkerThread + WR performOperation() throws IOException { + return operation.performWebauthnSecurityKeyOperation(fido2AppletConnection, webauthnCommand); + } + + @Override + @UiThread + void deliverResponse(WR response) { + callback.onResponse(response); + } + + @Override + @UiThread + void deliverIoException(IOException e) { + callback.onIoException(e); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborAttestationObjectSerializer.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborAttestationObjectSerializer.java new file mode 100644 index 0000000..92797d5 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborAttestationObjectSerializer.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.cbor; + + +import java.io.IOException; +import java.util.List; + +import de.cotech.hw.fido2.internal.cbor_java.CborBuilder; +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; +import de.cotech.hw.fido2.domain.create.AttestationObject; + + +public class CborAttestationObjectSerializer { + public byte[] serializeAttestationObject(AttestationObject attestationObject) throws IOException { + try { + return serializeAttestationObjectOrThrow(attestationObject); + } catch (CborException e) { + throw new IOException(e); + } + } + + private byte[] serializeAttestationObjectOrThrow(AttestationObject attestationObject) + throws CborException { + List cborData = new CborBuilder() + .addMap() + .put(CborConstants.FMT, new UnicodeString(attestationObject.fmt())) + .put(CborConstants.AUTH_DATA, new ByteString(attestationObject.authData())) + .put(CborConstants.ATT_STMT, CborDecoder.decode(attestationObject.attStmt()).get(0)) + .end() + .build(); + return CborUtils.writeCborDataToBytes(cborData); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborConstants.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborConstants.java new file mode 100644 index 0000000..ac116fe --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborConstants.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.cbor; + + +import java.io.ByteArrayOutputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; + + +public class CborConstants { + public static final UnicodeString FMT = new UnicodeString("fmt"); + public static final UnicodeString AUTH_DATA = new UnicodeString("authData"); + public static final UnicodeString ATT_STMT = new UnicodeString("attStmt"); + + public static final UnicodeString TYPE = new UnicodeString("type"); + public static final UnicodeString ID = new UnicodeString("id"); + + public static final Map EMPTY_MAP = new Map(); + public static final byte[] EMPTY_MAP_BYTES = emptyMap(); + + private CborConstants() { } + + private static byte[] emptyMap() { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + new CborEncoder(outputStream).encode(EMPTY_MAP); + return outputStream.toByteArray(); + } catch (CborException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborCtap1AttestationStatementUtil.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborCtap1AttestationStatementUtil.java new file mode 100644 index 0000000..f7b85d6 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborCtap1AttestationStatementUtil.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.cbor; + + +import java.util.List; + +import de.cotech.hw.fido2.internal.cbor_java.CborBuilder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; + +public class CborCtap1AttestationStatementUtil { + public static byte[] toAttestionStatement(byte[] x509certificate, byte[] signature) { + try { + List dataItems = new CborBuilder() + .addMap() + .put("sig", signature) + .putArray("x5c") + .add(x509certificate) + .end() + .end() + .build(); + return CborUtils.writeCborDataToBytes(dataItems); + } catch (CborException e) { + // this operation will always work + throw new IllegalStateException(e); + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborPublicKeyCredentialDescriptorParser.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborPublicKeyCredentialDescriptorParser.java new file mode 100644 index 0000000..081429f --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborPublicKeyCredentialDescriptorParser.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.cbor; + + +import java.io.IOException; +import java.util.List; + +import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; +import de.cotech.hw.fido2.domain.PublicKeyCredentialType; +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; + + +public class CborPublicKeyCredentialDescriptorParser { + public PublicKeyCredentialDescriptor parse(byte[] data) throws IOException { + try { + List dataItems = CborDecoder.decode(data); + if (dataItems.size() < 1) { + throw new IOException( + "Failed to parse PublicKeyCredentialDescriptor, expected 1 element!"); + } + Map cborMap = (Map) dataItems.get(0); + + PublicKeyCredentialType publicKeyCredentialType = PublicKeyCredentialType.fromString( + ((UnicodeString) cborMap.get(CborConstants.TYPE)).getString()); + byte[] id = ((ByteString) cborMap.get(CborConstants.ID)).getBytes(); + + return PublicKeyCredentialDescriptor.create(publicKeyCredentialType, id, null); + } catch (ClassCastException e) { + throw new IOException("Failed to parse PublicKeyCredentialDescriptor!", e); + } catch (CborException e) { + throw new IOException("Failed to parse PublicKeyCredentialDescriptor!", e); + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborUtils.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborUtils.java new file mode 100644 index 0000000..e512f4f --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor/CborUtils.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.cbor; + + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.Array; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; +import de.cotech.hw.fido2.internal.cbor_java.model.UnsignedInteger; + + +public class CborUtils { + public static byte[] writeCborDataToBytes(List cborData) throws CborException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + new CborEncoder(outputStream).encode(cborData); + return outputStream.toByteArray(); + } + + public static byte[] writeCborDataToBytes(DataItem cborData) throws CborException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + new CborEncoder(outputStream).encode(cborData); + return outputStream.toByteArray(); + } + + public static List cborArrayToIntegerArray(Array array) { + ArrayList result = new ArrayList<>(); + for (DataItem versionDataItem : array.getDataItems()) { + UnsignedInteger unsignedInteger = (UnsignedInteger) versionDataItem; + result.add(unsignedInteger.getValue().intValue()); + } + return Collections.unmodifiableList(result); + } + + public static List cborArrayToStringArray(Array array) { + ArrayList result = new ArrayList<>(); + for (DataItem versionDataItem : array.getDataItems()) { + UnicodeString string = (UnicodeString) versionDataItem; + result.add(string.getString()); + } + return Collections.unmodifiableList(result); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/CborBuilder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/CborBuilder.java new file mode 100644 index 0000000..19107c6 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/CborBuilder.java @@ -0,0 +1,129 @@ +package de.cotech.hw.fido2.internal.cbor_java; + +import java.math.BigInteger; +import java.util.LinkedList; +import java.util.List; + +import de.cotech.hw.fido2.internal.cbor_java.builder.AbstractBuilder; +import de.cotech.hw.fido2.internal.cbor_java.builder.ArrayBuilder; +import de.cotech.hw.fido2.internal.cbor_java.builder.ByteStringBuilder; +import de.cotech.hw.fido2.internal.cbor_java.builder.MapBuilder; +import de.cotech.hw.fido2.internal.cbor_java.builder.UnicodeStringBuilder; +import de.cotech.hw.fido2.internal.cbor_java.model.Array; +import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; + +public class CborBuilder extends AbstractBuilder { + + private final List dataItems = new LinkedList<>(); + + public CborBuilder() { + super(null); + } + + public CborBuilder reset() { + dataItems.clear(); + return this; + } + + public List build() { + return dataItems; + } + + public CborBuilder add(DataItem dataItem) { + dataItems.add(dataItem); + return this; + } + + public CborBuilder add(long value) { + add(convert(value)); + return this; + } + + public CborBuilder add(BigInteger value) { + add(convert(value)); + return this; + } + + public CborBuilder add(boolean value) { + add(convert(value)); + return this; + } + + public CborBuilder add(float value) { + add(convert(value)); + return this; + } + + public CborBuilder add(double value) { + add(convert(value)); + return this; + } + + public CborBuilder add(byte[] bytes) { + add(convert(bytes)); + return this; + } + + public ByteStringBuilder startByteString() { + return startByteString(null); + } + + public ByteStringBuilder startByteString(byte[] bytes) { + add(new ByteString(bytes).setChunked(true)); + return new ByteStringBuilder(this); + } + + public CborBuilder add(String string) { + add(convert(string)); + return this; + } + + public UnicodeStringBuilder startString() { + return startString(null); + } + + public UnicodeStringBuilder startString(String string) { + add(new UnicodeString(string).setChunked(true)); + return new UnicodeStringBuilder(this); + } + + public CborBuilder addTag(long value) { + add(tag(value)); + return this; + } + + public ArrayBuilder startArray() { + Array array = new Array(); + array.setChunked(true); + add(array); + return new ArrayBuilder(this, array); + } + + public ArrayBuilder addArray() { + Array array = new Array(); + add(array); + return new ArrayBuilder(this, array); + } + + public MapBuilder addMap() { + Map map = new Map(); + add(map); + return new MapBuilder(this, map); + } + + public MapBuilder startMap() { + Map map = new Map(); + map.setChunked(true); + add(map); + return new MapBuilder(this, map); + } + + @Override + protected void addChunk(DataItem dataItem) { + add(dataItem); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/CborDecoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/CborDecoder.java new file mode 100644 index 0000000..5d734ea --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/CborDecoder.java @@ -0,0 +1,288 @@ +package de.cotech.hw.fido2.internal.cbor_java; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import de.cotech.hw.fido2.internal.cbor_java.decoder.ArrayDecoder; +import de.cotech.hw.fido2.internal.cbor_java.decoder.ByteStringDecoder; +import de.cotech.hw.fido2.internal.cbor_java.decoder.MapDecoder; +import de.cotech.hw.fido2.internal.cbor_java.decoder.NegativeIntegerDecoder; +import de.cotech.hw.fido2.internal.cbor_java.decoder.SpecialDecoder; +import de.cotech.hw.fido2.internal.cbor_java.decoder.TagDecoder; +import de.cotech.hw.fido2.internal.cbor_java.decoder.UnicodeStringDecoder; +import de.cotech.hw.fido2.internal.cbor_java.decoder.UnsignedIntegerDecoder; +import de.cotech.hw.fido2.internal.cbor_java.model.Array; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.LanguageTaggedString; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; +import de.cotech.hw.fido2.internal.cbor_java.model.Number; +import de.cotech.hw.fido2.internal.cbor_java.model.RationalNumber; +import de.cotech.hw.fido2.internal.cbor_java.model.Tag; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; + +/** + * Decoder for the CBOR format based. + */ +public class CborDecoder { + + private final InputStream inputStream; + private final UnsignedIntegerDecoder unsignedIntegerDecoder; + private final NegativeIntegerDecoder negativeIntegerDecoder; + private final ByteStringDecoder byteStringDecoder; + private final UnicodeStringDecoder unicodeStringDecoder; + private final ArrayDecoder arrayDecoder; + private final MapDecoder mapDecoder; + private final TagDecoder tagDecoder; + private final SpecialDecoder specialDecoder; + + private boolean autoDecodeInfinitiveArrays = true; + private boolean autoDecodeInfinitiveMaps = true; + private boolean autoDecodeInfinitiveByteStrings = true; + private boolean autoDecodeInfinitiveUnicodeStrings = true; + private boolean autoDecodeRationalNumbers = true; + private boolean autoDecodeLanguageTaggedStrings = true; + private boolean rejectDuplicateKeys = false; + + /** + * Initialize a new decoder which reads the binary encoded data from an + * {@link InputStream}. + */ + public CborDecoder(InputStream inputStream) { + Objects.requireNonNull(inputStream); + this.inputStream = inputStream; + unsignedIntegerDecoder = new UnsignedIntegerDecoder(this, inputStream); + negativeIntegerDecoder = new NegativeIntegerDecoder(this, inputStream); + byteStringDecoder = new ByteStringDecoder(this, inputStream); + unicodeStringDecoder = new UnicodeStringDecoder(this, inputStream); + arrayDecoder = new ArrayDecoder(this, inputStream); + mapDecoder = new MapDecoder(this, inputStream); + tagDecoder = new TagDecoder(this, inputStream); + specialDecoder = new SpecialDecoder(this, inputStream); + } + + /** + * Convenience method to decode a byte array directly. + * + * @param bytes + * the CBOR encoded data + * @return a list of {@link DataItem}s + * @throws CborException + * if decoding failed + */ + public static List decode(byte[] bytes) throws CborException { + return new CborDecoder(new ByteArrayInputStream(bytes)).decode(); + } + + /** + * Decode the {@link InputStream} to a list of {@link DataItem}s. + * + * @return the list of {@link DataItem}s + * @throws CborException + * if decoding failed + */ + public List decode() throws CborException { + List dataItems = new LinkedList<>(); + DataItem dataItem; + while ((dataItem = decodeNext()) != null) { + dataItems.add(dataItem); + } + return dataItems; + } + + /** + * Streaming decoding of an input stream. On each decoded DataItem, the + * callback listener is invoked. + * + * @param dataItemListener + * the callback listener + * @throws CborException + * if decoding failed + */ + public void decode(DataItemListener dataItemListener) throws CborException { + Objects.requireNonNull(dataItemListener); + DataItem dataItem = decodeNext(); + while (dataItem != null) { + dataItemListener.onDataItem(dataItem); + dataItem = decodeNext(); + } + } + + /** + * Decodes exactly one DataItem from the input stream. + * + * @return a {@link DataItem} or null if end of stream has reached. + * @throws CborException + * if decoding failed + */ + public DataItem decodeNext() throws CborException { + int symbol; + try { + symbol = inputStream.read(); + } catch (IOException ioException) { + throw new CborException(ioException); + } + if (symbol == -1) { + return null; + } + switch (MajorType.ofByte(symbol)) { + case ARRAY: + return arrayDecoder.decode(symbol); + case BYTE_STRING: + return byteStringDecoder.decode(symbol); + case MAP: + return mapDecoder.decode(symbol); + case NEGATIVE_INTEGER: + return negativeIntegerDecoder.decode(symbol); + case UNICODE_STRING: + return unicodeStringDecoder.decode(symbol); + case UNSIGNED_INTEGER: + return unsignedIntegerDecoder.decode(symbol); + case SPECIAL: + return specialDecoder.decode(symbol); + case TAG: + Tag tag = tagDecoder.decode(symbol); + DataItem next = decodeNext(); + if (next == null) { + throw new CborException("Unexpected end of stream: tag without following data item."); + } else { + if (autoDecodeRationalNumbers && tag.getValue() == 30) { + return decodeRationalNumber(next); + } else if (autoDecodeLanguageTaggedStrings && tag.getValue() == 38) { + return decodeLanguageTaggedString(next); + } else { + DataItem itemToTag = next; + while (itemToTag.hasTag()) + itemToTag = itemToTag.getTag(); + itemToTag.setTag(tag); + return next; + } + } + case INVALID: + default: + throw new CborException("Not implemented major type " + symbol); + } + } + + private DataItem decodeLanguageTaggedString(DataItem dataItem) throws CborException { + if (!(dataItem instanceof Array)) { + throw new CborException("Error decoding LanguageTaggedString: not an array"); + } + + Array array = (Array) dataItem; + + if (array.getDataItems().size() != 2) { + throw new CborException("Error decoding LanguageTaggedString: array size is not 2"); + } + + DataItem languageDataItem = array.getDataItems().get(0); + + if (!(languageDataItem instanceof UnicodeString)) { + throw new CborException("Error decoding LanguageTaggedString: first data item is not an UnicodeString"); + } + + DataItem stringDataItem = array.getDataItems().get(1); + + if (!(stringDataItem instanceof UnicodeString)) { + throw new CborException("Error decoding LanguageTaggedString: second data item is not an UnicodeString"); + } + + UnicodeString language = (UnicodeString) languageDataItem; + UnicodeString string = (UnicodeString) stringDataItem; + + return new LanguageTaggedString(language, string); + } + + private DataItem decodeRationalNumber(DataItem dataItem) throws CborException { + if (!(dataItem instanceof Array)) { + throw new CborException("Error decoding RationalNumber: not an array"); + } + + Array array = (Array) dataItem; + + if (array.getDataItems().size() != 2) { + throw new CborException("Error decoding RationalNumber: array size is not 2"); + } + + DataItem numeratorDataItem = array.getDataItems().get(0); + + if (!(numeratorDataItem instanceof Number)) { + throw new CborException("Error decoding RationalNumber: first data item is not a number"); + } + + DataItem denominatorDataItem = array.getDataItems().get(1); + + if (!(denominatorDataItem instanceof Number)) { + throw new CborException("Error decoding RationalNumber: second data item is not a number"); + } + + Number numerator = (Number) numeratorDataItem; + Number denominator = (Number) denominatorDataItem; + + return new RationalNumber(numerator, denominator); + } + + public boolean isAutoDecodeInfinitiveArrays() { + return autoDecodeInfinitiveArrays; + } + + public void setAutoDecodeInfinitiveArrays(boolean autoDecodeInfinitiveArrays) { + this.autoDecodeInfinitiveArrays = autoDecodeInfinitiveArrays; + } + + public boolean isAutoDecodeInfinitiveMaps() { + return autoDecodeInfinitiveMaps; + } + + public void setAutoDecodeInfinitiveMaps(boolean autoDecodeInfinitiveMaps) { + this.autoDecodeInfinitiveMaps = autoDecodeInfinitiveMaps; + } + + public boolean isAutoDecodeInfinitiveByteStrings() { + return autoDecodeInfinitiveByteStrings; + } + + public void setAutoDecodeInfinitiveByteStrings( + boolean autoDecodeInfinitiveByteStrings) { + this.autoDecodeInfinitiveByteStrings = autoDecodeInfinitiveByteStrings; + } + + public boolean isAutoDecodeInfinitiveUnicodeStrings() { + return autoDecodeInfinitiveUnicodeStrings; + } + + public void setAutoDecodeInfinitiveUnicodeStrings( + boolean autoDecodeInfinitiveUnicodeStrings) { + this.autoDecodeInfinitiveUnicodeStrings = autoDecodeInfinitiveUnicodeStrings; + } + + public boolean isAutoDecodeRationalNumbers() { + return autoDecodeRationalNumbers; + } + + public void setAutoDecodeRationalNumbers( + boolean autoDecodeRationalNumbers) { + this.autoDecodeRationalNumbers = autoDecodeRationalNumbers; + } + + public boolean isAutoDecodeLanguageTaggedStrings() { + return autoDecodeLanguageTaggedStrings; + } + + public void setAutoDecodeLanguageTaggedStrings( + boolean autoDecodeLanguageTaggedStrings) { + this.autoDecodeLanguageTaggedStrings = autoDecodeLanguageTaggedStrings; + } + + public boolean isRejectDuplicateKeys() { + return rejectDuplicateKeys; + } + + public void setRejectDuplicateKeys(boolean rejectDuplicateKeys) { + this.rejectDuplicateKeys = rejectDuplicateKeys; + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/CborEncoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/CborEncoder.java new file mode 100644 index 0000000..7c45906 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/CborEncoder.java @@ -0,0 +1,121 @@ +package de.cotech.hw.fido2.internal.cbor_java; + +import java.io.OutputStream; +import java.util.List; +import java.util.Objects; + +import de.cotech.hw.fido2.internal.cbor_java.encoder.ArrayEncoder; +import de.cotech.hw.fido2.internal.cbor_java.encoder.ByteStringEncoder; +import de.cotech.hw.fido2.internal.cbor_java.encoder.MapEncoder; +import de.cotech.hw.fido2.internal.cbor_java.encoder.NegativeIntegerEncoder; +import de.cotech.hw.fido2.internal.cbor_java.encoder.SpecialEncoder; +import de.cotech.hw.fido2.internal.cbor_java.encoder.TagEncoder; +import de.cotech.hw.fido2.internal.cbor_java.encoder.UnicodeStringEncoder; +import de.cotech.hw.fido2.internal.cbor_java.encoder.UnsignedIntegerEncoder; +import de.cotech.hw.fido2.internal.cbor_java.model.Array; +import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; +import de.cotech.hw.fido2.internal.cbor_java.model.NegativeInteger; +import de.cotech.hw.fido2.internal.cbor_java.model.SimpleValue; +import de.cotech.hw.fido2.internal.cbor_java.model.Special; +import de.cotech.hw.fido2.internal.cbor_java.model.Tag; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; +import de.cotech.hw.fido2.internal.cbor_java.model.UnsignedInteger; + +/** + * Encoder for the CBOR format based. + */ +public class CborEncoder { + + private final UnsignedIntegerEncoder unsignedIntegerEncoder; + private final NegativeIntegerEncoder negativeIntegerEncoder; + private final ByteStringEncoder byteStringEncoder; + private final UnicodeStringEncoder unicodeStringEncoder; + private final ArrayEncoder arrayEncoder; + private final MapEncoder mapEncoder; + private final TagEncoder tagEncoder; + private final SpecialEncoder specialEncoder; + + /** + * Initialize a new encoder which writes the binary encoded data to an + * {@link OutputStream}. + */ + public CborEncoder(OutputStream outputStream) { + Objects.requireNonNull(outputStream); + unsignedIntegerEncoder = new UnsignedIntegerEncoder(this, outputStream); + negativeIntegerEncoder = new NegativeIntegerEncoder(this, outputStream); + byteStringEncoder = new ByteStringEncoder(this, outputStream); + unicodeStringEncoder = new UnicodeStringEncoder(this, outputStream); + arrayEncoder = new ArrayEncoder(this, outputStream); + mapEncoder = new MapEncoder(this, outputStream); + tagEncoder = new TagEncoder(this, outputStream); + specialEncoder = new SpecialEncoder(this, outputStream); + } + + /** + * Encode a list of {@link DataItem}s, also known as a stream. + * + * @param dataItems + * a list of {@link DataItem}s + * @throws CborException + * if the {@link DataItem}s could not be encoded or there was an + * problem with the {@link OutputStream}. + */ + public void encode(List dataItems) throws CborException { + for (DataItem dataItem : dataItems) { + encode(dataItem); + } + } + + /** + * Encode a single {@link DataItem}. + * + * @param dataItem + * the {@link DataItem} to encode. If null, encoder encodes a + * {@link SimpleValue} NULL value. + * @throws CborException + * if {@link DataItem} could not be encoded or there was an + * problem with the {@link OutputStream}. + */ + public void encode(DataItem dataItem) throws CborException { + if (dataItem == null) { + dataItem = SimpleValue.NULL; + } + + if (dataItem.hasTag()) { + Tag tagDi = dataItem.getTag(); + tagEncoder.encode(tagDi); + } + + switch (dataItem.getMajorType()) { + case UNSIGNED_INTEGER: + unsignedIntegerEncoder.encode((UnsignedInteger) dataItem); + break; + case NEGATIVE_INTEGER: + negativeIntegerEncoder.encode((NegativeInteger) dataItem); + break; + case BYTE_STRING: + byteStringEncoder.encode((ByteString) dataItem); + break; + case UNICODE_STRING: + unicodeStringEncoder.encode((UnicodeString) dataItem); + break; + case ARRAY: + arrayEncoder.encode((Array) dataItem); + break; + case MAP: + mapEncoder.encode((Map) dataItem); + break; + case SPECIAL: + specialEncoder.encode((Special) dataItem); + break; + case TAG: + tagEncoder.encode((Tag) dataItem); + break; + default: + throw new CborException("Unknown major type"); + } + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/CborException.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/CborException.java new file mode 100644 index 0000000..dab8291 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/CborException.java @@ -0,0 +1,19 @@ +package de.cotech.hw.fido2.internal.cbor_java; + +public class CborException extends Exception { + + private static final long serialVersionUID = 8839905301881841410L; + + public CborException(String message) { + super(message); + } + + public CborException(Throwable cause) { + super(cause); + } + + public CborException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/DataItemListener.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/DataItemListener.java new file mode 100644 index 0000000..7e1388d --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/DataItemListener.java @@ -0,0 +1,15 @@ +package de.cotech.hw.fido2.internal.cbor_java; + +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; + +/** + * Callback interface for a streaming {@link CborDecoder}. + */ +public interface DataItemListener { + + /** + * Gets called on every decoded {@link DataItem}. + */ + void onDataItem(DataItem dataItem); + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/AbstractBuilder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/AbstractBuilder.java new file mode 100644 index 0000000..98bcc32 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/AbstractBuilder.java @@ -0,0 +1,113 @@ +package de.cotech.hw.fido2.internal.cbor_java.builder; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; + +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.decoder.HalfPrecisionFloatDecoder; +import de.cotech.hw.fido2.internal.cbor_java.encoder.HalfPrecisionFloatEncoder; +import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.DoublePrecisionFloat; +import de.cotech.hw.fido2.internal.cbor_java.model.HalfPrecisionFloat; +import de.cotech.hw.fido2.internal.cbor_java.model.NegativeInteger; +import de.cotech.hw.fido2.internal.cbor_java.model.SimpleValue; +import de.cotech.hw.fido2.internal.cbor_java.model.SinglePrecisionFloat; +import de.cotech.hw.fido2.internal.cbor_java.model.Tag; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; +import de.cotech.hw.fido2.internal.cbor_java.model.UnsignedInteger; + +public abstract class AbstractBuilder { + + private final T parent; + + public AbstractBuilder(T parent) { + this.parent = parent; + } + + protected T getParent() { + return parent; + } + + protected void addChunk(DataItem dataItem) { + throw new IllegalStateException(); + } + + protected DataItem convert(long value) { + if (value >= 0) { + return new UnsignedInteger(value); + } else { + return new NegativeInteger(value); + } + } + + protected DataItem convert(BigInteger value) { + if (value.signum() == -1) { + return new NegativeInteger(value); + } else { + return new UnsignedInteger(value); + } + } + + protected DataItem convert(boolean value) { + if (value) { + return SimpleValue.TRUE; + } else { + return SimpleValue.FALSE; + } + } + + protected DataItem convert(byte[] bytes) { + return new ByteString(bytes); + } + + protected DataItem convert(String string) { + return new UnicodeString(string); + } + + protected DataItem convert(float value) { + if (isHalfPrecisionEnough(value)) { + return new HalfPrecisionFloat(value); + } else { + return new SinglePrecisionFloat(value); + } + } + + protected DataItem convert(double value) { + return new DoublePrecisionFloat(value); + } + + protected Tag tag(long value) { + return new Tag(value); + } + + private boolean isHalfPrecisionEnough(float value) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + HalfPrecisionFloatEncoder encoder = getHalfPrecisionFloatEncoder(outputStream); + encoder.encode(new HalfPrecisionFloat(value)); + byte[] bytes = outputStream.toByteArray(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + HalfPrecisionFloatDecoder decoder = getHalfPrecisionFloatDecoder(inputStream); + if (inputStream.read() == -1) { // to skip type byte + throw new CborException("unexpected end of stream"); + } + HalfPrecisionFloat halfPrecisionFloat = decoder.decode(0); + return value == halfPrecisionFloat.getValue(); + } catch (CborException cborException) { + return false; + } + } + + protected HalfPrecisionFloatEncoder getHalfPrecisionFloatEncoder(OutputStream outputStream) { + return new HalfPrecisionFloatEncoder(null, outputStream); + } + + protected HalfPrecisionFloatDecoder getHalfPrecisionFloatDecoder(InputStream inputStream) { + return new HalfPrecisionFloatDecoder(null, inputStream); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/ArrayBuilder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/ArrayBuilder.java new file mode 100644 index 0000000..40e3608 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/ArrayBuilder.java @@ -0,0 +1,86 @@ +package de.cotech.hw.fido2.internal.cbor_java.builder; + +import de.cotech.hw.fido2.internal.cbor_java.model.Array; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; +import de.cotech.hw.fido2.internal.cbor_java.model.SimpleValue; + +public class ArrayBuilder> extends + AbstractBuilder { + + private final Array array; + + public ArrayBuilder(T parent, Array array) { + super(parent); + this.array = array; + } + + public ArrayBuilder add(DataItem dataItem) { + array.add(dataItem); + return this; + } + + public ArrayBuilder add(long value) { + add(convert(value)); + return this; + } + + public ArrayBuilder add(boolean value) { + add(convert(value)); + return this; + } + + public ArrayBuilder add(float value) { + add(convert(value)); + return this; + } + + public ArrayBuilder add(double value) { + add(convert(value)); + return this; + } + + public ArrayBuilder add(byte[] bytes) { + add(convert(bytes)); + return this; + } + + public ArrayBuilder add(String string) { + add(convert(string)); + return this; + } + + public ArrayBuilder> addArray() { + Array nestedArray = new Array(); + add(nestedArray); + return new ArrayBuilder>(this, nestedArray); + } + + public ArrayBuilder> startArray() { + Array nestedArray = new Array(); + nestedArray.setChunked(true); + add(nestedArray); + return new ArrayBuilder>(this, nestedArray); + } + + public MapBuilder> addMap() { + Map nestedMap = new Map(); + add(nestedMap); + return new MapBuilder>(this, nestedMap); + } + + public MapBuilder> startMap() { + Map nestedMap = new Map(); + nestedMap.setChunked(true); + add(nestedMap); + return new MapBuilder>(this, nestedMap); + } + + public T end() { + if (array.isChunked()) { + add(SimpleValue.BREAK); + } + return getParent(); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/ByteStringBuilder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/ByteStringBuilder.java new file mode 100644 index 0000000..1301271 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/ByteStringBuilder.java @@ -0,0 +1,22 @@ +package de.cotech.hw.fido2.internal.cbor_java.builder; + +import de.cotech.hw.fido2.internal.cbor_java.model.SimpleValue; + +public class ByteStringBuilder> extends + AbstractBuilder { + + public ByteStringBuilder(T parent) { + super(parent); + } + + public ByteStringBuilder add(byte[] bytes) { + getParent().addChunk(convert(bytes)); + return this; + } + + public T end() { + getParent().addChunk(SimpleValue.BREAK); + return getParent(); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/MapBuilder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/MapBuilder.java new file mode 100644 index 0000000..fbe1796 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/MapBuilder.java @@ -0,0 +1,155 @@ +package de.cotech.hw.fido2.internal.cbor_java.builder; + +import de.cotech.hw.fido2.internal.cbor_java.model.Array; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; + +public class MapBuilder> extends + AbstractBuilder { + + private final Map map; + + public MapBuilder(T parent, Map map) { + super(parent); + this.map = map; + } + + public MapBuilder put(DataItem key, DataItem value) { + map.put(key, value); + return this; + } + + public MapBuilder put(long key, long value) { + put(convert(key), convert(value)); + return this; + } + + public MapBuilder put(long key, boolean value) { + put(convert(key), convert(value)); + return this; + } + + public MapBuilder put(long key, float value) { + put(convert(key), convert(value)); + return this; + } + + public MapBuilder put(long key, double value) { + put(convert(key), convert(value)); + return this; + } + + public MapBuilder put(long key, byte[] value) { + put(convert(key), convert(value)); + return this; + } + + public MapBuilder put(long key, String value) { + put(convert(key), convert(value)); + return this; + } + + public MapBuilder put(String key, long value) { + put(convert(key), convert(value)); + return this; + } + + public MapBuilder put(String key, boolean value) { + put(convert(key), convert(value)); + return this; + } + + public MapBuilder put(String key, float value) { + put(convert(key), convert(value)); + return this; + } + + public MapBuilder put(String key, double value) { + put(convert(key), convert(value)); + return this; + } + + public MapBuilder put(String key, byte[] value) { + map.put(convert(key), convert(value)); + return this; + } + + public MapBuilder put(String key, String value) { + put(convert(key), convert(value)); + return this; + } + + public ArrayBuilder> putArray(DataItem key) { + Array array = new Array(); + put(key, array); + return new ArrayBuilder<>(this, array); + } + + public ArrayBuilder> putArray(long key) { + Array array = new Array(); + put(convert(key), array); + return new ArrayBuilder<>(this, array); + } + + public ArrayBuilder> putArray(String key) { + Array array = new Array(); + put(convert(key), array); + return new ArrayBuilder<>(this, array); + } + + public ArrayBuilder> startArray(DataItem key) { + Array array = new Array(); + array.setChunked(true); + put(key, array); + return new ArrayBuilder<>(this, array); + } + + public ArrayBuilder> startArray(long key) { + return startArray(convert(key)); + } + + public ArrayBuilder> startArray(String key) { + Array array = new Array(); + array.setChunked(true); + put(convert(key), array); + return new ArrayBuilder<>(this, array); + } + + public MapBuilder> putMap(DataItem key) { + Map nestedMap = new Map(); + put(key, nestedMap); + return new MapBuilder<>(this, nestedMap); + } + + public MapBuilder> putMap(long key) { + Map nestedMap = new Map(); + put(convert(key), nestedMap); + return new MapBuilder<>(this, nestedMap); + } + + public MapBuilder> putMap(String key) { + Map nestedMap = new Map(); + put(convert(key), nestedMap); + return new MapBuilder<>(this, nestedMap); + } + + public MapBuilder> startMap(DataItem key) { + Map nestedMap = new Map(); + nestedMap.setChunked(true); + put(key, nestedMap); + return new MapBuilder<>(this, nestedMap); + } + + public MapBuilder> startMap(long key) { + return startMap(convert(key)); + } + + public MapBuilder> startMap(String key) { + return startMap(convert(key)); + } + + public T end() { + return getParent(); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/UnicodeStringBuilder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/UnicodeStringBuilder.java new file mode 100644 index 0000000..88f2afb --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/builder/UnicodeStringBuilder.java @@ -0,0 +1,22 @@ +package de.cotech.hw.fido2.internal.cbor_java.builder; + +import de.cotech.hw.fido2.internal.cbor_java.model.SimpleValue; + +public class UnicodeStringBuilder> extends + AbstractBuilder { + + public UnicodeStringBuilder(T parent) { + super(parent); + } + + public UnicodeStringBuilder add(String string) { + getParent().addChunk(convert(string)); + return this; + } + + public T end() { + getParent().addChunk(SimpleValue.BREAK); + return getParent(); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/AbstractDecoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/AbstractDecoder.java new file mode 100644 index 0000000..5847268 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/AbstractDecoder.java @@ -0,0 +1,119 @@ +package de.cotech.hw.fido2.internal.cbor_java.decoder; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.AdditionalInformation; + +public abstract class AbstractDecoder { + + protected static final int INFINITY = -1; + + protected final InputStream inputStream; + protected final CborDecoder decoder; + + public AbstractDecoder(CborDecoder decoder, InputStream inputStream) { + this.decoder = decoder; + this.inputStream = inputStream; + } + + public abstract T decode(int initialByte) throws CborException; + + protected int nextSymbol() throws CborException { + try { + int symbol = inputStream.read(); + if (symbol == -1) { + throw new IOException("Unexpected end of stream"); + } + return symbol; + } catch (IOException ioException) { + throw new CborException(ioException); + } + } + + protected long getLength(int initialByte) throws CborException { + switch (AdditionalInformation.ofByte(initialByte)) { + case DIRECT: + return initialByte & 31; + case ONE_BYTE: + return nextSymbol(); + case TWO_BYTES: + long twoByteValue = 0; + twoByteValue |= nextSymbol() << 8; + twoByteValue |= nextSymbol() << 0; + return twoByteValue; + case FOUR_BYTES: + long fourByteValue = 0L; + fourByteValue |= (long) nextSymbol() << 24; + fourByteValue |= (long) nextSymbol() << 16; + fourByteValue |= (long) nextSymbol() << 8; + fourByteValue |= (long) nextSymbol() << 0; + return fourByteValue; + case EIGHT_BYTES: + long eightByteValue = 0; + eightByteValue |= (long) nextSymbol() << 56; + eightByteValue |= (long) nextSymbol() << 48; + eightByteValue |= (long) nextSymbol() << 40; + eightByteValue |= (long) nextSymbol() << 32; + eightByteValue |= (long) nextSymbol() << 24; + eightByteValue |= (long) nextSymbol() << 16; + eightByteValue |= (long) nextSymbol() << 8; + eightByteValue |= (long) nextSymbol() << 0; + return eightByteValue; + case INDEFINITE: + return INFINITY; + case RESERVED: + default: + throw new CborException("Reserved additional information"); + } + } + + protected BigInteger getLengthAsBigInteger(int initialByte) + throws CborException { + switch (AdditionalInformation.ofByte(initialByte)) { + case DIRECT: + return BigInteger.valueOf(initialByte & 31); + case ONE_BYTE: + return BigInteger.valueOf(nextSymbol()); + case TWO_BYTES: + long twoByteValue = 0; + twoByteValue |= nextSymbol() << 8; + twoByteValue |= nextSymbol() << 0; + return BigInteger.valueOf(twoByteValue); + case FOUR_BYTES: + long fourByteValue = 0L; + fourByteValue |= (long) nextSymbol() << 24; + fourByteValue |= (long) nextSymbol() << 16; + fourByteValue |= (long) nextSymbol() << 8; + fourByteValue |= (long) nextSymbol() << 0; + return BigInteger.valueOf(fourByteValue); + case EIGHT_BYTES: + BigInteger eightByteValue = BigInteger.ZERO; + eightByteValue = eightByteValue.or(BigInteger.valueOf(nextSymbol()) + .shiftLeft(56)); + eightByteValue = eightByteValue.or(BigInteger.valueOf(nextSymbol()) + .shiftLeft(48)); + eightByteValue = eightByteValue.or(BigInteger.valueOf(nextSymbol()) + .shiftLeft(40)); + eightByteValue = eightByteValue.or(BigInteger.valueOf(nextSymbol()) + .shiftLeft(32)); + eightByteValue = eightByteValue.or(BigInteger.valueOf(nextSymbol()) + .shiftLeft(24)); + eightByteValue = eightByteValue.or(BigInteger.valueOf(nextSymbol()) + .shiftLeft(16)); + eightByteValue = eightByteValue.or(BigInteger.valueOf(nextSymbol()) + .shiftLeft(8)); + eightByteValue = eightByteValue.or(BigInteger.valueOf(nextSymbol()) + .shiftLeft(0)); + return eightByteValue; + case INDEFINITE: + return BigInteger.valueOf(INFINITY); + case RESERVED: + default: + throw new CborException("Reserved additional information"); + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/ArrayDecoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/ArrayDecoder.java new file mode 100644 index 0000000..65a9c7c --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/ArrayDecoder.java @@ -0,0 +1,59 @@ +package de.cotech.hw.fido2.internal.cbor_java.decoder; + +import java.io.InputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.Array; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.Special; + +public class ArrayDecoder extends AbstractDecoder { + + public ArrayDecoder(CborDecoder decoder, InputStream inputStream) { + super(decoder, inputStream); + } + + @Override + public Array decode(int initialByte) throws CborException { + long length = getLength(initialByte); + if (length == INFINITY) { + return decodeInfinitiveLength(); + } else { + return decodeFixedLength(length); + } + } + + private Array decodeInfinitiveLength() throws CborException { + Array array = new Array(); + array.setChunked(true); + if (decoder.isAutoDecodeInfinitiveArrays()) { + DataItem dataItem; + for (;;) { + dataItem = decoder.decodeNext(); + if (dataItem == null) { + throw new CborException("Unexpected end of stream"); + } + if (Special.BREAK.equals(dataItem)) { + array.add(Special.BREAK); + break; + } + array.add(dataItem); + } + } + return array; + } + + private Array decodeFixedLength(long length) throws CborException { + Array array = new Array(); + for (long i = 0; i < length; i++) { + DataItem dataItem = decoder.decodeNext(); + if (dataItem == null) { + throw new CborException("Unexpected end of stream"); + } + array.add(dataItem); + } + return array; + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/ByteStringDecoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/ByteStringDecoder.java new file mode 100644 index 0000000..84c9075 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/ByteStringDecoder.java @@ -0,0 +1,67 @@ +package de.cotech.hw.fido2.internal.cbor_java.decoder; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; +import de.cotech.hw.fido2.internal.cbor_java.model.Special; + +public class ByteStringDecoder extends AbstractDecoder { + + public ByteStringDecoder(CborDecoder decoder, InputStream inputStream) { + super(decoder, inputStream); + } + + @Override + public ByteString decode(int initialByte) throws CborException { + long length = getLength(initialByte); + if (length == INFINITY) { + if (decoder.isAutoDecodeInfinitiveByteStrings()) { + return decodeInfinitiveLength(); + } else { + ByteString byteString = new ByteString(null); + byteString.setChunked(true); + return byteString; + } + } else { + return decodeFixedLength(length); + } + } + + private ByteString decodeInfinitiveLength() throws CborException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + for (;;) { + DataItem dataItem = decoder.decodeNext(); + if (dataItem == null) { + throw new CborException("Unexpected end of stream"); + } + MajorType majorType = dataItem.getMajorType(); + if (Special.BREAK.equals(dataItem)) { + break; + } else if (majorType == MajorType.BYTE_STRING) { + ByteString byteString = (ByteString) dataItem; + byte[] byteArray = byteString.getBytes(); + if (byteArray != null) { + bytes.write(byteArray, 0, byteArray.length); + } + } else { + throw new CborException("Unexpected major type " + + majorType); + } + } + return new ByteString(bytes.toByteArray()); + } + + private ByteString decodeFixedLength(long length) throws CborException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream((int) length); + for (long i = 0; i < length; i++) { + bytes.write(nextSymbol()); + } + return new ByteString(bytes.toByteArray()); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/DoublePrecisionFloatDecoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/DoublePrecisionFloatDecoder.java new file mode 100644 index 0000000..8618a19 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/DoublePrecisionFloatDecoder.java @@ -0,0 +1,38 @@ +package de.cotech.hw.fido2.internal.cbor_java.decoder; + +import java.io.InputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.DoublePrecisionFloat; + +public class DoublePrecisionFloatDecoder extends + AbstractDecoder { + + public DoublePrecisionFloatDecoder(CborDecoder decoder, + InputStream inputStream) { + super(decoder, inputStream); + } + + @Override + public DoublePrecisionFloat decode(int initialByte) throws CborException { + long bits = 0; + bits |= nextSymbol() & 0xFF; + bits <<= 8; + bits |= nextSymbol() & 0xFF; + bits <<= 8; + bits |= nextSymbol() & 0xFF; + bits <<= 8; + bits |= nextSymbol() & 0xFF; + bits <<= 8; + bits |= nextSymbol() & 0xFF; + bits <<= 8; + bits |= nextSymbol() & 0xFF; + bits <<= 8; + bits |= nextSymbol() & 0xFF; + bits <<= 8; + bits |= nextSymbol() & 0xFF; + return new DoublePrecisionFloat(Double.longBitsToDouble(bits)); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/HalfPrecisionFloatDecoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/HalfPrecisionFloatDecoder.java new file mode 100644 index 0000000..e1f5aa6 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/HalfPrecisionFloatDecoder.java @@ -0,0 +1,43 @@ +package de.cotech.hw.fido2.internal.cbor_java.decoder; + +import java.io.InputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.HalfPrecisionFloat; + +public class HalfPrecisionFloatDecoder extends + AbstractDecoder { + + public HalfPrecisionFloatDecoder(CborDecoder decoder, + InputStream inputStream) { + super(decoder, inputStream); + } + + @Override + public HalfPrecisionFloat decode(int initialByte) throws CborException { + int bits = nextSymbol() << 8 | nextSymbol(); + return new HalfPrecisionFloat(toFloat(bits)); + } + + /** + * @see http://stackoverflow.com/a/5684578 + */ + private static float toFloat(int bits) { + int s = (bits & 0x8000) >> 15; + int e = (bits & 0x7C00) >> 10; + int f = bits & 0x03FF; + + if (e == 0) { + return (float) ((s != 0 ? -1 : 1) * Math.pow(2, -14) * (f / Math + .pow(2, 10))); + } else if (e == 0x1F) { + return f != 0 ? Float.NaN + : (s != 0 ? -1 : 1) * Float.POSITIVE_INFINITY; + } + + return (float) ((s != 0 ? -1 : 1) * Math.pow(2, e - 15) * (1 + f / Math + .pow(2, 10))); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/MapDecoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/MapDecoder.java new file mode 100644 index 0000000..5d3d6ef --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/MapDecoder.java @@ -0,0 +1,65 @@ +package de.cotech.hw.fido2.internal.cbor_java.decoder; + +import java.io.InputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; +import de.cotech.hw.fido2.internal.cbor_java.model.Special; + +public class MapDecoder extends AbstractDecoder { + + public MapDecoder(CborDecoder decoder, InputStream inputStream) { + super(decoder, inputStream); + } + + @Override + public Map decode(int initialByte) throws CborException { + long length = getLength(initialByte); + if (length == INFINITY) { + return decodeInfinitiveLength(); + } else { + return decodeFixedLength(length); + } + } + + private Map decodeInfinitiveLength() throws CborException { + Map map = new Map(); + map.setChunked(true); + if (decoder.isAutoDecodeInfinitiveMaps()) { + for (;;) { + DataItem key = decoder.decodeNext(); + if (Special.BREAK.equals(key)) { + break; + } + DataItem value = decoder.decodeNext(); + if (key == null || value == null) { + throw new CborException("Unexpected end of stream"); + } + if (decoder.isRejectDuplicateKeys() && map.get(key) != null) { + throw new CborException("Duplicate key found in map"); + } + map.put(key, value); + } + } + return map; + } + + private Map decodeFixedLength(long length) throws CborException { + Map map = new Map((int) length); + for (long i = 0; i < length; i++) { + DataItem key = decoder.decodeNext(); + DataItem value = decoder.decodeNext(); + if (key == null || value == null) { + throw new CborException("Unexpected end of stream"); + } + if (decoder.isRejectDuplicateKeys() && map.get(key) != null) { + throw new CborException("Duplicate key found in map"); + } + map.put(key, value); + } + return map; + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/NegativeIntegerDecoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/NegativeIntegerDecoder.java new file mode 100644 index 0000000..20a6180 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/NegativeIntegerDecoder.java @@ -0,0 +1,23 @@ +package de.cotech.hw.fido2.internal.cbor_java.decoder; + +import java.io.InputStream; +import java.math.BigInteger; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.NegativeInteger; + +public class NegativeIntegerDecoder extends AbstractDecoder { + + private static final BigInteger MINUS_ONE = BigInteger.valueOf(-1); + + public NegativeIntegerDecoder(CborDecoder decoder, InputStream inputStream) { + super(decoder, inputStream); + } + + @Override + public NegativeInteger decode(int initialByte) throws CborException { + return new NegativeInteger(MINUS_ONE.subtract(getLengthAsBigInteger(initialByte))); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/SinglePrecisionFloatDecoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/SinglePrecisionFloatDecoder.java new file mode 100644 index 0000000..9f8a2d6 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/SinglePrecisionFloatDecoder.java @@ -0,0 +1,28 @@ +package de.cotech.hw.fido2.internal.cbor_java.decoder; + +import java.io.InputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.SinglePrecisionFloat; + +public class SinglePrecisionFloatDecoder extends AbstractDecoder { + + public SinglePrecisionFloatDecoder(CborDecoder decoder, InputStream inputStream) { + super(decoder, inputStream); + } + + @Override + public SinglePrecisionFloat decode(int initialByte) throws CborException { + int bits = 0; + bits |= nextSymbol() & 0xFF; + bits <<= 8; + bits |= nextSymbol() & 0xFF; + bits <<= 8; + bits |= nextSymbol() & 0xFF; + bits <<= 8; + bits |= nextSymbol() & 0xFF; + return new SinglePrecisionFloat(Float.intBitsToFloat(bits)); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/SpecialDecoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/SpecialDecoder.java new file mode 100644 index 0000000..6f8823f --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/SpecialDecoder.java @@ -0,0 +1,60 @@ +package de.cotech.hw.fido2.internal.cbor_java.decoder; + +import java.io.InputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.SimpleValue; +import de.cotech.hw.fido2.internal.cbor_java.model.SimpleValueType; +import de.cotech.hw.fido2.internal.cbor_java.model.Special; +import de.cotech.hw.fido2.internal.cbor_java.model.SpecialType; + +public class SpecialDecoder extends AbstractDecoder { + + private final HalfPrecisionFloatDecoder halfPrecisionFloatDecoder; + private final SinglePrecisionFloatDecoder singlePrecisionFloatDecoder; + private final DoublePrecisionFloatDecoder doublePrecisionFloatDecoder; + + public SpecialDecoder(CborDecoder decoder, InputStream inputStream) { + super(decoder, inputStream); + this.halfPrecisionFloatDecoder = new HalfPrecisionFloatDecoder(decoder, inputStream); + this.singlePrecisionFloatDecoder = new SinglePrecisionFloatDecoder(decoder, inputStream); + this.doublePrecisionFloatDecoder = new DoublePrecisionFloatDecoder(decoder, inputStream); + } + + @Override + public Special decode(int initialByte) throws CborException { + switch (SpecialType.ofByte(initialByte)) { + case BREAK: + return Special.BREAK; + case SIMPLE_VALUE: + switch (SimpleValueType.ofByte(initialByte)) { + case FALSE: + return SimpleValue.FALSE; + case TRUE: + return SimpleValue.TRUE; + case NULL: + return SimpleValue.NULL; + case UNDEFINED: + return SimpleValue.UNDEFINED; + case UNALLOCATED: + return new SimpleValue(initialByte & 31); + case RESERVED: + default: + throw new CborException("Not implemented"); + } + case IEEE_754_HALF_PRECISION_FLOAT: + return halfPrecisionFloatDecoder.decode(initialByte); + case IEEE_754_SINGLE_PRECISION_FLOAT: + return singlePrecisionFloatDecoder.decode(initialByte); + case IEEE_754_DOUBLE_PRECISION_FLOAT: + return doublePrecisionFloatDecoder.decode(initialByte); + case SIMPLE_VALUE_NEXT_BYTE: + return new SimpleValue(nextSymbol()); + case UNALLOCATED: + default: + throw new CborException("Not implemented"); + } + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/TagDecoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/TagDecoder.java new file mode 100644 index 0000000..7386380 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/TagDecoder.java @@ -0,0 +1,20 @@ +package de.cotech.hw.fido2.internal.cbor_java.decoder; + +import java.io.InputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.Tag; + +public class TagDecoder extends AbstractDecoder { + + public TagDecoder(CborDecoder decoder, InputStream inputStream) { + super(decoder, inputStream); + } + + @Override + public Tag decode(int initialByte) throws CborException { + return new Tag(getLength(initialByte)); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/UnicodeStringDecoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/UnicodeStringDecoder.java new file mode 100644 index 0000000..34394c7 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/UnicodeStringDecoder.java @@ -0,0 +1,65 @@ +package de.cotech.hw.fido2.internal.cbor_java.decoder; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; +import de.cotech.hw.fido2.internal.cbor_java.model.Special; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; + +public class UnicodeStringDecoder extends AbstractDecoder { + + public UnicodeStringDecoder(CborDecoder decoder, InputStream inputStream) { + super(decoder, inputStream); + } + + @Override + public UnicodeString decode(int initialByte) throws CborException { + long length = getLength(initialByte); + if (length == INFINITY) { + if (decoder.isAutoDecodeInfinitiveUnicodeStrings()) { + return decodeInfinitiveLength(); + } else { + UnicodeString unicodeString = new UnicodeString(null); + unicodeString.setChunked(true); + return unicodeString; + } + } else { + return decodeFixedLength(length); + } + } + + private UnicodeString decodeInfinitiveLength() throws CborException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + for (;;) { + DataItem dataItem = decoder.decodeNext(); + if (dataItem == null) { + throw new CborException("Unexpected end of stream"); + } + MajorType majorType = dataItem.getMajorType(); + if (Special.BREAK.equals(dataItem)) { + break; + } else if (majorType == MajorType.UNICODE_STRING) { + UnicodeString unicodeString = (UnicodeString) dataItem; + byte[] byteArray = unicodeString.toString().getBytes(StandardCharsets.UTF_8); + bytes.write(byteArray, 0, byteArray.length); + } else { + throw new CborException("Unexpected major type " + majorType); + } + } + return new UnicodeString(new String(bytes.toByteArray(), StandardCharsets.UTF_8)); + } + + private UnicodeString decodeFixedLength(long length) throws CborException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream((int) length); + for (long i = 0; i < length; i++) { + bytes.write(nextSymbol()); + } + return new UnicodeString(new String(bytes.toByteArray(), StandardCharsets.UTF_8)); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/UnsignedIntegerDecoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/UnsignedIntegerDecoder.java new file mode 100644 index 0000000..805bd10 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/decoder/UnsignedIntegerDecoder.java @@ -0,0 +1,20 @@ +package de.cotech.hw.fido2.internal.cbor_java.decoder; + +import java.io.InputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.UnsignedInteger; + +public class UnsignedIntegerDecoder extends AbstractDecoder { + + public UnsignedIntegerDecoder(CborDecoder decoder, InputStream inputStream) { + super(decoder, inputStream); + } + + @Override + public UnsignedInteger decode(int initialByte) throws CborException { + return new UnsignedInteger(getLengthAsBigInteger(initialByte)); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/AbstractEncoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/AbstractEncoder.java new file mode 100644 index 0000000..48e1598 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/AbstractEncoder.java @@ -0,0 +1,131 @@ +package de.cotech.hw.fido2.internal.cbor_java.encoder; + +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.AdditionalInformation; +import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; +import de.cotech.hw.fido2.internal.cbor_java.model.Tag; + +public abstract class AbstractEncoder { + + private final OutputStream outputStream; + protected final CborEncoder encoder; + + public AbstractEncoder(CborEncoder encoder, OutputStream outputStream) { + this.encoder = encoder; + this.outputStream = outputStream; + } + + public abstract void encode(T dataItem) throws CborException; + + protected void encodeTypeChunked(MajorType majorType) throws CborException { + int symbol = majorType.getValue() << 5; + symbol |= AdditionalInformation.INDEFINITE.getValue(); + try { + outputStream.write(symbol); + } catch (IOException ioException) { + throw new CborException(ioException); + } + } + + protected void encodeTypeAndLength(MajorType majorType, long length) throws CborException { + int symbol = majorType.getValue() << 5; + if (length <= 23L) { + write((int) (symbol | length)); + } else if (length <= 255L) { + symbol |= AdditionalInformation.ONE_BYTE.getValue(); + write(symbol); + write((int) length); + } else if (length <= 65535L) { + symbol |= AdditionalInformation.TWO_BYTES.getValue(); + write(symbol); + write((int) (length >> 8)); + write((int) (length & 0xFF)); + } else if (length <= 4294967295L) { + symbol |= AdditionalInformation.FOUR_BYTES.getValue(); + write(symbol); + write((int) ((length >> 24) & 0xFF)); + write((int) ((length >> 16) & 0xFF)); + write((int) ((length >> 8) & 0xFF)); + write((int) (length & 0xFF)); + } else { + symbol |= AdditionalInformation.EIGHT_BYTES.getValue(); + write(symbol); + write((int) ((length >> 56) & 0xFF)); + write((int) ((length >> 48) & 0xFF)); + write((int) ((length >> 40) & 0xFF)); + write((int) ((length >> 32) & 0xFF)); + write((int) ((length >> 24) & 0xFF)); + write((int) ((length >> 16) & 0xFF)); + write((int) ((length >> 8) & 0xFF)); + write((int) (length & 0xFF)); + } + } + + protected void encodeTypeAndLength(MajorType majorType, BigInteger length) throws CborException { + boolean negative = majorType == MajorType.NEGATIVE_INTEGER; + int symbol = majorType.getValue() << 5; + if (length.compareTo(BigInteger.valueOf(24)) == -1) { + write(symbol | length.intValue()); + } else if (length.compareTo(BigInteger.valueOf(256)) == -1) { + symbol |= AdditionalInformation.ONE_BYTE.getValue(); + write(symbol); + write(length.intValue()); + } else if (length.compareTo(BigInteger.valueOf(65536L)) == -1) { + symbol |= AdditionalInformation.TWO_BYTES.getValue(); + write(symbol); + long twoByteValue = length.longValue(); + write((int) (twoByteValue >> 8)); + write((int) (twoByteValue & 0xFF)); + } else if (length.compareTo(BigInteger.valueOf(4294967296L)) == -1) { + symbol |= AdditionalInformation.FOUR_BYTES.getValue(); + write(symbol); + long fourByteValue = length.longValue(); + write((int) ((fourByteValue >> 24) & 0xFF)); + write((int) ((fourByteValue >> 16) & 0xFF)); + write((int) ((fourByteValue >> 8) & 0xFF)); + write((int) (fourByteValue & 0xFF)); + } else if (length.compareTo(new BigInteger("18446744073709551616")) == -1) { + symbol |= AdditionalInformation.EIGHT_BYTES.getValue(); + write(symbol); + BigInteger mask = BigInteger.valueOf(0xFF); + write(length.shiftRight(56).and(mask).intValue()); + write(length.shiftRight(48).and(mask).intValue()); + write(length.shiftRight(40).and(mask).intValue()); + write(length.shiftRight(32).and(mask).intValue()); + write(length.shiftRight(24).and(mask).intValue()); + write(length.shiftRight(16).and(mask).intValue()); + write(length.shiftRight(8).and(mask).intValue()); + write(length.and(mask).intValue()); + } else { + if (negative) { + encoder.encode(new Tag(3)); + } else { + encoder.encode(new Tag(2)); + } + encoder.encode(new ByteString(length.toByteArray())); + } + } + + protected void write(int b) throws CborException { + try { + outputStream.write(b); + } catch (IOException ioException) { + throw new CborException(ioException); + } + } + + protected void write(byte[] bytes) throws CborException { + try { + outputStream.write(bytes); + } catch (IOException ioException) { + throw new CborException(ioException); + } + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/ArrayEncoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/ArrayEncoder.java new file mode 100644 index 0000000..80a39dc --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/ArrayEncoder.java @@ -0,0 +1,31 @@ +package de.cotech.hw.fido2.internal.cbor_java.encoder; + +import java.io.OutputStream; +import java.util.List; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.Array; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; + +public class ArrayEncoder extends AbstractEncoder { + + public ArrayEncoder(CborEncoder encoder, OutputStream outputStream) { + super(encoder, outputStream); + } + + @Override + public void encode(Array array) throws CborException { + List dataItems = array.getDataItems(); + if (array.isChunked()) { + encodeTypeChunked(MajorType.ARRAY); + } else { + encodeTypeAndLength(MajorType.ARRAY, dataItems.size()); + } + for (DataItem dataItem : dataItems) { + encoder.encode(dataItem); + } + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/ByteStringEncoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/ByteStringEncoder.java new file mode 100644 index 0000000..cdf7cb4 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/ByteStringEncoder.java @@ -0,0 +1,33 @@ +package de.cotech.hw.fido2.internal.cbor_java.encoder; + +import java.io.OutputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; +import de.cotech.hw.fido2.internal.cbor_java.model.SimpleValue; + +public class ByteStringEncoder extends AbstractEncoder { + + public ByteStringEncoder(CborEncoder encoder, OutputStream outputStream) { + super(encoder, outputStream); + } + + @Override + public void encode(ByteString byteString) throws CborException { + byte[] bytes = byteString.getBytes(); + if (byteString.isChunked()) { + encodeTypeChunked(MajorType.BYTE_STRING); + if (bytes != null) { + encode(new ByteString(bytes)); + } + } else if (bytes == null) { + encoder.encode(SimpleValue.NULL); + } else { + encodeTypeAndLength(MajorType.BYTE_STRING, bytes.length); + write(bytes); + } + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/DoublePrecisionFloatEncoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/DoublePrecisionFloatEncoder.java new file mode 100644 index 0000000..9b76d22 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/DoublePrecisionFloatEncoder.java @@ -0,0 +1,29 @@ +package de.cotech.hw.fido2.internal.cbor_java.encoder; + +import java.io.OutputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.DoublePrecisionFloat; + +public class DoublePrecisionFloatEncoder extends AbstractEncoder { + + public DoublePrecisionFloatEncoder(CborEncoder encoder, OutputStream outputStream) { + super(encoder, outputStream); + } + + @Override + public void encode(DoublePrecisionFloat dataItem) throws CborException { + write((7 << 5) | 27); + long bits = Double.doubleToRawLongBits(dataItem.getValue()); + write((int) ((bits >> 56) & 0xFF)); + write((int) ((bits >> 48) & 0xFF)); + write((int) ((bits >> 40) & 0xFF)); + write((int) ((bits >> 32) & 0xFF)); + write((int) ((bits >> 24) & 0xFF)); + write((int) ((bits >> 16) & 0xFF)); + write((int) ((bits >> 8) & 0xFF)); + write((int) ((bits >> 0) & 0xFF)); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/HalfPrecisionFloatEncoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/HalfPrecisionFloatEncoder.java new file mode 100644 index 0000000..26491a1 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/HalfPrecisionFloatEncoder.java @@ -0,0 +1,58 @@ +package de.cotech.hw.fido2.internal.cbor_java.encoder; + +import java.io.OutputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.HalfPrecisionFloat; + +public class HalfPrecisionFloatEncoder extends AbstractEncoder { + + public HalfPrecisionFloatEncoder(CborEncoder encoder, OutputStream outputStream) { + super(encoder, outputStream); + } + + @Override + public void encode(HalfPrecisionFloat dataItem) throws CborException { + write((7 << 5) | 25); + int bits = fromFloat(dataItem.getValue()); + write((bits >> 8) & 0xFF); + write((bits >> 0) & 0xFF); + } + + /** + * @see Stack Overflow + */ + + // returns all higher 16 bits as 0 for all results + public static int fromFloat(float fval) { + int fbits = Float.floatToIntBits(fval); + int sign = fbits >>> 16 & 0x8000; // sign only + int val = 0x1000 + fbits & 0x7fffffff; // rounded value + + if (val >= 0x47800000) // might be or become NaN/Inf + { // avoid Inf due to rounding + if ((fbits & 0x7fffffff) >= 0x47800000) { // is or must become + // NaN/Inf + if (val < 0x7f800000) {// was value but too large + return sign | 0x7c00; // make it +/-Inf + } + return sign | 0x7c00 | // remains +/-Inf or NaN + (fbits & 0x007fffff) >>> 13; // keep NaN (and + // Inf) bits + } + return sign | 0x7bff; // unrounded not quite Inf + } + if (val >= 0x38800000) { // remains normalized value + return sign | val - 0x38000000 >>> 13; // exp - 127 + 15 + } + if (val < 0x33000000) { // too small for subnormal + return sign; // becomes +/-0 + } + val = (fbits & 0x7fffffff) >>> 23; // tmp exp for subnormal calc + return sign | ((fbits & 0x7fffff | 0x800000) // add subnormal bit + + (0x800000 >>> val - 102) // round depending on cut off + >>> 126 - val); // div by 2^(1-(exp-127+15)) and >> 13 | exp=0 + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/MapEncoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/MapEncoder.java new file mode 100644 index 0000000..2e039ab --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/MapEncoder.java @@ -0,0 +1,100 @@ +package de.cotech.hw.fido2.internal.cbor_java.encoder; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.util.Collection; +import java.util.Comparator; +import java.util.TreeMap; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; +import de.cotech.hw.fido2.internal.cbor_java.model.SimpleValue; + +public class MapEncoder extends AbstractEncoder { + + public MapEncoder(CborEncoder encoder, OutputStream outputStream) { + super(encoder, outputStream); + } + + @Override + public void encode(Map map) throws CborException { + Collection keys = map.getKeys(); + + if (map.isChunked()) { + encodeTypeChunked(MajorType.MAP); + } else { + encodeTypeAndLength(MajorType.MAP, keys.size()); + } + + if (keys.isEmpty()) { + return; + } + + if (map.isChunked()) { + for (DataItem key : keys) { + encoder.encode(key); + encoder.encode(map.get(key)); + } + encoder.encode(SimpleValue.BREAK); + return; + } + + /** + * The keys in every map must be sorted lowest value to highest. Sorting + * is performed on the bytes of the representation of the key data items + * without paying attention to the 3/5 bit splitting for major types. + * (Note that this rule allows maps that have keys of different types, + * even though that is probably a bad practice that could lead to errors + * in some canonicalization implementations.) The sorting rules are: + * + * If two keys have different lengths, the shorter one sorts earlier; + * + * If two keys have the same length, the one with the lower value in + * (byte-wise) lexical order sorts earlier. + */ + + TreeMap sortedMap = new TreeMap<>(new Comparator() { + + @Override + public int compare(byte[] o1, byte[] o2) { + if (o1.length < o2.length) { + return -1; + } + if (o1.length > o2.length) { + return 1; + } + for (int i = 0; i < o1.length; i++) { + if (o1[i] < o2[i]) { + return -1; + } + if (o1[i] > o2[i]) { + return 1; + } + } + return 0; + } + + }); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + CborEncoder e = new CborEncoder(byteArrayOutputStream); + for (DataItem key : keys) { + // Key + e.encode(key); + byte[] keyBytes = byteArrayOutputStream.toByteArray(); + byteArrayOutputStream.reset(); + // Value + e.encode(map.get(key)); + byte[] valueBytes = byteArrayOutputStream.toByteArray(); + byteArrayOutputStream.reset(); + sortedMap.put(keyBytes, valueBytes); + } + for (java.util.Map.Entry entry : sortedMap.entrySet()) { + write(entry.getKey()); + write(entry.getValue()); + } + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/NegativeIntegerEncoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/NegativeIntegerEncoder.java new file mode 100644 index 0000000..9a28fd8 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/NegativeIntegerEncoder.java @@ -0,0 +1,24 @@ +package de.cotech.hw.fido2.internal.cbor_java.encoder; + +import java.io.OutputStream; +import java.math.BigInteger; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; +import de.cotech.hw.fido2.internal.cbor_java.model.NegativeInteger; + +public class NegativeIntegerEncoder extends AbstractEncoder { + + private static final BigInteger MINUS_ONE = BigInteger.valueOf(-1); + + public NegativeIntegerEncoder(CborEncoder encoder, OutputStream outputStream) { + super(encoder, outputStream); + } + + @Override + public void encode(NegativeInteger dataItem) throws CborException { + encodeTypeAndLength(MajorType.NEGATIVE_INTEGER, MINUS_ONE.subtract(dataItem.getValue()).abs()); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/SinglePrecisionFloatEncoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/SinglePrecisionFloatEncoder.java new file mode 100644 index 0000000..fbb16c7 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/SinglePrecisionFloatEncoder.java @@ -0,0 +1,25 @@ +package de.cotech.hw.fido2.internal.cbor_java.encoder; + +import java.io.OutputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.SinglePrecisionFloat; + +public class SinglePrecisionFloatEncoder extends AbstractEncoder { + + public SinglePrecisionFloatEncoder(CborEncoder encoder, OutputStream outputStream) { + super(encoder, outputStream); + } + + @Override + public void encode(SinglePrecisionFloat dataItem) throws CborException { + write((7 << 5) | 26); + int bits = Float.floatToRawIntBits(dataItem.getValue()); + write((bits >> 24) & 0xFF); + write((bits >> 16) & 0xFF); + write((bits >> 8) & 0xFF); + write((bits >> 0) & 0xFF); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/SpecialEncoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/SpecialEncoder.java new file mode 100644 index 0000000..0fe6b49 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/SpecialEncoder.java @@ -0,0 +1,81 @@ +package de.cotech.hw.fido2.internal.cbor_java.encoder; + +import java.io.OutputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.DoublePrecisionFloat; +import de.cotech.hw.fido2.internal.cbor_java.model.HalfPrecisionFloat; +import de.cotech.hw.fido2.internal.cbor_java.model.SimpleValue; +import de.cotech.hw.fido2.internal.cbor_java.model.SimpleValueType; +import de.cotech.hw.fido2.internal.cbor_java.model.SinglePrecisionFloat; +import de.cotech.hw.fido2.internal.cbor_java.model.Special; + +public class SpecialEncoder extends AbstractEncoder { + + private final HalfPrecisionFloatEncoder halfPrecisionFloatEncoder; + private final SinglePrecisionFloatEncoder singlePrecisionFloatEncoder; + private final DoublePrecisionFloatEncoder doublePrecisionFloatEncoder; + + public SpecialEncoder(CborEncoder encoder, OutputStream outputStream) { + super(encoder, outputStream); + halfPrecisionFloatEncoder = new HalfPrecisionFloatEncoder(encoder, outputStream); + singlePrecisionFloatEncoder = new SinglePrecisionFloatEncoder(encoder, outputStream); + doublePrecisionFloatEncoder = new DoublePrecisionFloatEncoder(encoder, outputStream); + } + + @Override + public void encode(Special dataItem) throws CborException { + switch (dataItem.getSpecialType()) { + case BREAK: + write((7 << 5) | 31); + break; + case SIMPLE_VALUE: + SimpleValue simpleValue = (SimpleValue) dataItem; + switch (simpleValue.getSimpleValueType()) { + case FALSE: + case NULL: + case TRUE: + case UNDEFINED: + SimpleValueType type = simpleValue.getSimpleValueType(); + write((7 << 5) | type.getValue()); + break; + case UNALLOCATED: + write((7 << 5) | simpleValue.getValue()); + break; + case RESERVED: + break; + } + break; + case UNALLOCATED: + throw new CborException("Unallocated special type"); + case IEEE_754_HALF_PRECISION_FLOAT: + if (!(dataItem instanceof HalfPrecisionFloat)) { + throw new CborException("Wrong data item type"); + } + halfPrecisionFloatEncoder.encode((HalfPrecisionFloat) dataItem); + break; + case IEEE_754_SINGLE_PRECISION_FLOAT: + if (!(dataItem instanceof SinglePrecisionFloat)) { + throw new CborException("Wrong data item type"); + } + singlePrecisionFloatEncoder.encode((SinglePrecisionFloat) dataItem); + break; + case IEEE_754_DOUBLE_PRECISION_FLOAT: + if (!(dataItem instanceof DoublePrecisionFloat)) { + throw new CborException("Wrong data item type"); + } + doublePrecisionFloatEncoder.encode((DoublePrecisionFloat) dataItem); + break; + case SIMPLE_VALUE_NEXT_BYTE: + if (!(dataItem instanceof SimpleValue)) { + throw new CborException("Wrong data item type"); + } + SimpleValue simpleValueNextByte = (SimpleValue) dataItem; + write((7 << 5) | 24); + write(simpleValueNextByte.getValue()); + break; + } + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/TagEncoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/TagEncoder.java new file mode 100644 index 0000000..e095a7e --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/TagEncoder.java @@ -0,0 +1,21 @@ +package de.cotech.hw.fido2.internal.cbor_java.encoder; + +import java.io.OutputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; +import de.cotech.hw.fido2.internal.cbor_java.model.Tag; + +public class TagEncoder extends AbstractEncoder { + + public TagEncoder(CborEncoder encoder, OutputStream outputStream) { + super(encoder, outputStream); + } + + @Override + public void encode(Tag tag) throws CborException { + encodeTypeAndLength(MajorType.TAG, tag.getValue()); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/UnicodeStringEncoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/UnicodeStringEncoder.java new file mode 100644 index 0000000..0e618e9 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/UnicodeStringEncoder.java @@ -0,0 +1,36 @@ +package de.cotech.hw.fido2.internal.cbor_java.encoder; + +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; +import de.cotech.hw.fido2.internal.cbor_java.model.SimpleValue; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; + +public class UnicodeStringEncoder extends AbstractEncoder { + + public UnicodeStringEncoder(CborEncoder encoder, OutputStream outputStream) { + super(encoder, outputStream); + } + + @Override + public void encode(UnicodeString dataItem) throws CborException { + String string = dataItem.getString(); + if (dataItem.isChunked()) { + encodeTypeChunked(MajorType.UNICODE_STRING); + if (string != null) { + encode(new UnicodeString(string)); + } + } else if (string == null) { + encoder.encode(SimpleValue.NULL); + } else { + byte[] bytes; + bytes = string.getBytes(StandardCharsets.UTF_8); + encodeTypeAndLength(MajorType.UNICODE_STRING, bytes.length); + write(bytes); + } + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/UnsignedIntegerEncoder.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/UnsignedIntegerEncoder.java new file mode 100644 index 0000000..4b8ef0a --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/encoder/UnsignedIntegerEncoder.java @@ -0,0 +1,21 @@ +package de.cotech.hw.fido2.internal.cbor_java.encoder; + +import java.io.OutputStream; + +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; +import de.cotech.hw.fido2.internal.cbor_java.model.UnsignedInteger; + +public class UnsignedIntegerEncoder extends AbstractEncoder { + + public UnsignedIntegerEncoder(CborEncoder encoder, OutputStream outputStream) { + super(encoder, outputStream); + } + + @Override + public void encode(UnsignedInteger dataItem) throws CborException { + encodeTypeAndLength(MajorType.UNSIGNED_INTEGER, dataItem.getValue()); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/AbstractFloat.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/AbstractFloat.java new file mode 100644 index 0000000..3da48e3 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/AbstractFloat.java @@ -0,0 +1,32 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.util.Objects; + +public class AbstractFloat extends Special { + + private final float value; + + public AbstractFloat(SpecialType specialType, float value) { + super(specialType); + this.value = value; + } + + public float getValue() { + return value; + } + + @Override + public boolean equals(Object object) { + if (object instanceof AbstractFloat) { + AbstractFloat other = (AbstractFloat) object; + return super.equals(object) && value == other.value; + } + return false; + } + + @Override + public int hashCode() { + return super.hashCode() ^ Objects.hashCode(value); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/AdditionalInformation.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/AdditionalInformation.java new file mode 100644 index 0000000..129b2dd --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/AdditionalInformation.java @@ -0,0 +1,55 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +/** + * The initial byte of each data item contains both information about the major + * type (the high-order 3 bits) and additional information (the low-order 5 + * bits). When the value of the additional information is less than 24, it is + * directly used as a small unsigned integer. When it is 24 to 27, the + * additional bytes for a variable-length integer immediately follow; the values + * 24 to 27 of the additional information specify that its length is a 1-, 2-, + * 4- or 8-byte unsigned integer, respectively. Additional information value 31 + * is used for indefinite length items, described in Section 2.2. Additional + * information values 28 to 30 are reserved for future expansion. + */ +public enum AdditionalInformation { + + DIRECT(0), // 0-23 + ONE_BYTE(24), // 24 + TWO_BYTES(25), // 25 + FOUR_BYTES(26), // 26 + EIGHT_BYTES(27), // 27 + RESERVED(28), // 28-30 + INDEFINITE(31); // 31 + + private final int value; + + private AdditionalInformation(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static AdditionalInformation ofByte(int b) { + switch (b & 31) { + case 24: + return ONE_BYTE; + case 25: + return TWO_BYTES; + case 26: + return FOUR_BYTES; + case 27: + return EIGHT_BYTES; + case 28: + case 29: + case 30: + return RESERVED; + case 31: + return INDEFINITE; + default: + return DIRECT; + } + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Array.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Array.java new file mode 100644 index 0000000..44eb5d7 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Array.java @@ -0,0 +1,49 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Array extends ChunkableDataItem { + + private final ArrayList objects; + + public Array() { + super(MajorType.ARRAY); + objects = new ArrayList<>(); + } + + public Array add(DataItem object) { + objects.add(object); + return this; + } + + public List getDataItems() { + return objects; + } + + @Override + public boolean equals(Object object) { + if (object instanceof Array) { + Array other = (Array) object; + return super.equals(object) && objects.equals(other.objects); + } + return false; + } + + @Override + public int hashCode() { + return super.hashCode() ^ objects.hashCode(); + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder("["); + if (isChunked()) { + stringBuilder.append("_ "); + } + stringBuilder.append(Arrays.toString(objects.toArray()).substring(1)); + return stringBuilder.toString(); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/ByteString.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/ByteString.java new file mode 100644 index 0000000..92d15e9 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/ByteString.java @@ -0,0 +1,40 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.util.Arrays; + +public class ByteString extends ChunkableDataItem { + + private final byte[] bytes; + + public ByteString(byte[] bytes) { + super(MajorType.BYTE_STRING); + if (bytes == null) { + this.bytes = null; + } else { + this.bytes = bytes; + } + } + + public byte[] getBytes() { + if (bytes == null) { + return null; + } else { + return bytes; + } + } + + @Override + public boolean equals(Object object) { + if (object instanceof ByteString) { + ByteString other = (ByteString) object; + return super.equals(object) && Arrays.equals(bytes, other.bytes); + } + return false; + } + + @Override + public int hashCode() { + return super.hashCode() ^ Arrays.hashCode(bytes); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/ChunkableDataItem.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/ChunkableDataItem.java new file mode 100644 index 0000000..4a8fc0c --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/ChunkableDataItem.java @@ -0,0 +1,36 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.util.Objects; + +class ChunkableDataItem extends DataItem { + + private boolean chunked = false; + + protected ChunkableDataItem(MajorType majorType) { + super(majorType); + } + + public boolean isChunked() { + return chunked; + } + + public ChunkableDataItem setChunked(boolean chunked) { + this.chunked = chunked; + return this; + } + + @Override + public boolean equals(Object object) { + if (object instanceof ChunkableDataItem) { + ChunkableDataItem other = (ChunkableDataItem) object; + return super.equals(object) && chunked == other.chunked; + } + return false; + } + + @Override + public int hashCode() { + return super.hashCode() ^ Objects.hashCode(chunked); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/DataItem.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/DataItem.java new file mode 100644 index 0000000..9ba7338 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/DataItem.java @@ -0,0 +1,69 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.util.Objects; + +public class DataItem { + + private final MajorType majorType; + private Tag tag; + + protected DataItem(MajorType majorType) { + this.majorType = majorType; + Objects.requireNonNull(majorType, "majorType is null"); + } + + public MajorType getMajorType() { + return majorType; + } + + public void setTag(int tag) { + if (tag < 0) { + throw new IllegalArgumentException("tag number must be 0 or greater"); + } + + this.tag = new Tag(tag); + } + + public void setTag(Tag tag) { + Objects.requireNonNull(tag, "tag is null"); + this.tag = tag; + } + + public void removeTag() { + tag = null; + } + + public Tag getTag() { + return tag; + } + + public boolean hasTag() { + return tag != null; + } + + @Override + public boolean equals(Object object) { + if (object instanceof DataItem) { + DataItem other = (DataItem) object; + if (tag != null) { + return tag.equals(other.tag) && majorType == other.majorType; + } else { + return other.tag == null && majorType == other.majorType; + } + } + + return false; + } + + @Override + public int hashCode() { + return Objects.hash(majorType, tag); + } + + protected void assertTrue(boolean condition, String message) { + if (!condition) { + throw new IllegalArgumentException(message); + } + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/DoublePrecisionFloat.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/DoublePrecisionFloat.java new file mode 100644 index 0000000..087790c --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/DoublePrecisionFloat.java @@ -0,0 +1,37 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.util.Objects; + +public class DoublePrecisionFloat extends Special { + + private final double value; + + public DoublePrecisionFloat(double value) { + super(SpecialType.IEEE_754_DOUBLE_PRECISION_FLOAT); + this.value = value; + } + + public double getValue() { + return value; + } + + @Override + public boolean equals(Object object) { + if (object instanceof DoublePrecisionFloat) { + DoublePrecisionFloat other = (DoublePrecisionFloat) object; + return super.equals(object) && value == other.value; + } + return false; + } + + @Override + public int hashCode() { + return super.hashCode() ^ Objects.hashCode(value); + } + + @Override + public String toString() { + return String.valueOf(value); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/HalfPrecisionFloat.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/HalfPrecisionFloat.java new file mode 100644 index 0000000..f8bfb1a --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/HalfPrecisionFloat.java @@ -0,0 +1,9 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +public class HalfPrecisionFloat extends AbstractFloat { + + public HalfPrecisionFloat(float value) { + super(SpecialType.IEEE_754_HALF_PRECISION_FLOAT, value); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/LanguageTaggedString.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/LanguageTaggedString.java new file mode 100644 index 0000000..c6d9ab9 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/LanguageTaggedString.java @@ -0,0 +1,28 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.util.Objects; + +/** + * See https://peteroupc.github.io/CBOR/langtags.html + */ +public class LanguageTaggedString extends Array { + + public LanguageTaggedString(String language, String string) { + this(new UnicodeString(language), new UnicodeString(string)); + } + + public LanguageTaggedString(UnicodeString language, UnicodeString string) { + setTag(38); + add(Objects.requireNonNull(language)); + add(Objects.requireNonNull(string)); + } + + public UnicodeString getLanguage() { + return (UnicodeString) getDataItems().get(0); + } + + public UnicodeString getString() { + return (UnicodeString) getDataItems().get(1); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/MajorType.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/MajorType.java new file mode 100644 index 0000000..1ce5ba7 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/MajorType.java @@ -0,0 +1,126 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +public enum MajorType { + + INVALID(-1), + + /** + * Major type 0: an unsigned integer. The 5-bit additional information is + * either the integer itself (for additional information values 0 through + * 23), or the length of additional data. Additional information 24 means + * the value is represented in an additional uint8_t, 25 means a uint16_t, + * 26 means a uint32_t, and 27 means a uint64_t. For example, the integer 10 + * is denoted as the one byte 0b000_01010 (major type 0, additional + * information 10). The integer 500 would be 0b000_11001 (major type 0, + * additional information 25) followed by the two bytes 0x01f4, which is 500 + * in decimal. + */ + UNSIGNED_INTEGER(0), + + /** + * Major type 1: a negative integer. The encoding follows the rules for + * unsigned integers (major type 0), except that the value is then -1 minus + * the encoded unsigned integer. For example, the integer -500 would be + * 0b001_11001 (major type 1, additional information 25) followed by the two + * bytes 0x01f3, which is 499 in decimal. + */ + NEGATIVE_INTEGER(1), + + /** + * Major type 2: a byte string. The string's length in bytes is represented + * following the rules for positive integers (major type 0). For example, a + * byte string whose length is 5 would have an initial byte of 0b010_00101 + * (major type 2, additional information 5 for the length), followed by 5 + * bytes of binary content. A byte string whose length is 500 would have 3 + * initial bytes of 0b010_11001 (major type 2, additional information 25 to + * indicate a two-byte length) followed by the two bytes 0x01f4 for a length + * of 500, followed by 500 bytes of binary content. + */ + BYTE_STRING(2), + + /** + * Major type 3: string of Unicode characters that is encoded as UTF-8 + * [RFC3629]. The format of this type is identical to that of byte strings + * (major type 2), that is, as with major type 2, the length gives the + * number of bytes. This type is provided for systems that need to interpret + * or display human-readable text. In contrast to formats such as JSON, the + * Unicode characters in this type are never escaped. Thus, a newline + * character (U+000A) is always represented in a string as the byte 0x0a, + * and never as the bytes 0x5c6e (the characters "\" and "n") or as + * 0x5c7530303061 (the characters "\", "u", "0", "0", "0", and "a"). + */ + UNICODE_STRING(3), + + /** + * Major type 4: an array of data items. Arrays are also called lists, + * sequences, or tuples. The array's length follows the rules for byte + * strings (major type 2), except that the length denotes the number of data + * items, not the length in bytes that the array takes up. Items in an array + * do not need to all be of the same type. For example, an array that + * contains 10 items of any type would have an initial byte of 0b100_01010 + * (major type of 4, additional information of 10 for the length) followed + * by the 10 remaining items. + */ + ARRAY(4), + + /** + * Major type 5: a map of pairs of data items. Maps are also called tables, + * dictionaries, hashes, or objects (in JSON). A map is comprised of pairs + * of data items, the even-numbered ones serving as keys and the following + * odd-numbered ones serving as values for the key that comes immediately + * before it. The map's length follows the rules for byte strings (major + * type 2), except that the length denotes the number of pairs, not the + * length in bytes that the map takes up. For example, a map that contains 9 + * pairs would have an initial byte of 0b101_01001 (major type of 5, + * additional information of 9 for the number of pairs) followed by the 18 + * remaining items. The first item is the first key, the second item is the + * first value, the third item is the second key, and so on. + */ + MAP(5), + + /** + * Major type 6: optional semantic tagging of other major types. See Section + * 2.4. + */ + TAG(6), + + /** + * Major type 7: floating point numbers and simple data types that need no + * content, as well as the "break" stop code. See Section 2.3. + */ + SPECIAL(7); + + private final int value; + + private MajorType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static MajorType ofByte(int b) { + switch (b >> 5) { + case 0: + return UNSIGNED_INTEGER; + case 1: + return NEGATIVE_INTEGER; + case 2: + return BYTE_STRING; + case 3: + return UNICODE_STRING; + case 4: + return ARRAY; + case 5: + return MAP; + case 6: + return TAG; + case 7: + return SPECIAL; + default: + return INVALID; + } + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Map.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Map.java new file mode 100644 index 0000000..db91b99 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Map.java @@ -0,0 +1,79 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +public class Map extends ChunkableDataItem { + + private final HashMap map; + private final List keys = new LinkedList<>(); + + public Map() { + super(MajorType.MAP); + map = new HashMap<>(); + } + + public Map(int initialCapacity) { + super(MajorType.MAP); + map = new HashMap<>(initialCapacity); + } + + public Map put(DataItem key, DataItem value) { + if (map.put(key, value) == null) { + keys.add(key); + } + return this; + } + + public DataItem get(DataItem key) { + return map.get(key); + } + + public DataItem remove(DataItem key) { + keys.remove(key); + return map.remove(key); + } + + public Collection getKeys() { + return keys; + } + + public Collection getValues() { + return map.values(); + } + + @Override + public boolean equals(Object object) { + if (object instanceof Map) { + Map other = (Map) object; + return super.equals(object) && map.equals(other.map); + } + return false; + } + + @Override + public int hashCode() { + return super.hashCode() ^ map.hashCode(); + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + if (isChunked()) { + stringBuilder.append("{_ "); + } else { + stringBuilder.append("{ "); + } + for (DataItem key : keys) { + stringBuilder.append(key).append(": ").append(map.get(key)).append(", "); + } + if (stringBuilder.toString().endsWith(", ")) { + stringBuilder.setLength(stringBuilder.length() - 2); + } + stringBuilder.append(" }"); + return stringBuilder.toString(); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/NegativeInteger.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/NegativeInteger.java new file mode 100644 index 0000000..64e506a --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/NegativeInteger.java @@ -0,0 +1,16 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.math.BigInteger; + +public class NegativeInteger extends Number { + + public NegativeInteger(long value) { + this(BigInteger.valueOf(value)); + assertTrue(value < 0L, "value " + value + " is not < 0"); + } + + public NegativeInteger(BigInteger value) { + super(MajorType.NEGATIVE_INTEGER, value); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Number.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Number.java new file mode 100644 index 0000000..fb70dca --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Number.java @@ -0,0 +1,38 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.math.BigInteger; +import java.util.Objects; + +public abstract class Number extends DataItem { + + private final BigInteger value; + + protected Number(MajorType majorType, BigInteger value) { + super(majorType); + this.value = Objects.requireNonNull(value); + } + + public BigInteger getValue() { + return value; + } + + @Override + public boolean equals(Object object) { + if (object instanceof Number) { + Number other = (Number) object; + return super.equals(object) && value.equals(other.value); + } + return false; + } + + @Override + public int hashCode() { + return super.hashCode() ^ value.hashCode(); + } + + @Override + public String toString() { + return value.toString(); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/RationalNumber.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/RationalNumber.java new file mode 100644 index 0000000..e41b4b2 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/RationalNumber.java @@ -0,0 +1,36 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.math.BigInteger; + +import de.cotech.hw.fido2.internal.cbor_java.CborException; + +/** + * Rational Numbers: http://peteroupc.github.io/CBOR/rational.html + */ + +public class RationalNumber extends Array { + + public RationalNumber(Number numerator, Number denominator) throws CborException { + setTag(30); + if (numerator == null) { + throw new CborException("Numerator is null"); + } + if (denominator == null) { + throw new CborException("Denominator is null"); + } + if (denominator.getValue().equals(BigInteger.ZERO)) { + throw new CborException("Denominator is zero"); + } + add(numerator); + add(denominator); + } + + public Number getNumerator() { + return (Number) getDataItems().get(0); + } + + public Number getDenominator() { + return (Number) getDataItems().get(1); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/SimpleValue.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/SimpleValue.java new file mode 100644 index 0000000..3c81dd9 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/SimpleValue.java @@ -0,0 +1,58 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.util.Objects; + +public class SimpleValue extends Special { + + private final SimpleValueType simpleValueType; + + public static final SimpleValue FALSE = new SimpleValue( + SimpleValueType.FALSE); + public static final SimpleValue TRUE = new SimpleValue(SimpleValueType.TRUE); + public static final SimpleValue NULL = new SimpleValue(SimpleValueType.NULL); + public static final SimpleValue UNDEFINED = new SimpleValue( + SimpleValueType.UNDEFINED); + + private final int value; + + public SimpleValue(SimpleValueType simpleValueType) { + super(SpecialType.SIMPLE_VALUE); + this.value = simpleValueType.getValue(); + this.simpleValueType = simpleValueType; + } + + public SimpleValue(int value) { + super(value <= 23 ? SpecialType.SIMPLE_VALUE + : SpecialType.SIMPLE_VALUE_NEXT_BYTE); + this.value = value; + this.simpleValueType = SimpleValueType.ofByte(value); + } + + public SimpleValueType getSimpleValueType() { + return simpleValueType; + } + + public int getValue() { + return value; + } + + @Override + public boolean equals(Object object) { + if (object instanceof SimpleValue) { + SimpleValue other = (SimpleValue) object; + return super.equals(object) && value == other.value; + } + return false; + } + + @Override + public int hashCode() { + return super.hashCode() ^ Objects.hashCode(value); + } + + @Override + public String toString() { + return simpleValueType.toString(); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/SimpleValueType.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/SimpleValueType.java new file mode 100644 index 0000000..725d27f --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/SimpleValueType.java @@ -0,0 +1,46 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +public enum SimpleValueType { + + FALSE(20), + TRUE(21), + NULL(22), + UNDEFINED(23), + RESERVED(0), + UNALLOCATED(0); + + private final int value; + + private SimpleValueType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static SimpleValueType ofByte(int b) { + switch (b & 31) { + case 20: + return FALSE; + case 21: + return TRUE; + case 22: + return NULL; + case 23: + return UNDEFINED; + case 24: + case 25: + case 26: + case 27: + case 28: + case 29: + case 30: + case 31: + return RESERVED; + default: + return UNALLOCATED; + } + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/SinglePrecisionFloat.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/SinglePrecisionFloat.java new file mode 100644 index 0000000..581aa68 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/SinglePrecisionFloat.java @@ -0,0 +1,9 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +public class SinglePrecisionFloat extends AbstractFloat { + + public SinglePrecisionFloat(float value) { + super(SpecialType.IEEE_754_SINGLE_PRECISION_FLOAT, value); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Special.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Special.java new file mode 100644 index 0000000..c322e4a --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Special.java @@ -0,0 +1,39 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.util.Objects; + +public class Special extends DataItem { + + public static final Special BREAK = new Special(SpecialType.BREAK); + + private final SpecialType specialType; + + protected Special(SpecialType specialType) { + super(MajorType.SPECIAL); + this.specialType = Objects.requireNonNull(specialType); + } + + public SpecialType getSpecialType() { + return specialType; + } + + @Override + public boolean equals(Object object) { + if (object instanceof Special) { + Special other = (Special) object; + return super.equals(object) && specialType == other.specialType; + } + return false; + } + + @Override + public int hashCode() { + return super.hashCode() ^ Objects.hashCode(specialType); + } + + @Override + public String toString() { + return specialType.name(); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/SpecialType.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/SpecialType.java new file mode 100644 index 0000000..b3e8e89 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/SpecialType.java @@ -0,0 +1,34 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +public enum SpecialType { + + SIMPLE_VALUE, + SIMPLE_VALUE_NEXT_BYTE, + IEEE_754_HALF_PRECISION_FLOAT, + IEEE_754_SINGLE_PRECISION_FLOAT, + IEEE_754_DOUBLE_PRECISION_FLOAT, + UNALLOCATED, + BREAK; + + public static SpecialType ofByte(int b) { + switch (b & 31) { + case 24: + return SIMPLE_VALUE_NEXT_BYTE; + case 25: + return IEEE_754_HALF_PRECISION_FLOAT; + case 26: + return IEEE_754_SINGLE_PRECISION_FLOAT; + case 27: + return IEEE_754_DOUBLE_PRECISION_FLOAT; + case 28: + case 29: + case 30: + return UNALLOCATED; + case 31: + return BREAK; + default: + return SIMPLE_VALUE; + } + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Tag.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Tag.java new file mode 100644 index 0000000..5901e97 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/Tag.java @@ -0,0 +1,37 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.util.Objects; + +public class Tag extends DataItem { + + private final long value; + + public Tag(long value) { + super(MajorType.TAG); + this.value = value; + } + + public long getValue() { + return value; + } + + @Override + public boolean equals(Object object) { + if (object instanceof Tag) { + Tag other = (Tag) object; + return super.equals(object) && value == other.value; + } + return false; + } + + @Override + public int hashCode() { + return super.hashCode() ^ Objects.hashCode(value); + } + + @Override + public String toString() { + return "Tag("+value+")"; + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/UnicodeString.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/UnicodeString.java new file mode 100644 index 0000000..535e0e5 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/UnicodeString.java @@ -0,0 +1,50 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +public class UnicodeString extends ChunkableDataItem { + + private final String string; + + public UnicodeString(String string) { + super(MajorType.UNICODE_STRING); + this.string = string; + } + + @Override + public String toString() { + if (string == null) { + return "null"; + } else { + return string; + } + } + + public String getString() { + return string; + } + + @Override + public boolean equals(Object object) { + if (object instanceof UnicodeString && super.equals(object)) { + UnicodeString other = (UnicodeString) object; + if (string == null) { + return other.string == null; + } else { + return string.equals(other.string); + } + } + return false; + } + + @Override + public int hashCode() { + int hash = 0; + + if (string != null) { + hash = super.hashCode(); + hash += string.hashCode(); + } + + return hash; + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/UnsignedInteger.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/UnsignedInteger.java new file mode 100644 index 0000000..e365012 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/model/UnsignedInteger.java @@ -0,0 +1,16 @@ +package de.cotech.hw.fido2.internal.cbor_java.model; + +import java.math.BigInteger; + +public class UnsignedInteger extends Number { + + public UnsignedInteger(long value) { + this(BigInteger.valueOf(value)); + assertTrue(value >= 0L, "value " + value + " is not >= 0"); + } + + public UnsignedInteger(BigInteger value) { + super(MajorType.UNSIGNED_INTEGER, value); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/package-info.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/package-info.java new file mode 100644 index 0000000..853871e --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cbor_java/package-info.java @@ -0,0 +1,4 @@ +/** + * Full copy of v0.8 of https://github.com/c-rack/cbor-java + */ +package de.cotech.hw.fido2.internal.cbor_java; \ No newline at end of file diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cose/CoseIdentifiers.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cose/CoseIdentifiers.java new file mode 100644 index 0000000..6acc2ba --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cose/CoseIdentifiers.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.cose; + + +import de.cotech.hw.fido2.internal.cbor_java.model.NegativeInteger; +import de.cotech.hw.fido2.internal.cbor_java.model.Number; +import de.cotech.hw.fido2.internal.cbor_java.model.UnsignedInteger; + + +public class CoseIdentifiers { + public static final Number KTY = new UnsignedInteger(1); + public static final Number ALG = new UnsignedInteger(3); + public static final Number CRV = new NegativeInteger(-1); + public static final Number X = new NegativeInteger(-2); + public static final Number Y = new NegativeInteger(-3); + public static final Number KTY_EC2 = new UnsignedInteger(2); + public static final Number CRV_P256 = new UnsignedInteger(1); + + public enum CoseAlg { + ES256(-7), ECDH_ES_w_HKDF_256(-25); + + public final int label; + public final Number cborLabel; + + public static CoseAlg fromIdentifier(int label) { + switch (label) { + case -7: + return ES256; + default: + return null; + } + } + + CoseAlg(int label) { + this.label = label; + this.cborLabel = label < 0 ? new NegativeInteger(label) : new UnsignedInteger(label); + } + } +} \ No newline at end of file diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cose/CosePublicKeyUtils.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cose/CosePublicKeyUtils.java new file mode 100644 index 0000000..ff7c267 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/cose/CosePublicKeyUtils.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.cose; + + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.List; + +import de.cotech.hw.fido2.internal.cbor_java.CborBuilder; +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; +import de.cotech.hw.fido2.internal.cbor.CborUtils; +import de.cotech.hw.fido2.internal.cose.CoseIdentifiers.CoseAlg; +import de.cotech.hw.util.Arrays; + + +public class CosePublicKeyUtils { + private static final int X962_UNCOMPRESSED = 0x04; + + public static byte[] encodex962PublicKeyAsCose(byte[] publicKey) throws IOException { + if (publicKey.length != 65) { + throw new IOException("Invalid length for X9.62 public key!"); + } + if (publicKey[0] != X962_UNCOMPRESSED) { + throw new IOException("X9.62 public key must be uncompressed format!"); + } + byte[] x = Arrays.copyOfRange(publicKey, 1, 33); + byte[] y = Arrays.copyOfRange(publicKey, 33, 65); + List coseKeyCbor = new CborBuilder() + .addMap() + .put(CoseIdentifiers.KTY, CoseIdentifiers.KTY_EC2) + .put(CoseIdentifiers.ALG, CoseAlg.ES256.cborLabel) + .put(CoseIdentifiers.CRV, CoseIdentifiers.CRV_P256) + .put(CoseIdentifiers.X, new ByteString(x)) + .put(CoseIdentifiers.Y, new ByteString(y)) + .end() + .build(); + try { + return CborUtils.writeCborDataToBytes(coseKeyCbor); + } catch (CborException e) { + throw new IllegalStateException(e); + } + } + + public static byte[] encodeCosePublicKeyAsX962(byte[] publicKey) throws IOException { + DataItem dataItem; + try { + CborDecoder decoder = new CborDecoder(new ByteArrayInputStream(publicKey)); + dataItem = decoder.decodeNext(); + if (decoder.decodeNext() != null) { + throw new IOException("Unexpected trailing CBOR data"); + } + } catch (CborException e) { + throw new IOException("Error parsing CBOR data for COSE public key!", e); + } + if (dataItem.getMajorType() != MajorType.MAP) { + throw new IOException("Expected map in CBOR data, found " + dataItem.getMajorType()); + } + + Map map = (Map) dataItem; + DataItem kty = map.get(CoseIdentifiers.KTY); + if (!CoseIdentifiers.KTY_EC2.equals(kty)) { + throw new IOException("Unexpected kty value. Expected " + CoseIdentifiers.KTY_EC2 + ", got " + kty); + } + DataItem alg = map.get(CoseIdentifiers.ALG); + if (!CoseAlg.ECDH_ES_w_HKDF_256.cborLabel.equals(alg) && !CoseAlg.ES256.cborLabel.equals(alg)) { + throw new IOException("Unexpected alg value. Expected " + CoseAlg.ES256.cborLabel + " or " + CoseAlg.ECDH_ES_w_HKDF_256.cborLabel + ", got " + alg); + } + DataItem crv = map.get(CoseIdentifiers.CRV); + if (!CoseIdentifiers.CRV_P256.equals(crv)) { + throw new IOException("Unexpected crv value. Expected " + CoseIdentifiers.CRV_P256 + ", got " + crv); + } + + DataItem x = map.get(CoseIdentifiers.X); + if (x == null) { + throw new IOException("Missing CBOR field X in COSE public key!"); + } + if (x.getMajorType() != MajorType.BYTE_STRING) { + throw new IOException("Expected X CBOR field to be a ByteString!"); + } + byte[] xBytes = ((ByteString) x).getBytes(); + if (xBytes.length != 32) { + throw new IOException("Expected X field to be 32 bytes, got " + xBytes.length + "!"); + } + + DataItem y = map.get(CoseIdentifiers.Y); + if (y == null) { + throw new IOException("Missing CBOR field X in COSE public key!"); + } + if (y.getMajorType() != MajorType.BYTE_STRING) { + throw new IOException("Expected Y CBOR field to be a ByteString!"); + } + byte[] yBytes = ((ByteString) y).getBytes(); + if (yBytes.length != 32) { + throw new IOException("Expected X field to be 32 bytes, got " + xBytes.length + "!"); + } + + byte[] x9PublicKey = new byte[65]; + x9PublicKey[0] = X962_UNCOMPRESSED; + System.arraycopy(xBytes, 0, x9PublicKey, 1, 32); + System.arraycopy(yBytes, 0, x9PublicKey, 33, 32); + return x9PublicKey; + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/crypto/P256.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/crypto/P256.java new file mode 100644 index 0000000..d81ed1b --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/crypto/P256.java @@ -0,0 +1,204 @@ +// Copyright 2017 The CrunchyCrypt Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This class is adapted from Google Crunchy (https://github.com/google/crunchy) + +package de.cotech.hw.fido2.internal.crypto; + +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.util.Arrays; + +import androidx.annotation.VisibleForTesting; +import javax.crypto.KeyAgreement; + + +/** Static functions for serializing/deserializing P256 points/exponents and point validation. */ +public final class P256 { + private static final int FIELD_LENGTH = 32; + private static final int POINT_LENGTH = FIELD_LENGTH * 2; + @VisibleForTesting + static final ECFieldFp FIELD; + private static final EllipticCurve CURVE; + private static final ECParameterSpec CURVE_SPEC; + + static { + // Curve P-256 + // http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf + BigInteger p = + new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951"); + BigInteger n = + new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"); + BigInteger a = p.subtract(new BigInteger("3")); + BigInteger b = new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16); + BigInteger gx = new BigInteger("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", 16); + BigInteger gy = new BigInteger("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5", 16); + int h = 1; + ECPoint g = new ECPoint(gx, gy); + + FIELD = new ECFieldFp(p); + CURVE = new EllipticCurve(FIELD, a, b); + CURVE_SPEC = new ECParameterSpec(CURVE, g, n, h); + } + + /** Returns a random P256 keyPair. */ + public static KeyPair newKeyPair() throws GeneralSecurityException { + KeyPairGenerator gen = KeyPairGenerator.getInstance("EC"); + gen.initialize(CURVE_SPEC); + return gen.genKeyPair(); + } + + /** Returns true if the given point is on the curve and not the identity point. */ + static boolean isPointOnCurve(ECPoint point) throws GeneralSecurityException { + BigInteger p = FIELD.getP(); + BigInteger x = point.getAffineX(); + BigInteger y = point.getAffineY(); + if (ECPoint.POINT_INFINITY.equals(point)) { + throw new GeneralSecurityException("point is at infinity"); + } + // Check 0 <= x < p and 0 <= y < p + if (x.signum() == -1 || x.compareTo(p) >= 0) { + throw new GeneralSecurityException("x is out of range"); + } + if (y.signum() == -1 || y.compareTo(p) >= 0) { + throw new GeneralSecurityException("y is out of range"); + } + // Check y^2 == x^3 + a x + b (mod p) + BigInteger lhs = y.multiply(y).mod(p); + BigInteger rhs = x.multiply(x).add(CURVE.getA()).multiply(x).add(CURVE.getB()).mod(p); + return lhs.equals(rhs); + } + + /** Performs a Diffie-Hellman key exchange and returns the result. */ + static byte[] ecdh(byte[] publicKey, byte[] privateKey) throws GeneralSecurityException { + KeyAgreement ka = KeyAgreement.getInstance("ECDH"); + ka.init(P256.deserializePrivateKey(privateKey)); + ka.doPhase(P256.deserializePublicKey(publicKey), true); + return ka.generateSecret(); + } + + /** + * Returns the concatenation of the two serialized {@code ECPoint} coordinates contained in the + * publicKey, where each point is serialized as a 32-byte big-endian integer. + */ + public static byte[] serializePublicKey(PublicKey publicKey) { + if (!(publicKey instanceof ECPublicKey)) { + throw new IllegalArgumentException("publicKey is not of type ECPublicKey"); + } + ECPoint point = ((ECPublicKey) publicKey).getW(); + byte[] result = new byte[POINT_LENGTH + 1]; + result[0] = 0x04; + fitBigInteger(point.getAffineX(), result, 1, FIELD_LENGTH); + fitBigInteger(point.getAffineY(), result, FIELD_LENGTH + 1, FIELD_LENGTH); + return result; + } + + /** + * The inverse of the serializePublicKey function above, takes the concatenation of two big-endian + * 32-bit integers and uses them as coordinates in a {@code ECPoint} inside the returned {@code + * PublicKey}. + */ + public static PublicKey deserializePublicKey(byte[] serializedPoint) throws GeneralSecurityException { + if (serializedPoint.length != 2 * FIELD_LENGTH + 1) { + throw new IllegalArgumentException( + "publicKey is the wrong size, expected " + + FIELD_LENGTH * 2 + + " bytes got " + + serializedPoint.length); + } + BigInteger x = new BigInteger(1, Arrays.copyOfRange(serializedPoint, 1, FIELD_LENGTH + 1)); + BigInteger y = new BigInteger(1, Arrays.copyOfRange(serializedPoint, FIELD_LENGTH + 1, 2 * FIELD_LENGTH + 1)); + + ECPoint publicPoint = new ECPoint(x, y); + if (!isPointOnCurve(publicPoint)) { + throw new GeneralSecurityException("point is not on the curve"); + } + ECPublicKeySpec publicSpec = new ECPublicKeySpec(publicPoint, CURVE_SPEC); + KeyFactory kf = KeyFactory.getInstance("EC"); + return kf.generatePublic(publicSpec); + } + + /** Returns a 32-byte big endian integer representing the p256 exponent of the private key. */ + static byte[] serializePrivateKey(PrivateKey privateKey) { + if (!(privateKey instanceof ECPrivateKey)) { + throw new IllegalArgumentException("publicKey is not of type ECPrivateKey"); + } + BigInteger s = ((ECPrivateKey) privateKey).getS(); + byte[] result = new byte[FIELD_LENGTH]; + fitBigInteger(s, result, 0, FIELD_LENGTH); + return result; + } + + /** + * Converts a 32-byte big-endian serialization of p256 exponent into an {@code PrivateKey} object. + */ + public static PrivateKey deserializePrivateKey(byte[] privateKey) + throws GeneralSecurityException { + if (privateKey.length != FIELD_LENGTH) { + throw new IllegalArgumentException( + "privateKey is the wrong size, expected " + + FIELD_LENGTH + + " bytes got " + + privateKey.length); + } + BigInteger s = new BigInteger(1 /* positive */, privateKey); + + ECPrivateKeySpec privateSpec = new ECPrivateKeySpec(s, CURVE_SPEC); + KeyFactory kf = KeyFactory.getInstance("EC"); + return kf.generatePrivate(privateSpec); + } + + /** + * Copies a BigInteger to a fixed-sized array as an unsigned big-endian integer, padding with + * leading zeros. The output array is assumed to contain |length| zero bytes starting at + * output[offset]. This is used as an alternative to BigInteger.toByteArray(), which will not + * return a fixed-sized array. In particular, BigInteger.toByteArray() would add an extra sign + * byte and drop leading zeros. + * + * @param bigInt The integer to fit. + * @param output The output buffer. + * @param offset The offset in output where the output starts. + * @param length The number of bytes we are allowed to write to output. + */ + private static void fitBigInteger(BigInteger bigInt, byte[] output, int offset, int length) { + byte[] array = bigInt.toByteArray(); + if (array.length > length + 1) { + throw new IllegalArgumentException("Array is too small to hold this BigInteger"); + } else if (array.length == length + 1) { + if (array[0] != 0x00) { + throw new IllegalArgumentException("Array is too small to hold this BigInteger"); + } + // Clip off extra sign bit + System.arraycopy(array, 1, output, offset, length); + } else { + // Preserve leading zeros + System.arraycopy(array, 0, output, offset + length - array.length, array.length); + } + } + + private P256() {} +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2CborConstants.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2CborConstants.java new file mode 100644 index 0000000..1572696 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2CborConstants.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2; + + +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; +import de.cotech.hw.fido2.internal.cbor_java.model.UnsignedInteger; + + +public class Ctap2CborConstants { + public static final UnsignedInteger CBOR_ONE = new UnsignedInteger(1); + public static final UnsignedInteger CBOR_TWO = new UnsignedInteger(2); + public static final UnsignedInteger CBOR_THREE = new UnsignedInteger(3); + public static final UnsignedInteger CBOR_FOUR = new UnsignedInteger(4); + public static final UnsignedInteger CBOR_FIVE = new UnsignedInteger(5); + + public static final UnicodeString CBOR_ID = new UnicodeString("id"); + public static final UnicodeString CBOR_NAME = new UnicodeString("name"); + public static final UnicodeString CBOR_DISPLAYNAME = new UnicodeString("displayName"); + public static final UnicodeString CBOR_ICON = new UnicodeString("icon"); +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2CborSerializer.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2CborSerializer.java new file mode 100644 index 0000000..b79e162 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2CborSerializer.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2; + + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.List; +import java.util.Map.Entry; + +import de.cotech.hw.fido2.internal.cbor_java.CborBuilder; +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.builder.ArrayBuilder; +import de.cotech.hw.fido2.internal.cbor_java.builder.MapBuilder; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cose.CoseIdentifiers.CoseAlg; +import de.cotech.hw.fido2.internal.ctap2.commands.clientPin.AuthenticatorClientPin; +import de.cotech.hw.fido2.internal.ctap2.commands.getAssertion.AuthenticatorGetAssertion; +import de.cotech.hw.fido2.internal.ctap2.commands.getInfo.AuthenticatorGetInfo; +import de.cotech.hw.fido2.internal.ctap2.commands.makeCredential.AuthenticatorMakeCredential; +import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; +import de.cotech.hw.fido2.domain.AuthenticatorTransport; +import de.cotech.hw.fido2.domain.PublicKeyCredentialParameters; +import de.cotech.hw.fido2.domain.PublicKeyCredentialType; +import de.cotech.hw.fido2.domain.PublicKeyCredentialEntity; +import de.cotech.hw.fido2.domain.PublicKeyCredentialRpEntity; +import de.cotech.hw.fido2.domain.PublicKeyCredentialUserEntity; +import de.cotech.hw.fido2.internal.ctap2.commands.makeCredential.AuthenticatorMakeCredential.AuthenticatorMakeCredentialOptions; +import de.cotech.hw.fido2.internal.ctap2.commands.rawCommand.RawCtap2Command; + + +class Ctap2CborSerializer { + + byte[] toCborBytes(Ctap2Command command) { + if (command instanceof RawCtap2Command) { + return ((RawCtap2Command) command).data(); + } + + CborBuilder cborBuilder = new CborBuilder(); + writeToBuilder(cborBuilder, command); + List cborData = cborBuilder.build(); + return writeCborDataToBytes(cborData); + } + + private byte[] writeCborDataToBytes(List cborData) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + new CborEncoder(outputStream).encode(cborData); + return outputStream.toByteArray(); + } catch (CborException e) { + e.printStackTrace(); + return null; + } + } + + private void writeToBuilder(CborBuilder cborBuilder, Ctap2Command command) { + if (command instanceof AuthenticatorMakeCredential) { + writeToBuilder(cborBuilder, (AuthenticatorMakeCredential) command); + } else if (command instanceof AuthenticatorGetAssertion) { + writeToBuilder(cborBuilder, (AuthenticatorGetAssertion) command); + } else if (command instanceof AuthenticatorGetInfo) { + // nothing to do, this command doesn't contain any parameters + } else if (command instanceof AuthenticatorClientPin) { + writeToBuilder(cborBuilder, (AuthenticatorClientPin) command); + } else { + throw new UnsupportedOperationException(); + } + } + + private void writeToBuilder(CborBuilder cborBuilder2, AuthenticatorMakeCredential amc) { + MapBuilder cborBuilder = cborBuilder2.addMap(); + + // clientDataHash 0x01 byte string (CBOR major type 2). + cborBuilder.put(0x01, amc.clientDataHash()); + + // rp 0x02 CBOR definite length map (CBOR major type 5). + MapBuilder rpMapBuilder = cborBuilder.putMap(0x02); + writeToMap(rpMapBuilder, amc.rp()); + + // user 0x03 CBOR definite length map (CBOR major type 5). + MapBuilder userMapBuilder = cborBuilder.putMap(0x03); + writeToMap(userMapBuilder, amc.user()); + + // pubKeyCredParams 0x04 CBOR definite length array (CBOR major type 4) of CBOR definite length maps (CBOR major type 5). + ArrayBuilder pubCredBuilder = cborBuilder.putArray(0x04); + for (PublicKeyCredentialParameters params : amc.pubKeyCredParams()) { + MapBuilder pubCredMapBuidler = pubCredBuilder.addMap(); + writeToMap(pubCredMapBuidler, params); + } + + // optional parameters + + // excludeList 0x05 CBOR definite length array (CBOR major type 4) of CBOR definite length maps (CBOR major type 5). + List excludeList = amc.excludeList(); + if (excludeList != null) { + ArrayBuilder arrayBuilder = cborBuilder.putArray(0x05); + for (PublicKeyCredentialDescriptor descriptor : excludeList) { + MapBuilder mapBuilder = arrayBuilder.addMap(); + writeToMap(mapBuilder, descriptor); + mapBuilder.end(); + } + arrayBuilder.end(); + } + // extensions 0x06 CBOR definite length map (CBOR major type 5). + + // options 0x07 CBOR definite length map (CBOR major type 5). + AuthenticatorMakeCredentialOptions options = amc.options(); + if (options != null) { + MapBuilder mapBuilder = cborBuilder.putMap(0x07); + Boolean rk = options.rk(); + if (rk != null) { + mapBuilder.put("rk", rk); + } + mapBuilder.end(); + /* not supported yet + Boolean uv = options.uv(); + if (uv != null) { + mapBuilder.put("uv", rk); + } + */ + } + + // pinAuth 0x08 byte string (CBOR major type 2). + if (amc.pinAuth() != null) { + cborBuilder.put(0x08, amc.pinAuth()); + } + + // pinProtocol 0x09 PIN protocol version chosen by the client. For this version of the spec, this SHALL be the number 1. + Integer pinProtocol = amc.pinProtocol(); + if (pinProtocol != null) { + cborBuilder.put(0x09, pinProtocol); + } + } + + private void writeToBuilder(CborBuilder cborBuilder2, AuthenticatorGetAssertion aga) { + MapBuilder cborBuilder = cborBuilder2.addMap(); + + // rpId 0x01 UTF-8 encoded text string (CBOR major type 3). + cborBuilder.put(0x01, aga.rpId()); + // clientDataHash 0x02 byte string (CBOR major type 2). + cborBuilder.put(0x02, aga.clientDataHash()); + + // optional parameters + + // allowList 0x03 CBOR definite length array (CBOR major type 4) of CBOR definite length maps (CBOR major type 5). + List publicKeyCredentialDescriptors = aga.allowList(); + if (publicKeyCredentialDescriptors != null && !publicKeyCredentialDescriptors.isEmpty()) { + ArrayBuilder pubCredBuilder = cborBuilder.putArray(0x03); + for (PublicKeyCredentialDescriptor params : publicKeyCredentialDescriptors) { + MapBuilder pubCredMapBuidler = pubCredBuilder.addMap(); + writeToMap(pubCredMapBuidler, params); + } + } + // extensions 0x04 CBOR definite length map (CBOR major type 5). + // options 0x05 CBOR definite length map (CBOR major type 5). + + // pinAuth 0x06 byte string (CBOR major type 2). + if (aga.pinAuth() != null) { + cborBuilder.put(0x06, aga.pinAuth()); + } + + // pinProtocol 0x07 PIN protocol version chosen by the client. For this version of the spec, this SHALL be the number 1. + Integer pinProtocol = aga.pinProtocol(); + if (pinProtocol != null) { + cborBuilder.put(0x07, pinProtocol); + } + } + + private void writeToBuilder(CborBuilder cborBuilder2, AuthenticatorClientPin acp) { + MapBuilder cborBuilder = cborBuilder2.addMap(); + + // pinProtocol (0x01) Unsigned Integer Required PIN protocol version chosen by the client. For this version of the spec, this SHALL be the number 1. + cborBuilder.put(0x01, acp.pinProtocol()); + // subCommand (0x02) Unsigned Integer Required The authenticator Client PIN sub command currently being requested + cborBuilder.put(0x02, acp.subCommand()); + + // optional parameters + + // keyAgreement (0x03) COSE_Key Optional Public key of platformKeyAgreementKey. The COSE_Key-encoded public key MUST contain the optional "alg" parameter and MUST NOT contain any other optional parameters. The "alg" parameter MUST contain a COSEAlgorithmIdentifier value. + if (acp.keyAgreement() != null) { + try { + cborBuilder.put(Ctap2CborConstants.CBOR_THREE, new CborDecoder(new ByteArrayInputStream(acp.keyAgreement())).decodeNext()); + } catch (CborException e) { + throw new IllegalArgumentException(e); + } + } + + // pinAuth (0x04) Byte Array Optional First 16 bytes of HMAC-SHA-256 of encrypted contents using sharedSecret. See Setting a new PIN, Changing existing PIN and Getting pinToken from the authenticator for more details. + if (acp.pinAuth() != null) { + cborBuilder.put(0x04, acp.pinAuth()); + } + + // newPinEnc (0x05) Byte Array Optional Encrypted new PIN using sharedSecret. Encryption is done over UTF-8 representation of new PIN. + if (acp.newPinEnc() != null) { + cborBuilder.put(0x05, acp.newPinEnc()); + } + + // pinHashEnc (0x06) Byte Array Optional Encrypted first 16 bytes of SHA-256 of PIN using sharedSecret. + if (acp.pinHashEnc() != null) { + cborBuilder.put(0x06, acp.pinHashEnc()); + } + } + + private void writeToMap(MapBuilder mapBuilder, PublicKeyCredentialRpEntity rpEntity) { + writeToMap(mapBuilder, (PublicKeyCredentialEntity) rpEntity); + if (rpEntity.id() != null) { + mapBuilder.put("id", rpEntity.id()); + } + } + + private void writeToMap(MapBuilder mapBuilder, PublicKeyCredentialUserEntity userEntity) { + writeToMap(mapBuilder, (PublicKeyCredentialEntity) userEntity); + mapBuilder.put("id", userEntity.id()); + if (userEntity.displayName() != null) { + mapBuilder.put("displayName", userEntity.displayName()); + } + } + + private void writeToMap(MapBuilder mapBuilder, PublicKeyCredentialParameters publicKeyCredentialParameters) { + for (Entry entry : publicKeyCredentialParameters.parameters().entrySet()) { + mapBuilder.put("type", entry.getKey().type); + mapBuilder.put("alg", entry.getValue().label); + } + } + + private void writeToMap(MapBuilder mapBuilder, PublicKeyCredentialEntity credentialEntity) { + mapBuilder.put("name", credentialEntity.name()); + if (credentialEntity.icon() != null) { + mapBuilder.put("icon", credentialEntity.icon()); + } + } + + private void writeToMap(MapBuilder mapBuilder, PublicKeyCredentialDescriptor descriptor) { + mapBuilder.put("type", descriptor.type().type); + mapBuilder.put("id", descriptor.id()); + List transports = descriptor.transports(); + if (transports != null) { + ArrayBuilder arrayBuilder = mapBuilder.putArray("transports"); + for (AuthenticatorTransport transport : transports) { + arrayBuilder.add(transport.transport); + } + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2Command.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2Command.java new file mode 100644 index 0000000..435349f --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2Command.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2; + + +public abstract class Ctap2Command { + public static final byte COMMAND_MAKE_CREDENTIAL = 0x01; + public static final byte COMMAND_GET_ASSERTION = 0x02; + public static final byte COMMAND_GET_INFO = 0x04; + public static final byte COMMAND_CLIENT_PIN = 0x06; + public static final byte COMMAND_RESET = 0x07; + public static final byte COMMAND_GET_NEXT_ASSERTION = 0x08; + + public abstract byte commandValue(); + + public abstract Ctap2ResponseFactory getResponseFactory(); +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2CommandApduTransformer.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2CommandApduTransformer.java new file mode 100644 index 0000000..cf50a69 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2CommandApduTransformer.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2; + + +import de.cotech.hw.internal.iso7816.CommandApdu; +import de.cotech.hw.util.Arrays; + + +public class Ctap2CommandApduTransformer { + private static final int FIDO2_CLA_PROPRIETARY = 0x80; + private static final int FIDO2_INS = 0x10; + private static final int FIDO2_P1 = 0x00; + private static final int FIDO2_P2 = 0x00; + + private final Ctap2CborSerializer ctap2CborSerializer = new Ctap2CborSerializer(); + + public CommandApdu toCommandApdu(Ctap2Command command) { + byte[] commandBytes = transformCommandToBytes(command); + return transformCommandBytesToCommandApdu(commandBytes); + } + + private CommandApdu transformCommandBytesToCommandApdu(byte[] commandBytes) { + return CommandApdu.create(FIDO2_CLA_PROPRIETARY, FIDO2_INS, FIDO2_P1, FIDO2_P2, commandBytes); + } + + private byte[] transformCommandToBytes(Ctap2Command command) { + byte[] cborBytes = ctap2CborSerializer.toCborBytes(command); + return Arrays.prepend(cborBytes, command.commandValue()); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2Exception.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2Exception.java new file mode 100644 index 0000000..74f3b64 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2Exception.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2; + + +import java.io.IOException; + + +public class Ctap2Exception extends IOException { + public final CtapErrorResponse ctapErrorResponse; + + public Ctap2Exception(CtapErrorResponse ctapErrorResponse) { + super(ctapErrorResponse.errorText()); + this.ctapErrorResponse = ctapErrorResponse; + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2Response.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2Response.java new file mode 100644 index 0000000..368de66 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2Response.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2; + + +public abstract class Ctap2Response { +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2ResponseFactory.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2ResponseFactory.java new file mode 100644 index 0000000..d8e55df --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2ResponseFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2; + + +import java.io.IOException; + +import de.cotech.hw.fido2.internal.ctap2.Ctap2Response; + + +public interface Ctap2ResponseFactory { + T createResponse(byte[] responseData) throws IOException; +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/CtapErrorResponse.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/CtapErrorResponse.java new file mode 100644 index 0000000..2558641 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/CtapErrorResponse.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2; + + +import com.google.auto.value.AutoValue; + + +@SuppressWarnings("unused") +@AutoValue +public abstract class CtapErrorResponse extends Ctap2Response { + public static final byte CTAP2_OK = 0x00; // Indicates successful response. + public static final byte CTAP1_ERR_SUCCESS = 0x00; // Same as CTAP2_OK + public static final byte CTAP1_ERR_INVALID_COMMAND = 0x01; // The command is not a valid CTAP command. + public static final byte CTAP1_ERR_INVALID_PARAMETER = 0x02; // The command included an invalid parameter. + public static final byte CTAP1_ERR_INVALID_LENGTH = 0x03; // Invalid message or item length. + public static final byte CTAP1_ERR_INVALID_SEQ = 0x04; // Invalid message sequencing. + public static final byte CTAP1_ERR_TIMEOUT = 0x05; // Message timed out. + public static final byte CTAP1_ERR_CHANNEL_BUSY = 0x06; // Channel busy. + public static final byte CTAP1_ERR_LOCK_REQUIRED = 0x0A; // Command requires channel lock. + public static final byte CTAP1_ERR_INVALID_CHANNEL = 0x0B; // Command not allowed on this cid. + public static final byte CTAP2_ERR_CBOR_UNEXPECTED_TYPE = 0x11; // Invalid/unexpected CBOR error. + public static final byte CTAP2_ERR_INVALID_CBOR = 0x12; // Error when parsing CBOR. + public static final byte CTAP2_ERR_MISSING_PARAMETER = 0x14; // Missing non-optional parameter. + public static final byte CTAP2_ERR_LIMIT_EXCEEDED = 0x15; // Limit for number of items exceeded. + public static final byte CTAP2_ERR_UNSUPPORTED_EXTENSION = 0x16; // Unsupported extension. + public static final byte CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19; // Valid credential found in the exclude list. + public static final byte CTAP2_ERR_PROCESSING = 0x21; // Processing (Lengthy operation is in progress). + public static final byte CTAP2_ERR_INVALID_CREDENTIAL = 0x22; // Credential not valid for the authenticator. + public static final byte CTAP2_ERR_USER_ACTION_PENDING = 0x23; // Authentication is waiting for user interaction. + public static final byte CTAP2_ERR_OPERATION_PENDING = 0x24; // Processing, lengthy operation is in progress. + public static final byte CTAP2_ERR_NO_OPERATIONS = 0x25; // No request is pending. + public static final byte CTAP2_ERR_UNSUPPORTED_ALGORITHM = 0x26; // Authenticator does not support requested algorithm. + public static final byte CTAP2_ERR_OPERATION_DENIED = 0x27; // Not authorized for requested operation. + public static final byte CTAP2_ERR_KEY_STORE_FULL = 0x28; // Internal key storage is full. + public static final byte CTAP2_ERR_NOT_BUSY = 0x29; // Authenticator cannot cancel as it is not busy. + public static final byte CTAP2_ERR_NO_OPERATION_PENDING = 0x2A; // No outstanding operations. + public static final byte CTAP2_ERR_UNSUPPORTED_OPTION = 0x2B; // Unsupported option. + public static final byte CTAP2_ERR_INVALID_OPTION = 0x2C; // Not a valid option for current operation. + public static final byte CTAP2_ERR_KEEPALIVE_CANCEL = 0x2D; // Pending keep alive was cancelled. + public static final byte CTAP2_ERR_NO_CREDENTIALS = 0x2E; // No valid credentials provided. + public static final byte CTAP2_ERR_USER_ACTION_TIMEOUT = 0x2F; // Timeout waiting for user interaction. + public static final byte CTAP2_ERR_NOT_ALLOWED = 0x30; // Continuation command, such as, authenticatorGetNextAssertion not allowed. + public static final byte CTAP2_ERR_PIN_INVALID = 0x31; // PIN Invalid. + public static final byte CTAP2_ERR_PIN_BLOCKED = 0x32; // PIN Blocked. + public static final byte CTAP2_ERR_PIN_AUTH_INVALID = 0x33; // PIN authentication,pinAuth, verification failed. + public static final byte CTAP2_ERR_PIN_AUTH_BLOCKED = 0x34; // PIN authentication,pinAuth, blocked. Requires power recycle to reset. + public static final byte CTAP2_ERR_PIN_NOT_SET = 0x35; // No PIN has been set. + public static final byte CTAP2_ERR_PIN_REQUIRED = 0x36; // PIN is required for the selected operation. + public static final byte CTAP2_ERR_PIN_POLICY_VIOLATION = 0x37; // PIN policy violation. Currently only enforces minimum length. + public static final byte CTAP2_ERR_PIN_TOKEN_EXPIRED = 0x38; // pinToken expired on authenticator. + public static final byte CTAP2_ERR_REQUEST_TOO_LARGE = 0x39; // Authenticator cannot handle this request due to memory constraints. + public static final byte CTAP2_ERR_ACTION_TIMEOUT = 0x3A; // The current operation has timed out. + public static final byte CTAP2_ERR_UP_REQUIRED = 0x3B; // User presence is required for the requested operation. + public static final byte CTAP1_ERR_OTHER = 0x7F; // Other unspecified error. + public static final byte CTAP2_ERR_SPEC_LAST = (byte) 0xDF; // CTAP 2 spec last error. + + private static final byte CTAP2_ERR_EXTENSION_FIRST = (byte) 0xE0; // Extension specific error. + private static final byte CTAP2_ERR_EXTENSION_LAST = (byte) 0xEF; // Extension specific error. + private static final byte CTAP2_ERR_VENDOR_FIRST = (byte) 0xF0; // Vendor specific error. + private static final byte CTAP2_ERR_VENDOR_LAST = (byte) 0xFF; // Vendor specific error. + + public static CtapErrorResponse create(byte errorCode) { + return new AutoValue_CtapErrorResponse(errorCode); + } + + public abstract byte errorCode(); + + String errorText() { + return errorDescription() + " (code 0x" + Integer.toHexString(errorCode()) + ")"; + } + + private String errorDescription() { + if (errorCode() >= CTAP2_ERR_EXTENSION_FIRST && errorCode() <= CTAP2_ERR_EXTENSION_LAST) { + return "CTAP extension error"; + } + if (errorCode() >= CTAP2_ERR_VENDOR_FIRST && errorCode() <= CTAP2_ERR_VENDOR_LAST) { + return "CTAP vendor error"; + } + switch (errorCode()) { + case CTAP1_ERR_INVALID_COMMAND: + return "The command is not a valid CTAP command."; + case CTAP1_ERR_INVALID_PARAMETER: + return "The command included an invalid parameter."; + case CTAP1_ERR_INVALID_LENGTH: + return "Invalid message or item length."; + case CTAP1_ERR_INVALID_SEQ: + return "Invalid message sequencing."; + case CTAP1_ERR_TIMEOUT: + return "Message timed out."; + case CTAP1_ERR_CHANNEL_BUSY: + return "Channel busy."; + case CTAP1_ERR_LOCK_REQUIRED: + return "Command requires channel lock."; + case CTAP1_ERR_INVALID_CHANNEL: + return "Command not allowed on this cid."; + case CTAP2_ERR_CBOR_UNEXPECTED_TYPE: + return "Invalid/unexpected CBOR error."; + case CTAP2_ERR_INVALID_CBOR: + return "Error when parsing CBOR."; + case CTAP2_ERR_MISSING_PARAMETER: + return "Missing non-optional parameter."; + case CTAP2_ERR_LIMIT_EXCEEDED: + return "Limit for number of items exceeded."; + case CTAP2_ERR_UNSUPPORTED_EXTENSION: + return "Unsupported extension."; + case CTAP2_ERR_CREDENTIAL_EXCLUDED: + return "Valid credential found in the exclude list."; + case CTAP2_ERR_PROCESSING: + return "Processing (Lengthy operation is in progress)."; + case CTAP2_ERR_INVALID_CREDENTIAL: + return "Credential not valid for the authenticator."; + case CTAP2_ERR_USER_ACTION_PENDING: + return "Authentication is waiting for user interaction."; + case CTAP2_ERR_OPERATION_PENDING: + return "Processing, lengthy operation is in progress."; + case CTAP2_ERR_NO_OPERATIONS: + return "No request is pending."; + case CTAP2_ERR_UNSUPPORTED_ALGORITHM: + return "Authenticator does not support requested algorithm."; + case CTAP2_ERR_OPERATION_DENIED: + return "Not authorized for requested operation."; + case CTAP2_ERR_KEY_STORE_FULL: + return "Internal key storage is full."; + case CTAP2_ERR_NOT_BUSY: + return "Authenticator cannot cancel as it is not busy."; + case CTAP2_ERR_NO_OPERATION_PENDING: + return "No outstanding operations."; + case CTAP2_ERR_UNSUPPORTED_OPTION: + return "Unsupported option."; + case CTAP2_ERR_INVALID_OPTION: + return "Not a valid option for current operation."; + case CTAP2_ERR_KEEPALIVE_CANCEL: + return "Pending keep alive was cancelled."; + case CTAP2_ERR_NO_CREDENTIALS: + return "No valid credentials provided."; + case CTAP2_ERR_USER_ACTION_TIMEOUT: + return "Timeout waiting for user interaction."; + case CTAP2_ERR_NOT_ALLOWED: + return "Continuation command, such as, authenticatorGetNextAssertion not allowed."; + case CTAP2_ERR_PIN_INVALID: + return "PIN Invalid."; + case CTAP2_ERR_PIN_BLOCKED: + return "PIN Blocked."; + case CTAP2_ERR_PIN_AUTH_INVALID: + return "PIN authentication failed."; + case CTAP2_ERR_PIN_AUTH_BLOCKED: + return "PIN authentication blocked. Requires power recycle to reset."; + case CTAP2_ERR_PIN_NOT_SET: + return "No PIN has been set."; + case CTAP2_ERR_PIN_REQUIRED: + return "PIN is required for the selected operation."; + case CTAP2_ERR_PIN_POLICY_VIOLATION: + return "PIN policy violation. Currently only enforces minimum length."; + case CTAP2_ERR_PIN_TOKEN_EXPIRED: + return "pinToken expired on authenticator."; + case CTAP2_ERR_REQUEST_TOO_LARGE: + return "Authenticator cannot handle this request due to memory constraints."; + case CTAP2_ERR_ACTION_TIMEOUT: + return "The current operation has timed out."; + case CTAP2_ERR_UP_REQUIRED: + return "User presence is required for the requested operation."; + case CTAP1_ERR_OTHER: + return "Other unspecified error."; + default: + return "Unknown CTAP error"; + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/clientPin/AuthenticatorClientPin.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/clientPin/AuthenticatorClientPin.java new file mode 100644 index 0000000..483f74c --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/clientPin/AuthenticatorClientPin.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.clientPin; + + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Command; +import de.cotech.hw.fido2.internal.ctap2.Ctap2ResponseFactory; + + +@AutoValue +public abstract class AuthenticatorClientPin extends Ctap2Command { + private static final byte PIN_PROTOCOL_V1 = 1; + private static final byte SUBCOMMAND_GET_RETRIES = 1; + private static final byte SUBCOMMAND_GET_KEY_AGREEMENT = 2; + private static final byte SUBCOMMAND_SET_PIN = 3; + private static final byte SUBCOMMAND_CHANGE_PIN = 4; + private static final byte SUBCOMMAND_GET_PIN_TOKEN = 5; + + // pinProtocol (0x01) Unsigned Integer Required PIN protocol version chosen by the client. For this version of the spec, this SHALL be the number 1. + public abstract byte pinProtocol(); + // subCommand (0x02) Unsigned Integer Required The authenticator Client PIN sub command currently being requested + public abstract byte subCommand(); + // keyAgreement (0x03) COSE_Key Optional Public key of platformKeyAgreementKey. The COSE_Key-encoded public key MUST contain the optional "alg" parameter and MUST NOT contain any other optional parameters. The "alg" parameter MUST contain a COSEAlgorithmIdentifier value. + @Nullable + @SuppressWarnings("mutable") + public abstract byte[] keyAgreement(); + // pinAuth (0x04) Byte Array Optional First 16 bytes of HMAC-SHA-256 of encrypted contents using sharedSecret. See Setting a new PIN, Changing existing PIN and Getting pinToken from the authenticator for more details. + @Nullable + @SuppressWarnings("mutable") + public abstract byte[] pinAuth(); + // newPinEnc (0x05) Byte Array Optional Encrypted new PIN using sharedSecret. Encryption is done over UTF-8 representation of new PIN. + @Nullable + @SuppressWarnings("mutable") + public abstract byte[] newPinEnc(); + // pinHashEnc (0x06) Byte Array Optional Encrypted first 16 bytes of SHA-256 of PIN using sharedSecret. + @Nullable + @SuppressWarnings("mutable") + public abstract byte[] pinHashEnc(); + + public static AuthenticatorClientPin createGetRetries() { + return new AutoValue_AuthenticatorClientPin(COMMAND_CLIENT_PIN, PIN_PROTOCOL_V1, + SUBCOMMAND_GET_RETRIES, null, null, null, null); + } + + public static AuthenticatorClientPin createGetKeyAgreement() { + return new AutoValue_AuthenticatorClientPin(COMMAND_CLIENT_PIN, PIN_PROTOCOL_V1, + SUBCOMMAND_GET_KEY_AGREEMENT, null, null, null, null); + } + + public static AuthenticatorClientPin createGetPinToken(byte[] keyAgreementPlatformPublicKey, + byte[] pinHashEnc) { + return new AutoValue_AuthenticatorClientPin(COMMAND_CLIENT_PIN, PIN_PROTOCOL_V1, + SUBCOMMAND_GET_PIN_TOKEN, keyAgreementPlatformPublicKey, null, null, pinHashEnc); + } + + @Override + public Ctap2ResponseFactory getResponseFactory() { + return new AuthenticatorClientPinResponseFactory(); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/clientPin/AuthenticatorClientPinResponse.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/clientPin/AuthenticatorClientPinResponse.java new file mode 100644 index 0000000..d80e5ad --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/clientPin/AuthenticatorClientPinResponse.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.clientPin; + + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Response; + + +@AutoValue +public abstract class AuthenticatorClientPinResponse extends Ctap2Response { + // KeyAgreement (0x01) COSE_Key Optional Authenticator key agreement public key in COSE_Key format. This will be used to establish a sharedSecret between platform and the authenticator. The COSE_Key-encoded public key MUST contain the optional "alg" parameter and MUST NOT contain any other optional parameters. The "alg" parameter MUST contain a COSEAlgorithmIdentifier value. + @Nullable + public abstract byte[] keyAgreement(); + // pinToken (0x02) Byte Array Optional Encrypted pinToken using sharedSecret to be used in subsequent authenticatorMakeCredential and authenticatorGetAssertion operations. + @Nullable + public abstract byte[] pinToken(); + // retries (0x03) Unsigned Integer Optional Number of PIN attempts remaining before lockout. This is optionally used to show in UI when collecting the PIN in Setting a new PIN, Changing existing PIN and Getting pinToken from the authenticator flows. + @Nullable + public abstract Integer retries(); + + public static AuthenticatorClientPinResponse create( + @Nullable byte[] keyAgreement, + @Nullable byte[] pinToken, + @Nullable Integer retries + ) { + return new AutoValue_AuthenticatorClientPinResponse(keyAgreement, pinToken, retries); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/clientPin/AuthenticatorClientPinResponseFactory.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/clientPin/AuthenticatorClientPinResponseFactory.java new file mode 100644 index 0000000..964a9d9 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/clientPin/AuthenticatorClientPinResponseFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.clientPin; + + +import java.io.IOException; +import java.util.List; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; +import de.cotech.hw.fido2.internal.cbor_java.model.UnsignedInteger; +import de.cotech.hw.fido2.internal.cbor.CborUtils; +import de.cotech.hw.fido2.internal.ctap2.Ctap2CborConstants; +import de.cotech.hw.fido2.internal.ctap2.Ctap2ResponseFactory; + + +public class AuthenticatorClientPinResponseFactory implements + Ctap2ResponseFactory { + @Override + public AuthenticatorClientPinResponse createResponse(byte[] rawResponseData) + throws IOException { + try { + List dataItems = CborDecoder.decode(rawResponseData); + Map map = (Map) dataItems.get(0); + + Map keyAgreementCbor = (Map) map.get(Ctap2CborConstants.CBOR_ONE); + byte[] keyAgreement = CborUtils.writeCborDataToBytes(keyAgreementCbor); + ByteString pinToken = (ByteString) map.get(Ctap2CborConstants.CBOR_TWO); + UnsignedInteger retries = (UnsignedInteger) map.get(Ctap2CborConstants.CBOR_THREE); + + return AuthenticatorClientPinResponse.create( + keyAgreement, + pinToken != null ? pinToken.getBytes() : null, + retries != null ? retries.getValue().intValue() : null + ); + } catch (CborException e) { + throw new IOException(e); + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertion.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertion.java new file mode 100644 index 0000000..fabef47 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertion.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.getAssertion; + + +import java.util.List; + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Command; +import de.cotech.hw.fido2.internal.ctap2.Ctap2ResponseFactory; +import de.cotech.hw.fido2.internal.ctap2.commands.getInfo.AuthenticatorOptions; + + +@AutoValue +public abstract class AuthenticatorGetAssertion extends Ctap2Command { + // rpId 0x01 UTF-8 encoded text string (CBOR major type 3). + public abstract String rpId(); + // clientDataHash 0x02 byte string (CBOR major type 2). + @SuppressWarnings("mutable") + public abstract byte[] clientDataHash(); + // Out of spec: The clientDataJson object associated with this request + public abstract String clientDataJson(); + // allowList 0x03 CBOR definite length array (CBOR major type 4) of CBOR definite length maps (CBOR major type 5). + @Nullable + public abstract List allowList(); + // extensions 0x04 CBOR definite length map (CBOR major type 5). + @Nullable + @SuppressWarnings("mutable") + abstract byte[] extensions(); + // options 0x05 CBOR definite length map (CBOR major type 5). + @Nullable + abstract AuthenticatorOptions options(); + // pinAuth 0x06 byte string (CBOR major type 2). + @Nullable + @SuppressWarnings("mutable") + public abstract byte[] pinAuth(); + // pinProtocol 0x07 PIN protocol version chosen by the client. For this version of the spec, this SHALL be the number 1. + @Nullable + public abstract Integer pinProtocol(); + + public static AuthenticatorGetAssertion create(String rpId, byte[] clientDataHash, String clientDataJson, List allowCredentials, AuthenticatorOptions options) { + return new AutoValue_AuthenticatorGetAssertion(COMMAND_GET_ASSERTION, rpId, clientDataHash, clientDataJson, allowCredentials, null, options, null, null); + } + + public static AuthenticatorGetAssertion create(String rpId, byte[] clientDataHash, String clientDataJson, List allowCredentials, AuthenticatorOptions options, byte[] pinAuth, Integer pinProtocol) { + return new AutoValue_AuthenticatorGetAssertion(COMMAND_GET_ASSERTION, rpId, clientDataHash, clientDataJson, allowCredentials, null, options, pinAuth, pinProtocol); + } + + @Override + public Ctap2ResponseFactory getResponseFactory() { + return new AuthenticatorGetAssertionResponseFactory(this); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponse.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponse.java new file mode 100644 index 0000000..c2de4cc --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponse.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.getAssertion; + + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.domain.PublicKeyCredentialUserEntity; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Response; + + +@AutoValue +public abstract class AuthenticatorGetAssertionResponse extends Ctap2Response { + // credential 0x01 definite length map (CBOR major type 5). + @Nullable + public abstract byte[] credential(); + // authData 0x02 byte string (CBOR major type 2). + public abstract byte[] authData(); + // signature 0x03 byte string (CBOR major type 2). + public abstract byte[] signature(); + // publicKeyCredentialUserEntity 0x04 definite length map (CBOR major type 5). + @Nullable + public abstract PublicKeyCredentialUserEntity user(); + // numberOfCredentials 0x05 unsigned integer(CBOR major type 0). + @Nullable + public abstract Integer numberOfCredentials(); + + public abstract byte[] clientDataJSON(); + + + public static AuthenticatorGetAssertionResponse create( + @Nullable byte[] credential, + byte[] authData, + byte[] signature, + @Nullable PublicKeyCredentialUserEntity user, + @Nullable Integer numberOfCredentials, + byte[] clientDataJSON + ) { + return new AutoValue_AuthenticatorGetAssertionResponse(credential, authData, signature, user, numberOfCredentials, clientDataJSON); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponseFactory.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponseFactory.java new file mode 100644 index 0000000..02a7fb7 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponseFactory.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.getAssertion; + + +import java.io.IOException; +import java.util.List; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; +import de.cotech.hw.fido2.internal.cbor_java.model.UnsignedInteger; +import de.cotech.hw.fido2.domain.PublicKeyCredentialUserEntity; +import de.cotech.hw.fido2.internal.cbor.CborUtils; +import de.cotech.hw.fido2.internal.ctap2.Ctap2CborConstants; +import de.cotech.hw.fido2.internal.ctap2.Ctap2ResponseFactory; + + +public class AuthenticatorGetAssertionResponseFactory implements + Ctap2ResponseFactory { + private final AuthenticatorGetAssertion authenticatorGetAssertion; + + AuthenticatorGetAssertionResponseFactory(AuthenticatorGetAssertion authenticatorGetAssertion) { + this.authenticatorGetAssertion = authenticatorGetAssertion; + } + + @Override + public AuthenticatorGetAssertionResponse createResponse(byte[] rawResponseData) + throws IOException { + try { + List dataItems = CborDecoder.decode(rawResponseData); + DataItem dataItem = dataItems.get(0); + return readAuthenticatorGetAssertionResponse(dataItem); + } catch (CborException | ClassCastException e) { + throw new IOException("Received incorrectly formatted AuthenticatorGetAssertionResponse", e); + } + } + + private AuthenticatorGetAssertionResponse readAuthenticatorGetAssertionResponse(DataItem dataItem) + throws CborException, IOException { + Map responseMap = (Map) dataItem; + + DataItem credential = responseMap.get(Ctap2CborConstants.CBOR_ONE); + ByteString authData = (ByteString) responseMap.get(Ctap2CborConstants.CBOR_TWO); + ByteString signature = (ByteString) responseMap.get(Ctap2CborConstants.CBOR_THREE); + DataItem user = responseMap.get(Ctap2CborConstants.CBOR_FOUR); + UnsignedInteger numberOfCredentials = + (UnsignedInteger) responseMap.get(Ctap2CborConstants.CBOR_FIVE); + + PublicKeyCredentialUserEntity publicKeyCredentialUserEntity = + readPublicKeyCredentialUserEntity(user); + + return AuthenticatorGetAssertionResponse.create( + credential != null ? CborUtils.writeCborDataToBytes(credential) : null, + authData.getBytes(), + signature.getBytes(), + publicKeyCredentialUserEntity, + numberOfCredentials != null ? numberOfCredentials.getValue().intValue() : null, + authenticatorGetAssertion.clientDataJson().getBytes() + ); + } + + private PublicKeyCredentialUserEntity readPublicKeyCredentialUserEntity(DataItem dataItem) + throws IOException { + if (dataItem == null) { + return null; + } + if (dataItem.getMajorType() != MajorType.MAP) { + throw new IOException("Expected user field to be of type Map, found " + + dataItem.getMajorType()); + } + + Map userMap = ((Map) dataItem); + ByteString id = (ByteString) userMap.get(Ctap2CborConstants.CBOR_ID); + UnicodeString name = (UnicodeString) userMap.get(Ctap2CborConstants.CBOR_NAME); + UnicodeString displayName = (UnicodeString) userMap.get(Ctap2CborConstants.CBOR_DISPLAYNAME); + UnicodeString icon = (UnicodeString) userMap.get(Ctap2CborConstants.CBOR_ICON); + + return PublicKeyCredentialUserEntity.create( + id.getBytes(), + name != null ? name.getString() : null, + displayName != null ? displayName.getString() : null, + icon != null ? icon.getString() : null + ); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getInfo/AuthenticatorGetInfo.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getInfo/AuthenticatorGetInfo.java new file mode 100644 index 0000000..7cd8958 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getInfo/AuthenticatorGetInfo.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.getInfo; + + +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Command; +import de.cotech.hw.fido2.internal.ctap2.Ctap2ResponseFactory; + + +@AutoValue +public abstract class AuthenticatorGetInfo extends Ctap2Command { + public static AuthenticatorGetInfo create() { + return new AutoValue_AuthenticatorGetInfo(COMMAND_GET_INFO); + } + + public Ctap2ResponseFactory getResponseFactory() { + return new AuthenticatorGetInfoResponseFactory(); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getInfo/AuthenticatorGetInfoResponse.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getInfo/AuthenticatorGetInfoResponse.java new file mode 100644 index 0000000..65c0c9c --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getInfo/AuthenticatorGetInfoResponse.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.getInfo; + + +import java.util.Collections; +import java.util.List; + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Response; + + +@AutoValue +public abstract class AuthenticatorGetInfoResponse extends Ctap2Response { + private static final int DEFAULT_MAX_MSG_SIZE = 1024; + + public abstract List versions(); + public abstract List extensions(); + @SuppressWarnings("mutable") + public abstract byte[] aaguid(); + public abstract AuthenticatorOptions options(); + public abstract int maxMsgSize(); + @Nullable + public abstract List pinProtocols(); + + public static AuthenticatorGetInfoResponse create(List versions, List extensions, byte[] aaguid, + AuthenticatorOptions options, Integer maxMsgSize, List pinProtocols) { + if (extensions == null) { + extensions = Collections.emptyList(); + } + if (maxMsgSize == null) { + maxMsgSize = DEFAULT_MAX_MSG_SIZE; + } + return new AutoValue_AuthenticatorGetInfoResponse(versions, extensions, aaguid, options, maxMsgSize, pinProtocols); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getInfo/AuthenticatorGetInfoResponseFactory.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getInfo/AuthenticatorGetInfoResponseFactory.java new file mode 100644 index 0000000..7366812 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getInfo/AuthenticatorGetInfoResponseFactory.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.getInfo; + + +import java.io.IOException; +import java.util.List; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.Array; +import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; +import de.cotech.hw.fido2.internal.cbor_java.model.SimpleValue; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; +import de.cotech.hw.fido2.internal.cbor_java.model.UnsignedInteger; +import de.cotech.hw.fido2.internal.cbor.CborUtils; +import de.cotech.hw.fido2.internal.ctap2.Ctap2ResponseFactory; + + +public class AuthenticatorGetInfoResponseFactory + implements Ctap2ResponseFactory { + @Override + public AuthenticatorGetInfoResponse createResponse(byte[] responseData) throws IOException { + try { + return fromAuthenticatorGetInfoBytes(responseData); + } catch (CborException e) { + throw new IOException(e); + } + } + + private AuthenticatorGetInfoResponse fromAuthenticatorGetInfoBytes(byte[] responseBytes) + throws CborException { + List dataItems = CborDecoder.decode(responseBytes); + Map map = (Map) dataItems.get(0); + + List versions = null; + List extensions = null; + byte[] aaguid = null; + AuthenticatorOptions options = null; + Integer maxMsgSize = null; + List pinProtocols = null; + + for (DataItem key : map.getKeys()) { + int type = ((UnsignedInteger) key).getValue().intValue(); + DataItem value = map.get(key); + switch (type) { + // versions + case 0x01: { + // definite length array (CBOR major type 4) of UTF-8 encoded strings (CBOR major type 3). + versions = CborUtils.cborArrayToStringArray((Array) value); + break; + } + + // extensions + case 0x02: { + // definite length array (CBOR major type 4) of UTF-8 encoded strings (CBOR major type 3). + extensions = CborUtils.cborArrayToStringArray((Array) value); + break; + } + + // aaguid + case 0x03: { + // byte string (CBOR major type 2). 16 bytes in length and encoded the same as MakeCredential AuthenticatorData, as specified in [WebAuthN]. + ByteString byteString = (ByteString) value; + aaguid = byteString.getBytes(); + break; + } + + // options + case 0x04: { + // Definite length map (CBOR major type 5) of key-value pairs where keys are UTF8 strings (CBOR major type 3) and values are booleans (CBOR simple value 21). + options = cborMapToAuthenticatorOptions((Map) value); + break; + } + + // maxMsgSize + case 0x05: { + // unsigned integer(CBOR major type 0). This is the maximum message size supported by the authenticator. + UnsignedInteger unsignedInteger = (UnsignedInteger) value; + maxMsgSize = unsignedInteger.getValue().intValue(); + break; + } + + // pinProtocols + case 0x06: { + // array of unsigned integers (CBOR major type). This is the list of pinProtocols supported by the authenticator. + pinProtocols = CborUtils.cborArrayToIntegerArray((Array) value); + break; + } + } + } + + if (options == null) { + options = AuthenticatorOptions.create(); + } + + return AuthenticatorGetInfoResponse.create(versions, extensions, aaguid, options, maxMsgSize, pinProtocols); + } + + private AuthenticatorOptions cborMapToAuthenticatorOptions(Map map) { + // defaults are handled in AuthenticatorOptions itself + Boolean plat = null; + Boolean rk = null; + Boolean clientPin = null; + Boolean up = null; + Boolean uv = null; + + for (DataItem key : map.getKeys()) { + UnicodeString stringKey = (UnicodeString) key; + boolean value = map.get(key) == SimpleValue.TRUE; + switch (stringKey.getString()) { + case "plat": { + plat = value; + break; + } + case "rk": { + rk = value; + break; + } + case "clientPin": { + clientPin = value; + break; + } + case "up": { + up = value; + break; + } + case "uv": { + uv = value; + break; + } + } + } + + return AuthenticatorOptions.create(plat, rk, clientPin, up, uv); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getInfo/AuthenticatorOptions.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getInfo/AuthenticatorOptions.java new file mode 100644 index 0000000..6f08aa4 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getInfo/AuthenticatorOptions.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.getInfo; + + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; + + +@AutoValue +public abstract class AuthenticatorOptions { + // plat platform device: Indicates that the device is attached to the client and therefore can’t be removed and used on another client. false + public abstract boolean plat(); + + // rk resident key: Indicates that the device is capable of storing keys on the device itself and therefore can satisfy the authenticatorGetAssertion request with allowList parameter not specified or empty. false + public abstract boolean rk(); + + // clientPin + // If present and set to true, it indicates that the device is capable of accepting a PIN from the client and PIN has been set. + // If present and set to false, it indicates that the device is capable of accepting a PIN from the client and PIN has not been set yet. + // If absent, it indicates that the device is not capable of accepting a PIN from the client. + @Nullable + public abstract Boolean clientPin(); + + // up user presence: Indicates that the device is capable of testing user presence. true + public abstract boolean up(); + + // uv user verification: Indicates that the device is capable of verifying the user within itself. For example, devices with UI, biometrics fall into this category. + // If present and set to true, it indicates that the device is capable of user verification within itself and has been configured. + // If present and set to false, it indicates that the device is capable of user verification within itself and has not been yet configured. For example, a biometric device that has not yet been configured will return this parameter set to false. + // If absent, it indicates that the device is not capable of user verification within itself. + // A device that can only do Client PIN will not return the "uv" parameter. + // If a device is capable of verifying the user within itself as well as able to do Client PIN, it will return both "uv" and the Client PIN option. + @Nullable + public abstract Boolean uv(); + + public static AuthenticatorOptions create() { + return create(null, null, null, null, null); + } + + public static AuthenticatorOptions create(Boolean plat, Boolean rk, Boolean clientPin, Boolean up, Boolean uv) { + if (plat == null) { + plat = false; + } + if (rk == null) { + rk = false; + } + if (up == null) { + up = true; + } + + return new AutoValue_AuthenticatorOptions(plat, rk, clientPin, up, uv); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getNextAssertion/AuthenticatorGetNextAssertion.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getNextAssertion/AuthenticatorGetNextAssertion.java new file mode 100644 index 0000000..c2bbd4d --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getNextAssertion/AuthenticatorGetNextAssertion.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.getNextAssertion; + + +class AuthenticatorGetNextAssertion { + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredential.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredential.java new file mode 100644 index 0000000..ba661be --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredential.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.makeCredential; + + +import java.util.List; + +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Command; +import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; +import de.cotech.hw.fido2.domain.PublicKeyCredentialParameters; +import de.cotech.hw.fido2.domain.PublicKeyCredentialRpEntity; +import de.cotech.hw.fido2.domain.PublicKeyCredentialUserEntity; +import de.cotech.hw.fido2.internal.ctap2.Ctap2ResponseFactory; + + +@AutoValue +public abstract class AuthenticatorMakeCredential extends Ctap2Command { + // Byte Array Required Hash of the ClientData contextual binding specified by host. See [WebAuthN]. + @SuppressWarnings("mutable") + public abstract byte[] clientDataHash(); + + // Out of spec: The clientDataJson object associated with this request + public abstract String clientDataJson(); + + // PublicKeyCredentialRpEntity Required This PublicKeyCredentialRpEntity data structure describes a Relying Party with which the new public key credential will be associated. It contains the Relying party identifier, (optionally) a human-friendly RP name, and (optionally) a URL referencing a RP icon image. The RP name is to be used by the authenticator when displaying the credential to the user for selection and usage authorization. + public abstract PublicKeyCredentialRpEntity rp(); + + // PublicKeyCredentialUserEntity Required This PublicKeyCredentialUserEntity data structure describes the user account to which the new public key credential will be associated at the RP. It contains an RP-specific user account identifier, (optionally) a user name, (optionally) a user display name, and (optionally) a URL referencing a user icon image (of a user avatar, for example). The authenticator associates the created public key credential with the account identifier, and MAY also associate any or all of the user name, user display name, and image data (pointed to by the URL, if any). + public abstract PublicKeyCredentialUserEntity user(); + + // CBOR Array Required A sequence of CBOR maps consisting of pairs of PublicKeyCredentialType (a string) and cryptographic algorithm (a positive or negative integer), where algorithm identifiers are values that SHOULD be registered in the IANA COSE Algorithms registry [IANA-COSE-ALGS-REG]. This sequence is ordered from most preferred (by the RP) to least preferred. + public abstract List pubKeyCredParams(); + + // Sequence of PublicKeyCredentialDescriptors Optional A sequence of PublicKeyCredentialDescriptor structures, as specified in [WebAuthN]. The authenticator returns an error if the authenticator already contains one of the credentials enumerated in this sequence. This allows RPs to limit the creation of multiple credentials for the same account on a single authenticator. + @Nullable + public abstract List excludeList(); + + // CBOR map of extension identifier → authenticator extension input values Optional Parameters to influence authenticator operation, as specified in [WebAuthN]. These parameters might be authenticator specific. + // public abstract int extensions(); + + // Map of authenticator options Optional Parameters to influence authenticator operation, as specified in in the table below. + @Nullable + public abstract AuthenticatorMakeCredentialOptions options(); + + // Byte Array Optional First 16 bytes of HMAC-SHA-256 of clientDataHash using pinToken which platform got from the authenticator: HMAC-SHA-256(pinToken, clientDataHash). + @Nullable + @SuppressWarnings("mutable") + public abstract byte[] pinAuth(); + + // Unsigned Integer Optional PIN protocol version chosen by the client + @Nullable + public abstract Integer pinProtocol(); + + @AutoValue + public static abstract class AuthenticatorMakeCredentialOptions { + @Nullable + public abstract Boolean rk(); + @Nullable + public abstract Boolean uv(); + + public static AuthenticatorMakeCredentialOptions create(Boolean rk, Boolean uv) { + return new AutoValue_AuthenticatorMakeCredential_AuthenticatorMakeCredentialOptions(rk, uv); + } + } + + @Override + public Ctap2ResponseFactory getResponseFactory() { + return new AuthenticatorMakeCredentialResponseFactory(this); + } + + public static AuthenticatorMakeCredential create( + byte[] clientDataHash, String clientDataJson, PublicKeyCredentialRpEntity rp, PublicKeyCredentialUserEntity user) { + return create(clientDataHash, clientDataJson, rp, user, PublicKeyCredentialParameters.createDefaultEs256List(), + null, null, null, null); + } + + public static AuthenticatorMakeCredential create(byte[] clientDataHash, String clientDataJson, + PublicKeyCredentialRpEntity rp, PublicKeyCredentialUserEntity user, List pubKeyCredParams) { + return create(clientDataHash, clientDataJson, rp, user, pubKeyCredParams, null, null, null, null); + } + + public static AuthenticatorMakeCredential create(byte[] clientDataHash, String clientDataJson, + PublicKeyCredentialRpEntity rp, PublicKeyCredentialUserEntity user, List pubKeyCredParams, + List excludeList) { + return create(clientDataHash, clientDataJson, rp, user, pubKeyCredParams, excludeList, null, null, null); + } + + public static AuthenticatorMakeCredential create(byte[] clientDataHash, String clientDataJson, + PublicKeyCredentialRpEntity rp, PublicKeyCredentialUserEntity user, List pubKeyCredParams, + List excludeList, + AuthenticatorMakeCredentialOptions options, byte[] pinAuth, Integer pinProtocol) { + return new AutoValue_AuthenticatorMakeCredential(Ctap2Command.COMMAND_MAKE_CREDENTIAL, clientDataHash, clientDataJson, rp, user, + pubKeyCredParams, excludeList, options, pinAuth, pinProtocol); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredentialResponse.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredentialResponse.java new file mode 100644 index 0000000..b98dec6 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredentialResponse.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.makeCredential; + + +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Response; + + +@AutoValue +public abstract class AuthenticatorMakeCredentialResponse extends Ctap2Response { + // fmt 0x01 text string (CBOR major type 3). + public abstract String fmt(); + // authData 0x02 byte string (CBOR major type 2). + public abstract byte[] authData(); + // attStmt 0x03 definite length map (CBOR major type 5). + public abstract byte[] attStmt(); + + public abstract byte[] clientDataJSON(); + + + public static AuthenticatorMakeCredentialResponse create(String fmt, byte[] authData, byte[] attStmt, byte[] clientDataJSON) { + return new AutoValue_AuthenticatorMakeCredentialResponse(fmt, authData, attStmt, clientDataJSON); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredentialResponseFactory.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredentialResponseFactory.java new file mode 100644 index 0000000..4c484d7 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredentialResponseFactory.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.makeCredential; + + +import java.io.IOException; +import java.util.List; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; +import de.cotech.hw.fido2.internal.cbor.CborUtils; +import de.cotech.hw.fido2.internal.ctap2.Ctap2CborConstants; +import de.cotech.hw.fido2.internal.ctap2.Ctap2ResponseFactory; + + +public class AuthenticatorMakeCredentialResponseFactory implements + Ctap2ResponseFactory { + private final AuthenticatorMakeCredential authenticatorMakeCredential; + + AuthenticatorMakeCredentialResponseFactory(AuthenticatorMakeCredential authenticatorMakeCredential) { + this.authenticatorMakeCredential = authenticatorMakeCredential; + } + + @Override + public AuthenticatorMakeCredentialResponse createResponse(byte[] rawResponseData) + throws IOException { + try { + List dataItems = CborDecoder.decode(rawResponseData); + Map map = (Map) dataItems.get(0); + + UnicodeString fmt = (UnicodeString) map.get(Ctap2CborConstants.CBOR_ONE); + ByteString authData = (ByteString) map.get(Ctap2CborConstants.CBOR_TWO); + DataItem attStmt = map.get(Ctap2CborConstants.CBOR_THREE); + + return AuthenticatorMakeCredentialResponse.create( + fmt.getString(), + authData.getBytes(), + CborUtils.writeCborDataToBytes(attStmt), + authenticatorMakeCredential.clientDataJson().getBytes() + ); + } catch (CborException e) { + throw new IOException(e); + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/rawCommand/RawCtap2Command.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/rawCommand/RawCtap2Command.java new file mode 100644 index 0000000..4dd0d87 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/rawCommand/RawCtap2Command.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.rawCommand; + + +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Command; +import de.cotech.hw.fido2.internal.ctap2.Ctap2ResponseFactory; + + +@AutoValue +public abstract class RawCtap2Command extends Ctap2Command { + public abstract byte[] data(); + + public Ctap2ResponseFactory getResponseFactory() { + return RawCtap2Response::create; + } + + public static RawCtap2Command create(byte command, byte[] data) { + return new AutoValue_RawCtap2Command(command, data); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/rawCommand/RawCtap2Response.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/rawCommand/RawCtap2Response.java new file mode 100644 index 0000000..8d5784b --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/rawCommand/RawCtap2Response.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.rawCommand; + + +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Response; + + +@AutoValue +public abstract class RawCtap2Response extends Ctap2Response { + public abstract byte[] data(); + + static RawCtap2Response create(byte[] data) { + return new AutoValue_RawCtap2Response(data); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/reset/AuthenticatorReset.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/reset/AuthenticatorReset.java new file mode 100644 index 0000000..531933b --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/reset/AuthenticatorReset.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.ctap2.commands.reset; + + +class AuthenticatorReset { + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/json/JsonCollectedClientDataSerializer.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/json/JsonCollectedClientDataSerializer.java new file mode 100644 index 0000000..126221e --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/json/JsonCollectedClientDataSerializer.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.json; + + +import de.cotech.hw.fido2.domain.CollectedClientData; +import de.cotech.hw.fido2.internal.utils.WebsafeBase64; +import org.json.JSONException; +import org.json.JSONObject; + + +public class JsonCollectedClientDataSerializer { + public String clientClientDataToJson(CollectedClientData clientData) { + try { + JSONObject result = new JSONObject(); + result.put("type", clientData.type()); + result.put("origin", clientData.origin()); + result.put("challenge", WebsafeBase64.encodeToString(clientData.challenge())); + result.put("hashAlgorithm", clientData.hashAlgorithm()); + return result.toString(); + } catch (JSONException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/json/JsonPublicKeyCredentialSerializer.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/json/JsonPublicKeyCredentialSerializer.java new file mode 100644 index 0000000..bf4aebe --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/json/JsonPublicKeyCredentialSerializer.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.json; + + +import de.cotech.hw.fido2.internal.utils.WebsafeBase64; +import de.cotech.hw.fido2.domain.create.AuthenticatorAttestationResponse; +import de.cotech.hw.fido2.domain.get.AuthenticatorAssertionResponse; +import de.cotech.hw.fido2.domain.AuthenticatorResponse; +import de.cotech.hw.fido2.PublicKeyCredential; +import org.json.JSONException; +import org.json.JSONObject; + + +public class JsonPublicKeyCredentialSerializer { + public String publicKeyCredentialToJsonString(PublicKeyCredential publicKeyCredential) { + return publicKeyCredentialToJson(publicKeyCredential).toString(); + } + + private JSONObject publicKeyCredentialToJson(PublicKeyCredential publicKeyCredential) { + try { + JSONObject result = new JSONObject(); + result.put("type", publicKeyCredential.type()); + result.put("id", publicKeyCredential.id()); + result.put("response", authenticatorResponseToJson(publicKeyCredential.response())); + return result; + } catch (JSONException e) { + throw new IllegalArgumentException(e); + } + } + + private JSONObject authenticatorResponseToJson(AuthenticatorResponse authenticatorResponse) { + try { + JSONObject result = new JSONObject(); + result.put("clientDataJsonB64", WebsafeBase64.encodeToString(authenticatorResponse.clientDataJson())); + if (authenticatorResponse instanceof AuthenticatorAttestationResponse) { + AuthenticatorAttestationResponse authenticatorAttestationResponse = + (AuthenticatorAttestationResponse) authenticatorResponse; + String attestationObjectB64 = WebsafeBase64.encodeToString( + authenticatorAttestationResponse.attestationObject()); + result.put("attestationObjectB64", attestationObjectB64); + } + if (authenticatorResponse instanceof AuthenticatorAssertionResponse) { + AuthenticatorAssertionResponse authenticatorAttestationResponse = + (AuthenticatorAssertionResponse) authenticatorResponse; + String authenticatorDataB64 = WebsafeBase64.encodeToString( + authenticatorAttestationResponse.authenticatorData()); + result.put("authenticatorDataB64", authenticatorDataB64); + String signatureB64 = WebsafeBase64.encodeToString( + authenticatorAttestationResponse.signature()); + result.put("signatureB64", signatureB64); + + byte[] userHandle = authenticatorAttestationResponse.userHandle(); + if (userHandle != null) { + String userHandleB64 = WebsafeBase64.encodeToString(userHandle); + result.put("userHandleB64", userHandleB64); + } + } + return result; + } catch (JSONException e) { + throw new IllegalArgumentException(e); + } + } +} \ No newline at end of file diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/json/JsonWebauthnOptionsParser.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/json/JsonWebauthnOptionsParser.java new file mode 100644 index 0000000..1f40009 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/json/JsonWebauthnOptionsParser.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.json; + + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import de.cotech.hw.fido2.domain.AuthenticatorTransport; +import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; +import de.cotech.hw.fido2.domain.PublicKeyCredentialParameters; +import de.cotech.hw.fido2.domain.PublicKeyCredentialRpEntity; +import de.cotech.hw.fido2.domain.PublicKeyCredentialType; +import de.cotech.hw.fido2.domain.PublicKeyCredentialUserEntity; +import de.cotech.hw.fido2.domain.UserVerificationRequirement; +import de.cotech.hw.fido2.domain.create.AttestationConveyancePreference; +import de.cotech.hw.fido2.domain.create.AuthenticatorAttachment; +import de.cotech.hw.fido2.domain.create.AuthenticatorSelectionCriteria; +import de.cotech.hw.fido2.domain.create.PublicKeyCredentialCreationOptions; +import de.cotech.hw.fido2.domain.get.PublicKeyCredentialRequestOptions; +import de.cotech.hw.fido2.internal.cose.CoseIdentifiers.CoseAlg; +import de.cotech.hw.util.HwTimber; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + + +public class JsonWebauthnOptionsParser { + public PublicKeyCredentialCreationOptions fromOptionsJsonMakeCredential(String jsonString) throws JSONException { + JSONObject jsonObject = new JSONObject(jsonString); + return fromJsonMakeCredential(jsonObject); + } + + private PublicKeyCredentialCreationOptions fromJsonMakeCredential(JSONObject jsonObject) throws JSONException { + JSONObject publicKeyObject = jsonObject.getJSONObject("publicKey"); + + PublicKeyCredentialRpEntity rp = jsonToRpEntity(publicKeyObject.getJSONObject("rp")); + PublicKeyCredentialUserEntity user = jsonToUserEntity(publicKeyObject.getJSONObject("user")); + byte[] challenge = jsonObjectOrArrayToByteArray(publicKeyObject.getJSONObject("challenge")); + List pubKeyCredParams = jsonToPubKeyCredParamsList(publicKeyObject.optJSONArray("pubKeyCredParams")); + Long timeout = !publicKeyObject.isNull("timeout") ? publicKeyObject.getLong("timeout") : null; + AttestationConveyancePreference attestationConveyancePreference = + AttestationConveyancePreference.fromString(publicKeyObject.optString("attestation", null)); + AuthenticatorSelectionCriteria authenticatorSelection = jsonToAuthenticatorSelectionCriteria( + publicKeyObject.optJSONObject("authenticatorSelection")); + + return PublicKeyCredentialCreationOptions.create(rp, user, challenge, pubKeyCredParams, timeout, + authenticatorSelection, null, attestationConveyancePreference); + } + + private List jsonToPubKeyCredParamsList( + JSONArray pubKeyCredParams) throws JSONException { + List result = new ArrayList<>(); + if (pubKeyCredParams == null) { + return result; + } + + for (int i = 0; i < pubKeyCredParams.length(); i++) { + JSONObject pubkeyCredParam = pubKeyCredParams.getJSONObject(i); + PublicKeyCredentialType type = PublicKeyCredentialType.fromString(pubkeyCredParam.getString("type")); + CoseAlg alg = CoseAlg.fromIdentifier(pubkeyCredParam.getInt("alg")); + if (alg == null) { + HwTimber.d("Skipping unknown COSE identifier: %s", pubkeyCredParam.getInt("alg")); + continue; + } + PublicKeyCredentialParameters parameters = + PublicKeyCredentialParameters.createSingle(type, alg); + result.add(parameters); + } + return result; + } + + private List jsonToPubKeyKeyCredDescriptorList(JSONArray allowCredentials) + throws JSONException { + List result = new ArrayList<>(); + if (allowCredentials == null) { + return null; + } + + for (int i = 0; i < allowCredentials.length(); i++) { + JSONObject pubkeyCredParam = allowCredentials.getJSONObject(i); + PublicKeyCredentialType type = PublicKeyCredentialType.fromString(pubkeyCredParam.getString("type")); + byte[] id = jsonObjectOrArrayToByteArray(pubkeyCredParam.get("id")); + List transports = jsonToAuthenticatorTransports(pubkeyCredParam.optJSONArray("transports")); + PublicKeyCredentialDescriptor parameters = + PublicKeyCredentialDescriptor.create(type, id, transports); + result.add(parameters); + } + return result; + } + + private List jsonToAuthenticatorTransports(JSONArray transports) { + if (transports == null) { + return null; + } + try { + return jsonToAuthenticatorTransportsOrThrow(transports); + } catch(JSONException e){ + return null; + } + } + + private List jsonToAuthenticatorTransportsOrThrow(JSONArray transports) + throws JSONException { + List result = new ArrayList<>(); + for (int i = 0; i < transports.length(); i++) { + String transportName = transports.getString(i); + AuthenticatorTransport transport = AuthenticatorTransport.fromString(transportName); + if (transport != null) { + result.add(transport); + } else { + HwTimber.e("Ignoring unknown transport value: %s", transportName); + } + } + return result; + } + + private AuthenticatorSelectionCriteria jsonToAuthenticatorSelectionCriteria( + JSONObject authenticatorSelection) { + if (authenticatorSelection == null) { + return null; + } + AuthenticatorAttachment authenticatorAttachment = AuthenticatorAttachment.fromString( + authenticatorSelection.optString("authenticatorAttachment", null)); + boolean requireResidentKey = authenticatorSelection.optBoolean("requireResidentKey", false); + UserVerificationRequirement userVerification = UserVerificationRequirement.fromString( + authenticatorSelection.optString("userVerification", null)); + + return AuthenticatorSelectionCriteria.create(authenticatorAttachment, requireResidentKey, userVerification); + } + + private PublicKeyCredentialUserEntity jsonToUserEntity(JSONObject jsonObject) throws JSONException { + byte[] id = jsonObjectOrArrayToByteArray(jsonObject.get("id")); + String name = jsonObject.optString("name", null); + String displayName = jsonObject.optString("displayName", null); + String icon = jsonObject.optString("icon", null); + + return PublicKeyCredentialUserEntity.create(id, name, displayName, icon); + } + + private PublicKeyCredentialRpEntity jsonToRpEntity(JSONObject jsonObject) { + String id = jsonObject.optString("id", null); + String name = jsonObject.optString("name", null); + String icon = jsonObject.optString("icon", null); + + return PublicKeyCredentialRpEntity.create(id, name, icon); + } + + private byte[] jsonObjectOrArrayToByteArray(Object object) throws JSONException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + if (object instanceof JSONObject) { + JSONObject jsonObject = (JSONObject) object; + for (int i = 0; i < jsonObject.length(); i++) { + String key = Integer.toString(i); + if (!jsonObject.has(key)) { + throw new JSONException("Missing key '" + key + "' for byte array in JSON object!"); + } + baos.write(jsonObject.getInt(key)); + } + } else if (object instanceof JSONArray) { + JSONArray jsonArray = (JSONArray) object; + for (int i = 0; i < jsonArray.length(); i++) { + baos.write(jsonArray.getInt(i)); + } + } + return baos.toByteArray(); + } + + public PublicKeyCredentialRequestOptions fromOptionsJsonGetAssertion(String jsonString) throws JSONException { + JSONObject jsonObject = new JSONObject(jsonString); + return fromOptionsJsonGetAssertion(jsonObject); + } + + private PublicKeyCredentialRequestOptions fromOptionsJsonGetAssertion(JSONObject jsonObject) + throws JSONException { + JSONObject publicKeyObject = jsonObject.getJSONObject("publicKey"); + + byte[] challenge = jsonObjectOrArrayToByteArray(publicKeyObject.getJSONObject("challenge")); + Long timeout = !publicKeyObject.isNull("timeout") ? publicKeyObject.getLong("timeout") : null; + String rpId = publicKeyObject.optString("rpId", null); + List allowCredentials = jsonToPubKeyKeyCredDescriptorList(publicKeyObject.optJSONArray("allowCredentials")); + UserVerificationRequirement userVerification = UserVerificationRequirement.fromString( + publicKeyObject.optString("userVerification", null)); + + return PublicKeyCredentialRequestOptions.create( + challenge, + timeout, + rpId, + allowCredentials, + userVerification + ); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/WebauthnSecurityKeyOperation.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/WebauthnSecurityKeyOperation.java new file mode 100644 index 0000000..6d4917c --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/WebauthnSecurityKeyOperation.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.operations; + + +import java.io.IOException; + +import de.cotech.hw.fido2.internal.Fido2AppletConnection; +import de.cotech.hw.fido2.internal.pinauth.PinToken; +import de.cotech.hw.fido2.internal.webauthn.WebauthnCommand; +import de.cotech.hw.fido2.internal.webauthn.WebauthnResponse; + + +public abstract class WebauthnSecurityKeyOperation { + public abstract WR performWebauthnSecurityKeyOperation( + Fido2AppletConnection fido2AppletConnection, WC webauthnCommand) + throws IOException; +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/WebauthnSecurityKeyOperationFactory.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/WebauthnSecurityKeyOperationFactory.java new file mode 100644 index 0000000..1ef3877 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/WebauthnSecurityKeyOperationFactory.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.operations; + + +import de.cotech.hw.fido2.PublicKeyCredentialCreate; +import de.cotech.hw.fido2.PublicKeyCredentialGet; +import de.cotech.hw.fido2.internal.cbor.CborPublicKeyCredentialDescriptorParser; +import de.cotech.hw.fido2.internal.json.JsonCollectedClientDataSerializer; +import de.cotech.hw.fido2.internal.operations.ctap1.AuthenticatorGetAssertionCtap1Operation; +import de.cotech.hw.fido2.internal.operations.ctap1.AuthenticatorMakeCredentialCtap1Operation; +import de.cotech.hw.fido2.internal.operations.ctap2.AuthenticatorGetAssertionOperation; +import de.cotech.hw.fido2.internal.operations.ctap2.AuthenticatorMakeCredentialOperation; +import de.cotech.hw.fido2.internal.pinauth.PinProtocolV1; +import de.cotech.hw.fido2.internal.webauthn.ConstructCredentialAlg; +import de.cotech.hw.fido2.internal.webauthn.WebauthnCommand; +import de.cotech.hw.fido2.internal.webauthn.WebauthnResponse; + + +public class WebauthnSecurityKeyOperationFactory { + private final PinProtocolV1 pinProtocolV1; + private static final CborPublicKeyCredentialDescriptorParser + CBOR_PUBLIC_KEY_CREDENTIAL_DESCRIPTOR_PARSER = new CborPublicKeyCredentialDescriptorParser(); + private static final ConstructCredentialAlg CONSTRUCT_CREDENTIAL_ALG = new ConstructCredentialAlg(); + private static final JsonCollectedClientDataSerializer JSON_COLLECTED_CLIENT_DATA_SERIALIZER = + new JsonCollectedClientDataSerializer(); + + public WebauthnSecurityKeyOperationFactory(PinProtocolV1 pinProtocolV1) { + this.pinProtocolV1 = pinProtocolV1; + } + + + public + WebauthnSecurityKeyOperation getOperation(WC webauthnCommand, boolean isCtap2Supported) { + if (isCtap2Supported) { + return getCtap2Operation(webauthnCommand); + } else { + return getCtap1Operation(webauthnCommand); + } + } + + @SuppressWarnings("unchecked") // we hand out only correctly matching operations + private WebauthnSecurityKeyOperation getCtap2Operation( + WC webauthnCommand) { + if (webauthnCommand instanceof PublicKeyCredentialCreate) { + return (WebauthnSecurityKeyOperation) getAuthenticatorMakeCredentialOperation(); + } else if (webauthnCommand instanceof PublicKeyCredentialGet) { + return (WebauthnSecurityKeyOperation) getAuthenticatorGetAssertionOperation(); + } else { + throw new UnsupportedOperationException(); + } + } + + @SuppressWarnings("unchecked") // we hand out only correctly matching operations + private WebauthnSecurityKeyOperation getCtap1Operation( + WC webauthnCommand) { + if (webauthnCommand instanceof PublicKeyCredentialCreate) { + return (WebauthnSecurityKeyOperation) new AuthenticatorMakeCredentialCtap1Operation( + getAuthenticatorMakeCredentialOperation()); + } else if (webauthnCommand instanceof PublicKeyCredentialGet) { + return (WebauthnSecurityKeyOperation) new AuthenticatorGetAssertionCtap1Operation( + getAuthenticatorGetAssertionOperation()); + } else { + throw new UnsupportedOperationException(); + } + } + + private AuthenticatorMakeCredentialOperation getAuthenticatorMakeCredentialOperation() { + return new AuthenticatorMakeCredentialOperation(CONSTRUCT_CREDENTIAL_ALG, pinProtocolV1); + } + + private AuthenticatorGetAssertionOperation getAuthenticatorGetAssertionOperation() { + return new AuthenticatorGetAssertionOperation( + CBOR_PUBLIC_KEY_CREDENTIAL_DESCRIPTOR_PARSER, pinProtocolV1, + JSON_COLLECTED_CLIENT_DATA_SERIALIZER); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/AuthenticatorGetAssertionCtap1Operation.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/AuthenticatorGetAssertionCtap1Operation.java new file mode 100644 index 0000000..6a74fa9 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/AuthenticatorGetAssertionCtap1Operation.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.operations.ctap1; + + +import java.io.IOException; +import java.util.List; + +import de.cotech.hw.fido2.PublicKeyCredential; +import de.cotech.hw.fido2.PublicKeyCredentialGet; +import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; +import de.cotech.hw.fido2.domain.create.AuthenticatorData; +import de.cotech.hw.fido2.domain.get.AuthenticatorAssertionResponse; +import de.cotech.hw.fido2.exceptions.FidoWrongKeyHandleException; +import de.cotech.hw.fido2.internal.Fido2AppletConnection; +import de.cotech.hw.fido2.internal.Fido2CommandApduFactory; +import de.cotech.hw.fido2.internal.ctap2.commands.getAssertion.AuthenticatorGetAssertion; +import de.cotech.hw.fido2.internal.operations.WebauthnSecurityKeyOperation; +import de.cotech.hw.fido2.internal.operations.ctap2.AuthenticatorGetAssertionOperation; +import de.cotech.hw.fido2.internal.webauthn.AuthenticatorDataParser; +import de.cotech.hw.internal.iso7816.CommandApdu; +import de.cotech.hw.internal.iso7816.ResponseApdu; +import de.cotech.hw.util.Arrays; +import de.cotech.hw.util.HashUtil; +import de.cotech.hw.util.HwTimber; + + +public class AuthenticatorGetAssertionCtap1Operation extends + WebauthnSecurityKeyOperation { + private final AuthenticatorGetAssertionOperation ctap2Operation; + private final Fido2CommandApduFactory fido2CommandApduFactory = new Fido2CommandApduFactory(); + + public AuthenticatorGetAssertionCtap1Operation( + AuthenticatorGetAssertionOperation ctap2Operation) { + this.ctap2Operation = ctap2Operation; + } + + @Override + public PublicKeyCredential performWebauthnSecurityKeyOperation( + Fido2AppletConnection fido2AppletConnection, + PublicKeyCredentialGet request) + throws IOException { + AuthenticatorGetAssertion authenticatorGetAssertion = + ctap2Operation.webauthnCommandToCtap2Command(request, null); + byte[] rpIdHash = HashUtil.sha256(authenticatorGetAssertion.rpId()); + + List allowedCredentials = + authenticatorGetAssertion.allowList(); + for (int i = 0, count = allowedCredentials.size(); i < count; i++) { + try { + PublicKeyCredentialDescriptor credential = allowedCredentials.get(i); + HwTimber.i("Attempting credentials (%d/%d): %s", i+1, count, credential); + return attemptU2fAuthentication(fido2AppletConnection, authenticatorGetAssertion, + rpIdHash, credential); + } catch (FidoWrongKeyHandleException e) { + HwTimber.d("Key handle rejected"); + } + } + + // CtapErrorResponse.create(CtapErrorResponse.CTAP2_ERR_NO_CREDENTIALS); + throw new IOException("No valid credentials provided!"); + } + + private PublicKeyCredential attemptU2fAuthentication( + Fido2AppletConnection fido2AppletConnection, + AuthenticatorGetAssertion authenticatorGetAssertion, byte[] rpIdHash, + PublicKeyCredentialDescriptor publicKeyCredentialDescriptor) throws IOException { + CommandApdu authenticationCommand = createCtap1CommandApdu( + authenticatorGetAssertion.clientDataHash(), rpIdHash, publicKeyCredentialDescriptor.id()); + ResponseApdu responseApdu = fido2AppletConnection.communicateOrThrow(authenticationCommand); + return ctap1ResponseApduToWebauthnResponse(authenticatorGetAssertion, publicKeyCredentialDescriptor, rpIdHash, responseApdu); + } + + private CommandApdu createCtap1CommandApdu( + byte[] challengeParam, byte[] applicationParam, byte[] keyHandle) { + byte[] keyHandleLength = new byte[] { (byte) keyHandle.length }; + byte[] payload = Arrays.concatenate(challengeParam, applicationParam, keyHandleLength, keyHandle); + return fido2CommandApduFactory.createAuthenticationCommand(payload); + } + + private PublicKeyCredential ctap1ResponseApduToWebauthnResponse( + AuthenticatorGetAssertion authenticatorGetAssertion, + PublicKeyCredentialDescriptor publicKeyCredentialDescriptor, byte[] rpIdHash, + ResponseApdu responseApdu) throws IOException { + byte[] responseData = responseApdu.getData(); + U2fAuthenticateResponse u2fResponse = U2fAuthenticateResponse.fromBytes(responseData); + + AuthenticatorData authenticatorData = + AuthenticatorData.create(rpIdHash, (byte) 1, u2fResponse.counter(), null, null); + byte[] authenticatorDataBytes = new AuthenticatorDataParser().toBytes(authenticatorData); + AuthenticatorAssertionResponse authenticatorResponse = + AuthenticatorAssertionResponse.create( + authenticatorGetAssertion.clientDataJson().getBytes(), + authenticatorDataBytes, + u2fResponse.signature(), + null + ); + + return PublicKeyCredential.create(publicKeyCredentialDescriptor.id(), authenticatorResponse); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/AuthenticatorMakeCredentialCtap1Operation.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/AuthenticatorMakeCredentialCtap1Operation.java new file mode 100644 index 0000000..c42938d --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/AuthenticatorMakeCredentialCtap1Operation.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.operations.ctap1; + + +import java.io.IOException; + +import de.cotech.hw.fido2.internal.cbor.CborCtap1AttestationStatementUtil; +import de.cotech.hw.fido2.internal.cose.CosePublicKeyUtils; +import de.cotech.hw.fido2.internal.ctap2.commands.makeCredential.AuthenticatorMakeCredential; +import de.cotech.hw.fido2.domain.create.AttestationObject; +import de.cotech.hw.fido2.domain.create.AttestedCredentialData; +import de.cotech.hw.fido2.domain.create.AuthenticatorAttestationResponse; +import de.cotech.hw.fido2.domain.create.AuthenticatorData; +import de.cotech.hw.fido2.PublicKeyCredentialCreate; +import de.cotech.hw.fido2.internal.Fido2AppletConnection; +import de.cotech.hw.fido2.internal.Fido2CommandApduFactory; +import de.cotech.hw.fido2.internal.operations.WebauthnSecurityKeyOperation; +import de.cotech.hw.fido2.internal.operations.ctap2.AuthenticatorMakeCredentialOperation; +import de.cotech.hw.fido2.internal.webauthn.AuthenticatorDataParser; +import de.cotech.hw.fido2.PublicKeyCredential; +import de.cotech.hw.fido2.internal.cbor.CborAttestationObjectSerializer; +import de.cotech.hw.internal.iso7816.CommandApdu; +import de.cotech.hw.internal.iso7816.ResponseApdu; +import de.cotech.hw.util.Arrays; +import de.cotech.hw.util.HashUtil; + + +public class AuthenticatorMakeCredentialCtap1Operation extends + WebauthnSecurityKeyOperation { + private final AuthenticatorMakeCredentialOperation ctap2Operation; + private final Fido2CommandApduFactory fido2CommandApduFactory = new Fido2CommandApduFactory(); + private final AuthenticatorDataParser authenticatorDataParser = new AuthenticatorDataParser(); + + public AuthenticatorMakeCredentialCtap1Operation( + AuthenticatorMakeCredentialOperation ctap2Operation) { + this.ctap2Operation = ctap2Operation; + } + + @Override + public PublicKeyCredential performWebauthnSecurityKeyOperation( + Fido2AppletConnection fido2AppletConnection, + PublicKeyCredentialCreate create) throws IOException { + AuthenticatorMakeCredential authenticatorGetAssertion = + ctap2Operation.webauthnToCtap2Command(create, null); + + byte[] rpIdHash = HashUtil.sha256(authenticatorGetAssertion.rp().id()); + + CommandApdu registrationCommand = createCtap1CommandApdu(authenticatorGetAssertion, rpIdHash); + ResponseApdu responseApdu = fido2AppletConnection.communicateOrThrow(registrationCommand); + return ctap1ResponseApduToWebauthnResponse(authenticatorGetAssertion, rpIdHash, responseApdu); + } + + private CommandApdu createCtap1CommandApdu( + AuthenticatorMakeCredential authenticatorGetAssertion, byte[] rpIdHash) { + byte[] challengeParam = authenticatorGetAssertion.clientDataHash(); + byte[] payload = Arrays.concatenate(challengeParam, rpIdHash); + return fido2CommandApduFactory.createRegistrationCommand(payload); + } + + private PublicKeyCredential ctap1ResponseApduToWebauthnResponse( + AuthenticatorMakeCredential authenticatorGetAssertion, byte[] applicationParam, + ResponseApdu responseApdu) throws IOException { + byte[] responseData = responseApdu.getData(); + U2fRegisterResponse u2fResponse = U2fRegisterResponse.fromBytes(responseData); + byte[] coseEncodedCredentialPublicKey = CosePublicKeyUtils.encodex962PublicKeyAsCose(u2fResponse.publicKey()); + AttestedCredentialData attestedCredentialData = + AttestedCredentialData.create(new byte[16], u2fResponse.keyHandle(), coseEncodedCredentialPublicKey); + byte flags = (byte) (AuthenticatorData.FLAG_USER_PRESENT | AuthenticatorData.FLAG_ATTESTED_CREDENTIAL_DATA); + AuthenticatorData authenticatorData = AuthenticatorData + .create(applicationParam, flags, 0, attestedCredentialData, null); + byte[] authData = authenticatorDataParser.toBytes(authenticatorData); + byte[] attStmt = CborCtap1AttestationStatementUtil.toAttestionStatement( + u2fResponse.attestationCertificate(), u2fResponse.signature()); + + AttestationObject attestationObject = AttestationObject.create("fido-u2f", authData, attStmt); + + byte[] rawId = authenticatorData.attestedCredentialData().credentialId(); + byte[] attestationObjectBytes = new CborAttestationObjectSerializer().serializeAttestationObject(attestationObject); + AuthenticatorAttestationResponse response = AuthenticatorAttestationResponse.create( + authenticatorGetAssertion.clientDataJson().getBytes(), attestationObjectBytes); + return PublicKeyCredential.create(rawId, response); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/U2fAuthenticateRequest.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/U2fAuthenticateRequest.java new file mode 100644 index 0000000..206b3fa --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/U2fAuthenticateRequest.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.operations.ctap1; + + +import com.google.auto.value.AutoValue; + + +@AutoValue +public abstract class U2fAuthenticateRequest { + abstract byte[] challengeParameter(); + abstract byte[] applicationParameter(); + abstract byte[] keyHandle(); + + public static U2fAuthenticateRequest create(byte[] challengeParameter, byte[] applicationParameter, byte[] keyHandle) { + return new AutoValue_U2fAuthenticateRequest(challengeParameter, applicationParameter, keyHandle); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/U2fAuthenticateResponse.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/U2fAuthenticateResponse.java new file mode 100644 index 0000000..8b94d09 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/U2fAuthenticateResponse.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.operations.ctap1; + + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.google.auto.value.AutoValue; + + +@AutoValue +public abstract class U2fAuthenticateResponse { + abstract byte presence(); + abstract int counter(); + abstract byte[] signature(); + + public static U2fAuthenticateResponse fromBytes(byte[] data) throws IOException { + ByteBuffer buf = ByteBuffer.wrap(data); + + byte presence = buf.get(); + int counter = buf.getInt(); + + int signatureLength = buf.remaining(); + if (signatureLength < 70 || signatureLength > 73) { + throw new IOException("Signature length must be 71-73 bytes!"); + } + byte[] signature = new byte[signatureLength]; + buf.get(signature); + + return new AutoValue_U2fAuthenticateResponse(presence, counter, signature); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/U2fRegisterResponse.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/U2fRegisterResponse.java new file mode 100644 index 0000000..a37914e --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/U2fRegisterResponse.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.operations.ctap1; + + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.internal.utils.DerUtils; + + +@AutoValue +public abstract class U2fRegisterResponse { + abstract byte[] publicKey(); + abstract byte[] keyHandle(); + abstract byte[] attestationCertificate(); + abstract byte[] signature(); + + public static U2fRegisterResponse fromBytes(byte[] data) throws IOException { + ByteBuffer buf = ByteBuffer.wrap(data); + + if (buf.get() != 0x05) { + throw new IOException("Invalid U2F response, first byte must be 0x05!"); + } + byte[] publicKey = new byte[65]; + buf.get(publicKey); + + int keyHandleLength = buf.get(); + byte[] keyHandle = new byte[keyHandleLength]; + buf.get(keyHandle); + + int attestationSignatureLength = DerUtils.findDerEncodedLength(buf.asReadOnlyBuffer()); + byte[] attestationSignature = new byte[attestationSignatureLength]; + buf.get(attestationSignature); + + int signatureLength = buf.remaining(); + if (signatureLength < 70 || signatureLength > 73) { + throw new IOException("Signature length must be 71-73 bytes!"); + } + byte[] signature = new byte[signatureLength]; + buf.get(signature); + + return new AutoValue_U2fRegisterResponse(publicKey, keyHandle, attestationSignature, signature); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorGetAssertionOperation.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorGetAssertionOperation.java new file mode 100644 index 0000000..1fd0948 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorGetAssertionOperation.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.operations.ctap2; + + +import java.io.IOException; +import java.util.List; + +import android.net.Uri; + +import de.cotech.hw.fido2.PublicKeyCredential; +import de.cotech.hw.fido2.PublicKeyCredentialGet; +import de.cotech.hw.fido2.domain.CollectedClientData; +import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; +import de.cotech.hw.fido2.domain.PublicKeyCredentialUserEntity; +import de.cotech.hw.fido2.domain.UserVerificationRequirement; +import de.cotech.hw.fido2.domain.get.AssertionCreationData; +import de.cotech.hw.fido2.domain.get.AuthenticatorAssertionResponse; +import de.cotech.hw.fido2.domain.get.PublicKeyCredentialRequestOptions; +import de.cotech.hw.fido2.exceptions.FidoClientPinNotSetException; +import de.cotech.hw.fido2.exceptions.FidoClientPinNotSupportedException; +import de.cotech.hw.fido2.exceptions.FidoClientPinRequiredException; +import de.cotech.hw.fido2.exceptions.FidoInvalidCredentialException; +import de.cotech.hw.fido2.exceptions.FidoResidentKeyNoCredentialException; +import de.cotech.hw.fido2.exceptions.FidoResidentKeyNotSupportedException; +import de.cotech.hw.fido2.exceptions.FidoSecurityError; +import de.cotech.hw.fido2.internal.Fido2AppletConnection; +import de.cotech.hw.fido2.internal.cbor.CborPublicKeyCredentialDescriptorParser; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Exception; +import de.cotech.hw.fido2.internal.ctap2.CtapErrorResponse; +import de.cotech.hw.fido2.internal.ctap2.commands.getAssertion.AuthenticatorGetAssertion; +import de.cotech.hw.fido2.internal.ctap2.commands.getAssertion.AuthenticatorGetAssertionResponse; +import de.cotech.hw.fido2.internal.json.JsonCollectedClientDataSerializer; +import de.cotech.hw.fido2.internal.operations.WebauthnSecurityKeyOperation; +import de.cotech.hw.fido2.internal.pinauth.PinProtocolV1; +import de.cotech.hw.fido2.internal.pinauth.PinToken; +import de.cotech.hw.util.HashUtil; +import de.cotech.hw.util.HwTimber; + + +public class AuthenticatorGetAssertionOperation extends + WebauthnSecurityKeyOperation { + private static final String CLIENT_DATA_TYPE_GET = "webauthn.get"; + + private final CborPublicKeyCredentialDescriptorParser cborPublicKeyCredentialDescriptorParser; + private final PinProtocolV1 pinProtocolV1; + private final JsonCollectedClientDataSerializer jsonCollectedClientDataSerializer; + + public AuthenticatorGetAssertionOperation( + CborPublicKeyCredentialDescriptorParser cborPublicKeyCredentialDescriptorParser, + PinProtocolV1 pinProtocolV1, + JsonCollectedClientDataSerializer jsonCollectedClientDataSerializer) { + this.cborPublicKeyCredentialDescriptorParser = cborPublicKeyCredentialDescriptorParser; + this.pinProtocolV1 = pinProtocolV1; + this.jsonCollectedClientDataSerializer = jsonCollectedClientDataSerializer; + } + + @Override + public PublicKeyCredential performWebauthnSecurityKeyOperation( + Fido2AppletConnection fido2AppletConnection, + PublicKeyCredentialGet request) + throws IOException { + List allowCredentials = request.options().allowCredentials(); + boolean isResidentKey = allowCredentials == null || allowCredentials.isEmpty(); + if (isResidentKey && !fido2AppletConnection.isSupportResidentKeys()) { + throw new FidoResidentKeyNotSupportedException(); + } + + PinToken pinToken = acquirePinToken(fido2AppletConnection, request); + AuthenticatorGetAssertion authenticatorGetAssertion = webauthnCommandToCtap2Command(request, pinToken); + HwTimber.d(authenticatorGetAssertion.toString()); + try { + AuthenticatorGetAssertionResponse response = + fido2AppletConnection.ctap2CommunicateOrThrow(authenticatorGetAssertion); + return ctap2ResponseToWebauthnResponse(request, response); + } catch (Ctap2Exception e) { + switch (e.ctapErrorResponse.errorCode()) { + case CtapErrorResponse.CTAP2_ERR_PIN_REQUIRED: + throw new FidoClientPinRequiredException(); + case CtapErrorResponse.CTAP2_ERR_INVALID_CREDENTIAL: + case CtapErrorResponse.CTAP2_ERR_NO_CREDENTIALS: { + if (isResidentKey) { + throw new FidoResidentKeyNoCredentialException(); + } else { + throw new FidoInvalidCredentialException(); + } + } + } + throw e; + } + } + + private PinToken acquirePinToken( + Fido2AppletConnection fido2AppletConnection, + PublicKeyCredentialGet request + ) throws IOException { + if (fido2AppletConnection.getCachedPinToken() != null) { + return fido2AppletConnection.getCachedPinToken(); + } + if (request.options().userVerification() == UserVerificationRequirement.REQUIRED) { + if (!fido2AppletConnection.isSupportClientPin()) { + throw new FidoClientPinNotSupportedException(); + } + if (!fido2AppletConnection.isClientPinSet()) { + throw new FidoClientPinNotSetException(); + } + if (request.clientPin() == null) { + throw new FidoClientPinRequiredException(); + } + } + if (request.clientPin() == null || !fido2AppletConnection.isSupportClientPin() || !fido2AppletConnection.isClientPinSet()) { + return null; + } + + PinToken pinToken = pinProtocolV1.clientPinAuthenticate(fido2AppletConnection, request.clientPin(), request.lastAttemptOk()); + fido2AppletConnection.setCachedPinToken(pinToken); + return pinToken; + } + + public AuthenticatorGetAssertion webauthnCommandToCtap2Command( + PublicKeyCredentialGet credentialCreate, PinToken pinToken) throws FidoSecurityError { + Uri callerOrigin = Uri.parse(credentialCreate.origin()); + String effectivedomain = callerOrigin.getHost(); + + PublicKeyCredentialRequestOptions options = credentialCreate.options(); + + String rpId = options.rpId(); + if (rpId == null) { + rpId = effectivedomain; + } else if (!rpId.equals(effectivedomain)) { + throw new FidoSecurityError("Security error: rpId is not a valid subdomain of caller origin!"); + } + + CollectedClientData collectedClientData = + CollectedClientData.create(CLIENT_DATA_TYPE_GET, options.challenge(), credentialCreate.origin(), "SHA-256"); + String clientDataJson = jsonCollectedClientDataSerializer.clientClientDataToJson(collectedClientData); + byte[] clientDataHash = HashUtil.sha256(clientDataJson); + + if (pinToken != null) { + byte[] pinAuth = pinProtocolV1.calculatePinAuth(pinToken, clientDataHash); + return AuthenticatorGetAssertion + .create(rpId, clientDataHash, clientDataJson, options.allowCredentials(), null, + pinAuth, PinProtocolV1.PIN_PROTOCOL); + } else { + return AuthenticatorGetAssertion.create(rpId, clientDataHash, clientDataJson, options.allowCredentials(), null); + } + } + + private PublicKeyCredential ctap2ResponseToWebauthnResponse( + PublicKeyCredentialGet credentialCreate, + AuthenticatorGetAssertionResponse response + ) throws IOException { + byte[] credential = determinePublicKeyCredentialId(credentialCreate, response); + + PublicKeyCredentialUserEntity user = response.user(); + AssertionCreationData assertionCreationData = AssertionCreationData.create( + credential, + response.clientDataJSON(), + response.authData(), + response.signature(), + user != null ? user.id() : null + ); + + // Strictly following the spec, these next few lines belong in a ConstructAssertionAlg object. + // However, they are really just copying data from AssertionCreationData, so we just do that + // inline. + AuthenticatorAssertionResponse authenticatorResponse = AuthenticatorAssertionResponse.create( + assertionCreationData.clientDataJSONResult(), + assertionCreationData.authenticatorDataResult(), + assertionCreationData.signatureResult(), + assertionCreationData.userHandleResult() + ); + return PublicKeyCredential + .create(assertionCreationData.credentialIdResult(), authenticatorResponse); + } + + private byte[] determinePublicKeyCredentialId(PublicKeyCredentialGet credentialCreate, + AuthenticatorGetAssertionResponse response) throws IOException { + byte[] credential; + List requestedCredentials = + credentialCreate.options().allowCredentials(); + if (requestedCredentials != null && requestedCredentials.size() == 1) { + credential = requestedCredentials.get(0).id(); + } else if (response.credential() != null) { + Integer numberOfCredentials = response.numberOfCredentials(); + if (numberOfCredentials != null && numberOfCredentials > 1) { + HwTimber.d("More than one credential returned, but not supported. Returning first."); + } + PublicKeyCredentialDescriptor publicKeyCredentialDescriptor = + cborPublicKeyCredentialDescriptorParser.parse(response.credential()); + credential = publicKeyCredentialDescriptor.id(); + } else { + throw new IOException("Authenticator failed to transmit credential!"); + } + return credential; + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorMakeCredentialOperation.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorMakeCredentialOperation.java new file mode 100644 index 0000000..e93b42f --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorMakeCredentialOperation.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.operations.ctap2; + + +import java.io.IOException; + +import android.net.Uri; + +import de.cotech.hw.fido2.PublicKeyCredential; +import de.cotech.hw.fido2.PublicKeyCredentialCreate; +import de.cotech.hw.fido2.domain.CollectedClientData; +import de.cotech.hw.fido2.domain.PublicKeyCredentialRpEntity; +import de.cotech.hw.fido2.domain.UserVerificationRequirement; +import de.cotech.hw.fido2.domain.create.AttestationObject; +import de.cotech.hw.fido2.domain.create.CredentialCreationData; +import de.cotech.hw.fido2.domain.create.PublicKeyCredentialCreationOptions; +import de.cotech.hw.fido2.exceptions.FidoClientPinNotSetException; +import de.cotech.hw.fido2.exceptions.FidoClientPinNotSupportedException; +import de.cotech.hw.fido2.exceptions.FidoClientPinRequiredException; +import de.cotech.hw.fido2.exceptions.FidoSecurityError; +import de.cotech.hw.fido2.internal.Fido2AppletConnection; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Exception; +import de.cotech.hw.fido2.internal.ctap2.CtapErrorResponse; +import de.cotech.hw.fido2.internal.ctap2.commands.makeCredential.AuthenticatorMakeCredential; +import de.cotech.hw.fido2.internal.ctap2.commands.makeCredential.AuthenticatorMakeCredential.AuthenticatorMakeCredentialOptions; +import de.cotech.hw.fido2.internal.ctap2.commands.makeCredential.AuthenticatorMakeCredentialResponse; +import de.cotech.hw.fido2.internal.json.JsonCollectedClientDataSerializer; +import de.cotech.hw.fido2.internal.operations.WebauthnSecurityKeyOperation; +import de.cotech.hw.fido2.internal.pinauth.PinProtocolV1; +import de.cotech.hw.fido2.internal.pinauth.PinToken; +import de.cotech.hw.fido2.internal.webauthn.ConstructCredentialAlg; +import de.cotech.hw.util.HashUtil; +import de.cotech.hw.util.HwTimber; + + +public class AuthenticatorMakeCredentialOperation extends + WebauthnSecurityKeyOperation { + private static final String CLIENT_DATA_TYPE_CREATE = "webauthn.create"; + private final ConstructCredentialAlg constructCredentialAlg; + private final PinProtocolV1 pinProtocolV1; + + public AuthenticatorMakeCredentialOperation( + ConstructCredentialAlg constructCredentialAlg, + PinProtocolV1 pinProtocolV1) { + this.constructCredentialAlg = constructCredentialAlg; + this.pinProtocolV1 = pinProtocolV1; + } + + @Override + public PublicKeyCredential performWebauthnSecurityKeyOperation( + Fido2AppletConnection fido2AppletConnection, + PublicKeyCredentialCreate request) throws IOException { + PinToken pinToken = acquirePinToken(fido2AppletConnection, request); + AuthenticatorMakeCredential authenticatorMakeCredential = webauthnToCtap2Command(request, pinToken); + HwTimber.d(authenticatorMakeCredential.toString()); + try { + AuthenticatorMakeCredentialResponse response = + fido2AppletConnection.ctap2CommunicateOrThrow(authenticatorMakeCredential); + return ctap2ToWebauthnResponse(request, response); + } catch (Ctap2Exception e) { + switch (e.ctapErrorResponse.errorCode()) { + case CtapErrorResponse.CTAP2_ERR_PIN_REQUIRED: + throw new FidoClientPinRequiredException(); + } + throw e; + } + } + + private PinToken acquirePinToken( + Fido2AppletConnection fido2AppletConnection, + PublicKeyCredentialCreate request + ) throws IOException { + if (fido2AppletConnection.getCachedPinToken() != null) { + return fido2AppletConnection.getCachedPinToken(); + } + if (request.options().authenticatorSelection().userVerification() == UserVerificationRequirement.REQUIRED) { + if (!fido2AppletConnection.isSupportClientPin()) { + throw new FidoClientPinNotSupportedException(); + } + if (!fido2AppletConnection.isClientPinSet()) { + throw new FidoClientPinNotSetException(); + } + if (request.clientPin() == null) { + throw new FidoClientPinRequiredException(); + } + } + if (request.clientPin() == null || !fido2AppletConnection.isSupportClientPin() || !fido2AppletConnection.isClientPinSet()) { + return null; + } + + PinToken pinToken = pinProtocolV1.clientPinAuthenticate(fido2AppletConnection, request.clientPin(), request.lastAttemptOk()); + fido2AppletConnection.setCachedPinToken(pinToken); + return pinToken; + } + + public AuthenticatorMakeCredential webauthnToCtap2Command( + PublicKeyCredentialCreate credentialCreate, PinToken pinToken) + throws FidoSecurityError { + Uri callerOrigin = Uri.parse(credentialCreate.origin()); + String effectivedomain = callerOrigin.getHost(); + + PublicKeyCredentialCreationOptions options = credentialCreate.options(); + + PublicKeyCredentialRpEntity rp = options.rp(); + String rpId = rp.id(); + if (rpId == null) { + rp = rp.withId(effectivedomain); + } else if (!rpId.equals(effectivedomain)) { + throw new FidoSecurityError("Security error: rpId is not a valid subdomain of caller origin!"); + } + + + CollectedClientData collectedClientData = + CollectedClientData.create(CLIENT_DATA_TYPE_CREATE, options.challenge(), credentialCreate.origin(), "SHA-256"); + String clientDataJson = new JsonCollectedClientDataSerializer().clientClientDataToJson(collectedClientData); + byte[] clientDataHash = HashUtil.sha256(clientDataJson); + + AuthenticatorMakeCredentialOptions authenticatorOptions; + boolean requireResidentKey = options.authenticatorSelection().requireResidentKey(); + // this is the only supported option so far. only set options if it is set + if (requireResidentKey) { + authenticatorOptions = AuthenticatorMakeCredentialOptions.create(true, false); + } else { + authenticatorOptions = null; + } + + if (pinToken != null) { + byte[] pinAuth = pinProtocolV1.calculatePinAuth(pinToken, clientDataHash); + return AuthenticatorMakeCredential.create(clientDataHash, clientDataJson, rp, options.user(), options.pubKeyCredParams(), + options.excludeCredentials(), authenticatorOptions, pinAuth, PinProtocolV1.PIN_PROTOCOL); + } else { + return AuthenticatorMakeCredential.create(clientDataHash, clientDataJson, rp, options.user(), options.pubKeyCredParams(), + options.excludeCredentials(), authenticatorOptions, null, null); + } + } + + private PublicKeyCredential ctap2ToWebauthnResponse( + PublicKeyCredentialCreate credentialCreate, + AuthenticatorMakeCredentialResponse response + ) throws IOException { + CredentialCreationData credentialCreationData = CredentialCreationData.create( + AttestationObject.create(response.fmt(), response.authData(), response.attStmt()), + response.clientDataJSON(), + credentialCreate.options().attestation() + ); + return constructCredentialAlg.publicKeyCredential(credentialCreationData); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinAuthCryptoUtil.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinAuthCryptoUtil.java new file mode 100644 index 0000000..9d9b4e6 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinAuthCryptoUtil.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.pinauth; + + +import java.io.IOException; +import java.nio.charset.Charset; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; + +import androidx.annotation.Keep; +import androidx.annotation.VisibleForTesting; + +import de.cotech.hw.fido2.exceptions.FidoClientPinTooShortException; +import de.cotech.hw.fido2.internal.cose.CosePublicKeyUtils; +import de.cotech.hw.fido2.internal.crypto.P256; +import de.cotech.hw.util.Arrays; +import de.cotech.hw.util.HashUtil; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyAgreement; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + + +public class PinAuthCryptoUtil { + + private static final byte ZERO_BYTE = 0; + + /** + * CTAP2 pinAuth, used for authentication of operations based on pinToken. + * https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorClientPIN + */ + public byte[] calculatePinAuth(byte[] pinToken, byte[] clientDataHash) { + return hmacSha256Left16Bytes(pinToken, clientDataHash); + } + + /** + * Leftmost 16 bytes of an HMAC-SHA-256 operation. + */ + private byte[] hmacSha256Left16Bytes(byte[] secret, byte[] data) { + return Arrays.copyOfRange(hmacSha256(secret, data), 0, 16); + } + + /** + * Simple HMAC-SHA-256. + */ + private byte[] hmacSha256(byte[] secret, byte[] data) { + try { + Mac hmacSHA256 = Mac.getInstance("HmacSHA256"); + hmacSHA256.init(new SecretKeySpec(secret, "HmacSHA256")); + return hmacSHA256.doFinal(data); + } catch (InvalidKeyException | NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + public byte[] calculatePinHashEnc(byte[] sharedSecret, String pin) throws IOException { + byte[] pinHash = calculatePinHash(pin); + try { + SecretKeySpec secretKey = new SecretKeySpec(sharedSecret, "AES"); + IvParameterSpec ivParameterSpec = new IvParameterSpec(getIv()); + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec); + return cipher.doFinal(pinHash); + } catch (IllegalBlockSizeException | InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException | InvalidKeyException e) { + throw new IllegalStateException(e); + } + } + + @Keep + private byte[] getIv() { + // IV=0 as per CTAP2 specification + // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#settingNewPin + // Workaround to prevent warnings in Google Play Dev Console: + // https://support.google.com/faqs/answer/9450925?hl=en + byte[] iv = new byte[16]; + Arrays.fill(iv, ZERO_BYTE); + return iv; + } + + @VisibleForTesting + byte[] calculatePinHash(String pin) throws IOException { + if (pin.length() < 4) { + throw new FidoClientPinTooShortException(); + } + return Arrays.copyOfRange(HashUtil.sha256(pin.getBytes()), 0, 16); + } + + byte[] padPin(String pin) throws IOException { + if (pin.length() < 4) { + throw new FidoClientPinTooShortException(); + } + byte[] pinBytes = pin.getBytes(Charset.forName("UTF-8")); + if (pinBytes.length > 63) { + throw new IOException("PIN UTF-8 encoding must not exceed 63 bytes length!"); + } + byte[] result = new byte[64]; + System.arraycopy(pinBytes, 0, result, 0, pinBytes.length); + return result; + } + + public byte[] decryptPinToken(byte[] sharedSecret, byte[] pinTokenEnc) throws IOException { + try { + SecretKeySpec secretKey = new SecretKeySpec(sharedSecret, "AES"); + IvParameterSpec ivParameterSpec = new IvParameterSpec(new byte[16]); + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec); + return cipher.doFinal(pinTokenEnc); + } catch (NoSuchPaddingException | NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } catch (IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException | InvalidKeyException e) { + throw new IOException("Error decrypting pinToken from authenticator", e); + } + } + + public KeyPair generatePlatformKeyPair() { + try { + return P256.newKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } catch (GeneralSecurityException e) { + throw new IllegalStateException(e); + } + } + + public byte[] cosePublicKeyFromPublicKey(PublicKey publicKey) throws IOException { + return CosePublicKeyUtils.encodex962PublicKeyAsCose(P256.serializePublicKey(publicKey)); + } + + public PublicKey publicKeyFromCosePublicKey(byte[] cosePublicKey) throws IOException { + try { + byte[] authenticatorKeyAgreementX962Key = CosePublicKeyUtils.encodeCosePublicKeyAsX962(cosePublicKey); + return P256.deserializePublicKey(authenticatorKeyAgreementX962Key); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Missing ECDH algorithm in crypto provider! This is a build system bug, perhaps proguard stripped the crypto provider.", e); + } catch (GeneralSecurityException e) { + throw new IOException("Failed decoding authenticator public key", e); + } + } + + public byte[] generateSharedSecret( + PrivateKey platformPrivateKey, + PublicKey authenticatorPublicKey + ) { + try { + KeyAgreement ka = KeyAgreement.getInstance("ECDH"); + ka.init(platformPrivateKey); + ka.doPhase(authenticatorPublicKey, true); + + return HashUtil.sha256(ka.generateSecret()); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("Invalid key used for ECDH. This is a bug, perhaps a PrivateKey or PublicKey from a different provider was used?", e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Missing ECDH algorithm in crypto provider! This is a build system bug, perhaps proguard stripped the crypto provider.", e); + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinProtocolV1.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinProtocolV1.java new file mode 100644 index 0000000..976cbd9 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinProtocolV1.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.pinauth; + + +import java.io.IOException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; + +import de.cotech.hw.fido2.exceptions.FidoClientPinBlockedException; +import de.cotech.hw.fido2.exceptions.FidoClientPinInvalidException; +import de.cotech.hw.fido2.exceptions.FidoClientPinLastAttemptException; +import de.cotech.hw.fido2.internal.Fido2AppletConnection; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Command; +import de.cotech.hw.fido2.internal.ctap2.Ctap2Exception; +import de.cotech.hw.fido2.internal.ctap2.CtapErrorResponse; +import de.cotech.hw.fido2.internal.ctap2.commands.clientPin.AuthenticatorClientPin; +import de.cotech.hw.fido2.internal.ctap2.commands.clientPin.AuthenticatorClientPinResponse; +import de.cotech.hw.util.Arrays; +import de.cotech.hw.util.Hex; +import de.cotech.hw.util.HwTimber; + + +public class PinProtocolV1 { + public static final int PIN_PROTOCOL = 1; + + private final PinAuthCryptoUtil pinAuthCryptoUtil; + + public PinProtocolV1(PinAuthCryptoUtil pinAuthCryptoUtil) { + this.pinAuthCryptoUtil = pinAuthCryptoUtil; + } + + public PinToken clientPinAuthenticate( + Fido2AppletConnection fido2AppletConnection, String pin, boolean lastAttemptOk) throws IOException { + AuthenticatorClientPin ctap2Command; + AuthenticatorClientPinResponse authenticatorClientPinResponse; + + HwTimber.d("Authenticating with PIN"); + + int retries; + try { + retries = checkRetries(fido2AppletConnection); + if (retries == 0) { + throw new FidoClientPinBlockedException(); + } + if (retries == 1 && !lastAttemptOk) { + throw new FidoClientPinLastAttemptException(); + } + } catch (Ctap2Exception e) { + if (e.ctapErrorResponse.errorCode() == CtapErrorResponse.CTAP2_ERR_PIN_BLOCKED) { + throw new FidoClientPinBlockedException(); + } + throw e; + } + + ctap2Command = AuthenticatorClientPin.createGetKeyAgreement(); + authenticatorClientPinResponse = fido2AppletConnection.ctap2CommunicateOrThrow(ctap2Command); + + KeyPair platformKeyPair = pinAuthCryptoUtil.generatePlatformKeyPair(); + PrivateKey platformPrivateKey = platformKeyPair.getPrivate(); + PublicKey authenticatorPublicKey = pinAuthCryptoUtil.publicKeyFromCosePublicKey(authenticatorClientPinResponse.keyAgreement()); + + byte[] sharedSecret = pinAuthCryptoUtil.generateSharedSecret(platformPrivateKey, authenticatorPublicKey); + byte[] pinHashEnc = pinAuthCryptoUtil.calculatePinHashEnc(sharedSecret, pin); + + byte[] platformKeyAgreementKey = pinAuthCryptoUtil.cosePublicKeyFromPublicKey(platformKeyPair.getPublic()); + + try { + ctap2Command = + AuthenticatorClientPin.createGetPinToken(platformKeyAgreementKey, pinHashEnc); + authenticatorClientPinResponse = + fido2AppletConnection.ctap2CommunicateOrThrow(ctap2Command); + + byte[] pinToken = pinAuthCryptoUtil + .decryptPinToken(sharedSecret, authenticatorClientPinResponse.pinToken()); + HwTimber.d("Authentication successful. pinToken is " + + Hex.encodeHexString(pinToken)); + return PinToken.create(pinToken); + } catch (Ctap2Exception e) { + switch (e.ctapErrorResponse.errorCode()) { + case CtapErrorResponse.CTAP2_ERR_PIN_BLOCKED: + throw new FidoClientPinInvalidException(0); + case CtapErrorResponse.CTAP2_ERR_PIN_INVALID: + int retriesLeft = retries - 1; + throw new FidoClientPinInvalidException(retriesLeft); + } + throw e; + } finally { + Arrays.fill(sharedSecret, (byte) 0); + } + } + + private int checkRetries(Fido2AppletConnection fido2AppletConnection) + throws IOException { + Ctap2Command ctap2Command = AuthenticatorClientPin.createGetRetries(); + AuthenticatorClientPinResponse response = fido2AppletConnection.ctap2CommunicateOrThrow(ctap2Command); + Integer retries = response.retries(); + if (retries == null) { + throw new IOException("Failed to retrieve retries from authenticator."); + } + return retries; + } + + public byte[] calculatePinAuth(PinToken pinToken, byte[] clientDataHash) { + return pinAuthCryptoUtil.calculatePinAuth(pinToken.pinToken(), clientDataHash); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinToken.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinToken.java new file mode 100644 index 0000000..bd66805 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinToken.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.pinauth; + + +import com.google.auto.value.AutoValue; + + +@AutoValue +public abstract class PinToken { + @SuppressWarnings("mutable") + public abstract byte[] pinToken(); + + public static PinToken create(byte[] pinToken) { + return new AutoValue_PinToken(pinToken); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/utils/AndroidUtils.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/utils/AndroidUtils.java new file mode 100644 index 0000000..0257e85 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/utils/AndroidUtils.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.utils; + + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.AnyThread; +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; + + +@RestrictTo(Scope.LIBRARY_GROUP) +public class AndroidUtils { + + private AndroidUtils() { } + + @AnyThread + @SuppressLint("WrongThread") // we check dynamically and do the right thing + public static void addLifecycleObserver(LifecycleOwner lifecycleOwner, LifecycleObserver lifecycleObserver) { + if (lifecycleOwner != null) { + if (Looper.getMainLooper().getThread() == Thread.currentThread()) { + lifecycleOwner.getLifecycle().addObserver(lifecycleObserver); + } else { + new Handler(Looper.getMainLooper()).post(() -> + lifecycleOwner.getLifecycle().addObserver(lifecycleObserver)); + } + } + } + + public static String loadTextFromAssets(Context context, String assetsPath, Charset charset) throws IOException { + InputStream is = context.getResources().getAssets().open(assetsPath); + byte[] buffer = new byte[1024]; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for (int length = is.read(buffer); length != -1; length = is.read(buffer)) { + baos.write(buffer, 0, length); + } + is.close(); + baos.close(); + return charset == null ? new String(baos.toByteArray()) : new String(baos.toByteArray(), charset); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/utils/AnimatedVectorDrawableHelper.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/utils/AnimatedVectorDrawableHelper.java new file mode 100644 index 0000000..2b802bb --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/utils/AnimatedVectorDrawableHelper.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.utils; + +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.core.view.ViewCompat; +import androidx.vectordrawable.graphics.drawable.Animatable2Compat; +import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class AnimatedVectorDrawableHelper { + + public static void startAnimation(ImageView imageView, int resId) { + startAnimation(imageView, resId, null); + } + + public static void startAnimation(ImageView imageView, int resId, Animatable2Compat.AnimationCallback animationCallback) { + if (Build.VERSION.SDK_INT <= 24) { + AnimatedVectorDrawableCompat avdCompat = setAndStartAnimatedVectorDrawableSdk24(imageView, resId); + if (animationCallback != null) { + avdCompat.registerAnimationCallback(animationCallback); + } + } else { + if (animationCallback != null) { + AnimatedVectorDrawableCompat.registerAnimationCallback(imageView.getDrawable(), animationCallback); + } + Animatable animatable = (Animatable) imageView.getDrawable(); + animatable.start(); + } + } + + public static void startAndLoopAnimation(ImageView imageView, int resId) { + Animatable2Compat.AnimationCallback animationCallback = new Animatable2Compat.AnimationCallback() { + @NonNull + private final Handler fHandler = new Handler(Looper.getMainLooper()); + + @Override + public void onAnimationEnd(@NonNull Drawable drawable) { + if (!ViewCompat.isAttachedToWindow(imageView)) { + return; + } + + fHandler.post(() -> { + if (Build.VERSION.SDK_INT <= 24) { + AnimatedVectorDrawableCompat avdCompat = setAndStartAnimatedVectorDrawableSdk24(imageView, resId); + avdCompat.registerAnimationCallback(this); + } else { + ((Animatable) drawable).start(); + } + }); + } + }; + + if (Build.VERSION.SDK_INT <= 24) { + AnimatedVectorDrawableCompat avdCompat = setAndStartAnimatedVectorDrawableSdk24(imageView, resId); + avdCompat.registerAnimationCallback(animationCallback); + } else { + imageView.setImageResource(resId); + AnimatedVectorDrawableCompat.registerAnimationCallback(imageView.getDrawable(), animationCallback); + Animatable animatable = (Animatable) imageView.getDrawable(); + animatable.start(); + } + } + + private static AnimatedVectorDrawableCompat setAndStartAnimatedVectorDrawableSdk24(ImageView imageView, int resId) { + AnimatedVectorDrawableCompat avdCompat = AnimatedVectorDrawableCompat.create(imageView.getContext(), resId); + + // on SDK <= 24, the alphaFill values are not resetted properly to their initial state + // The states of AnimatedVectorDrawables are stored centrally per resource. + // Thus, making the drawable mutate allows it to have a completely new state + avdCompat.mutate(); + + imageView.setImageDrawable(avdCompat); + avdCompat.start(); + + return avdCompat; + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/utils/DerUtils.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/utils/DerUtils.java new file mode 100644 index 0000000..0767084 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/utils/DerUtils.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.utils; + + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + + +public class DerUtils { + private static final byte LOWER_7_BITS = (byte) 0x7F; + private static final int MAX_NUMBER_OF_BYTES = 4; + + public static int findDerEncodedLength(ByteBuffer buf) throws IOException { + try { + // skip tag byte + buf.get(); + + int i = buf.get(); + if (i == -1) { + throw new IOException("Invalid DER: length missing"); + } + + // A single byte short length + if ((i & ~LOWER_7_BITS) == 0) { + return i + 2; + } + + int num = i & LOWER_7_BITS; + + if (num > MAX_NUMBER_OF_BYTES) { + throw new IOException("Invalid DER: length field too big (" + i + ")"); + } + + byte[] bytes = new byte[num]; + + buf.get(bytes); + + int lengthValue = new BigInteger(1, bytes).intValue(); + return lengthValue + 2 + num; + } catch (BufferUnderflowException e) { + throw new IOException("Invalid DER: length too short", e); + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/utils/WebsafeBase64.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/utils/WebsafeBase64.java new file mode 100644 index 0000000..1ed6c39 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/utils/WebsafeBase64.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.utils; + +import android.util.Base64; + +import androidx.annotation.RestrictTo; +import androidx.annotation.RestrictTo.Scope; + + +public class WebsafeBase64 { + + /** + * Websafe Base64 encoding as specified by the FIDO U2F specification: + * https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#client-data + */ + public static String encodeToString(byte[] decoded) { + return Base64.encodeToString(decoded, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE); + } + + public static byte[] decode(String encoded) { + return Base64.decode(encoded, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/AuthenticatorDataParser.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/AuthenticatorDataParser.java new file mode 100644 index 0000000..5e60627 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/AuthenticatorDataParser.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.webauthn; + + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborException; +import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; +import de.cotech.hw.fido2.domain.create.AttestedCredentialData; +import de.cotech.hw.fido2.domain.create.AuthenticatorData; +import de.cotech.hw.fido2.internal.cbor.CborUtils; + + +public class AuthenticatorDataParser { + private static final int LENGTH_RPIDHASH = 32; + private static final int TOTAL_LENGTH_HEADER = LENGTH_RPIDHASH + 1 + 4; + + private static final int LENGTH_AAGUID = 16; + + AuthenticatorData fromBytes(byte[] bytes) throws IOException { + ByteBuffer buf = ByteBuffer.wrap(bytes).duplicate(); + buf.order(ByteOrder.BIG_ENDIAN); + + byte[] rpIdHash = new byte[LENGTH_RPIDHASH]; + buf.get(rpIdHash); + byte flags = buf.get(); + int sigCounter = buf.getInt(); + + AttestedCredentialData attestedCredentialData = null; + byte[] extensionData = null; + + boolean hasAttestedCredentialData = (flags & AuthenticatorData.FLAG_ATTESTED_CREDENTIAL_DATA) != 0; + if (hasAttestedCredentialData) { + attestedCredentialData = parseAttestedCredentialData(buf); + } + + boolean hasExtensionData = (flags & AuthenticatorData.FLAG_EXTENSION_DATA) != 0; + if (hasExtensionData) { + extensionData = new byte[buf.remaining()]; + buf.get(extensionData); + } + + return AuthenticatorData.create( + rpIdHash, flags, sigCounter, attestedCredentialData, extensionData); + } + + public byte[] toBytes(AuthenticatorData authenticatorData) { + byte[] attestedCredentialData = serializeAttestedCredentialData(authenticatorData.attestedCredentialData()); + + byte[] extensionData = authenticatorData.extensions(); + int extensionDataLength = authenticatorData.hasExtensionData() && extensionData != null ? extensionData.length : 0; + + ByteBuffer result = ByteBuffer.allocate( + TOTAL_LENGTH_HEADER + attestedCredentialData.length + extensionDataLength); + result.order(ByteOrder.BIG_ENDIAN); + + result.put(authenticatorData.rpIdHash()); + result.put(authenticatorData.flags()); + result.putInt(authenticatorData.sigCounter()); + result.put(attestedCredentialData); + if (extensionData != null) { + result.put(extensionData); + } + + return result.array(); + } + + private byte[] serializeAttestedCredentialData(AttestedCredentialData attestedCredentialData) { + if (attestedCredentialData == null) { + return new byte[0]; + } + short credentialIdLength = (short) attestedCredentialData.credentialId().length; + int credentialPublicKeyLength = attestedCredentialData.credentialPublicKey().length; + ByteBuffer result = ByteBuffer.allocate(LENGTH_AAGUID + 2 + credentialIdLength + credentialPublicKeyLength); + + result.put(attestedCredentialData.aaguid()); + result.putShort(credentialIdLength); + result.put(attestedCredentialData.credentialId()); + result.put(attestedCredentialData.credentialPublicKey()); + + return result.array(); + } + + private static AttestedCredentialData parseAttestedCredentialData(ByteBuffer buf) throws IOException { + byte[] aaguid = new byte[LENGTH_AAGUID]; + buf.get(aaguid); + int credentialIdLength = buf.getShort() & 0xFFFF; + byte[] credentialId = new byte[credentialIdLength]; + buf.get(credentialId); + + try { + ByteArrayInputStream inputStream = new ByteArrayInputStream( + buf.array(), buf.arrayOffset() + buf.position(), buf.remaining()); + DataItem dataItem = new CborDecoder(inputStream).decodeNext(); + byte[] credentialPublicKey = CborUtils.writeCborDataToBytes(dataItem); + buf.position(buf.position() + credentialPublicKey.length); + return AttestedCredentialData.create(aaguid, credentialId, credentialPublicKey); + } catch (CborException e) { + throw new IOException("Error reading CBOR-encoded credential data!", e); + } + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/ConstructCredentialAlg.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/ConstructCredentialAlg.java new file mode 100644 index 0000000..199929c --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/ConstructCredentialAlg.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.webauthn; + + +import java.io.IOException; + +import de.cotech.hw.fido2.PublicKeyCredential; +import de.cotech.hw.fido2.domain.create.AttestationObject; +import de.cotech.hw.fido2.domain.create.AuthenticatorAttestationResponse; +import de.cotech.hw.fido2.domain.create.AuthenticatorData; +import de.cotech.hw.fido2.domain.create.CredentialCreationData; +import de.cotech.hw.fido2.internal.cbor.CborAttestationObjectSerializer; +import de.cotech.hw.fido2.internal.cbor.CborConstants; +import de.cotech.hw.util.HwTimber; + + +public class ConstructCredentialAlg { + private static CborAttestationObjectSerializer + cborAttestationObjectSerializer = new CborAttestationObjectSerializer(); + private static AuthenticatorDataParser authenticatorDataParser = new AuthenticatorDataParser(); + + public PublicKeyCredential publicKeyCredential(CredentialCreationData credentialCreationData) + throws IOException { + switch (credentialCreationData.attestationConveyancePreferenceOption()) { + case NONE: + return none(credentialCreationData); + case INDIRECT: + HwTimber.e("Attestation method 'indirect' is not supported. Falling back to 'none'."); + return none(credentialCreationData); + case DIRECT: + return direct(credentialCreationData); + } + throw new UnsupportedOperationException(); + } + + private PublicKeyCredential none(CredentialCreationData credentialCreationData) + throws IOException { + AttestationObject anonymizedMakeCredentialResponse = + createAnonymizedMakeCredentialResponse(credentialCreationData); + + byte[] rawId = findRawIdFromMakeCredentialResponse(anonymizedMakeCredentialResponse); + AuthenticatorAttestationResponse response = createAuthenticatorAttestationResponse( + anonymizedMakeCredentialResponse, credentialCreationData.clientDataJSONResult()); + + return PublicKeyCredential.create(rawId, response); + } + + private AuthenticatorAttestationResponse createAuthenticatorAttestationResponse( + AttestationObject makeCredentialResponse, + byte[] clientDataJSONResult + ) throws IOException { + byte[] anonymizedAttestationObject = + cborAttestationObjectSerializer.serializeAttestationObject(makeCredentialResponse); + return AuthenticatorAttestationResponse + .create(clientDataJSONResult, anonymizedAttestationObject); + } + + private byte[] findRawIdFromMakeCredentialResponse( + AttestationObject makeCredentialResponse) throws IOException { + AuthenticatorData authenticatorData = authenticatorDataParser.fromBytes(makeCredentialResponse.authData()); + return authenticatorData.attestedCredentialData().credentialId(); + } + + private AttestationObject createAnonymizedMakeCredentialResponse( + CredentialCreationData credentialCreationData + ) throws IOException { + AuthenticatorData authenticatorData = authenticatorDataParser + .fromBytes(credentialCreationData.attestationObjectResult().authData()) + .withEmptyAaguid(); + byte[] anonymizedAuthenticatorData = authenticatorDataParser.toBytes(authenticatorData); + + return AttestationObject.create( + "none", anonymizedAuthenticatorData, CborConstants.EMPTY_MAP_BYTES); + } + + private PublicKeyCredential direct(CredentialCreationData credentialCreationData) + throws IOException { + AttestationObject attestationObject = credentialCreationData.attestationObjectResult(); + byte[] rawId = getRawIdFromAuthenticatorAttestationResponse(attestationObject); + byte[] attestationObjectBytes = new CborAttestationObjectSerializer().serializeAttestationObject(attestationObject); + AuthenticatorAttestationResponse response = AuthenticatorAttestationResponse.create( + credentialCreationData.clientDataJSONResult(), attestationObjectBytes); + return PublicKeyCredential.create(rawId, response); + } + + private static byte[] getRawIdFromAuthenticatorAttestationResponse(AttestationObject attestationObject) + throws IOException { + AuthenticatorData authenticatorData = authenticatorDataParser.fromBytes(attestationObject.authData()); + return authenticatorData.attestedCredentialData().credentialId(); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/WebauthnCommand.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/WebauthnCommand.java new file mode 100644 index 0000000..05d81f5 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/WebauthnCommand.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.webauthn; + + +import android.os.Parcelable; + + +public abstract class WebauthnCommand implements Parcelable { + public abstract WebauthnCommand withClientPin(String pin, boolean lastAttemptOk); +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/WebauthnResponse.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/WebauthnResponse.java new file mode 100644 index 0000000..fd0030d --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/WebauthnResponse.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.webauthn; + + +public interface WebauthnResponse { +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/package-info.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/package-info.java new file mode 100644 index 0000000..7f67546 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/package-info.java @@ -0,0 +1,4 @@ +/** + * The FIDO 2 package of the Hardware Security SDK. + */ +package de.cotech.hw.fido2; \ No newline at end of file diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/ui/GenericFido2SecurityKeyDialogFragment.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/ui/GenericFido2SecurityKeyDialogFragment.java new file mode 100644 index 0000000..8951834 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/ui/GenericFido2SecurityKeyDialogFragment.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.ui; + +import android.content.Context; +import android.os.Bundle; + +import androidx.annotation.NonNull; + +import de.cotech.hw.SecurityKeyManager; +import de.cotech.hw.fido2.Fido2SecurityKey; +import de.cotech.hw.fido2.Fido2SecurityKeyConnectionMode; +import de.cotech.hw.fido2.Fido2SecurityKeyConnectionModeConfig; +import de.cotech.hw.fido2.internal.GenericFido2SecurityKeyDialogPresenter; +import de.cotech.hw.ui.SecurityKeyDialogFragment; +import de.cotech.hw.ui.SecurityKeyDialogOptions; +import de.cotech.hw.ui.internal.SecurityKeyDialogPresenter; + +public class GenericFido2SecurityKeyDialogFragment extends SecurityKeyDialogFragment { + public static final String ARG_FIDO2_CONFIG = "de.cotech.hw.fido2.ARG_FIDO2_CONFIG"; + + public static SecurityKeyDialogFragment newInstance() { + return newInstance(SecurityKeyDialogOptions.builder().build(), Fido2SecurityKeyConnectionModeConfig.getDefaultConfig()); + } + + public static SecurityKeyDialogFragment newInstance(@NonNull SecurityKeyDialogOptions options) { + return newInstance(options, Fido2SecurityKeyConnectionModeConfig.getDefaultConfig()); + } + + public static SecurityKeyDialogFragment newInstance(@NonNull SecurityKeyDialogOptions options, @NonNull Fido2SecurityKeyConnectionModeConfig fido2Config) { + try { + Class.forName("de.cotech.hw.ui.SecurityKeyDialogFragment"); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("You must include the hwsecurity-ui Maven artifact!"); + } + + Bundle args = new Bundle(); + args.putParcelable(GenericFido2SecurityKeyDialogFragment.ARG_DIALOG_OPTIONS, options); + args.putParcelable(GenericFido2SecurityKeyDialogFragment.ARG_FIDO2_CONFIG, fido2Config); + + GenericFido2SecurityKeyDialogFragment fragment = new GenericFido2SecurityKeyDialogFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void initSecurityKeyConnectionMode(Bundle arguments) { + Fido2SecurityKeyConnectionModeConfig fido2Config = arguments.getParcelable(ARG_FIDO2_CONFIG); + SecurityKeyManager.getInstance().registerCallback(new Fido2SecurityKeyConnectionMode(fido2Config), this, this); + } + + @Override + public SecurityKeyDialogPresenter initPresenter(SecurityKeyDialogPresenter.View view, Context context, SecurityKeyDialogOptions options) { + return new GenericFido2SecurityKeyDialogPresenter(this, getActivity(), options); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/ui/WebauthnDialogFragment.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/ui/WebauthnDialogFragment.java new file mode 100644 index 0000000..7e20394 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/ui/WebauthnDialogFragment.java @@ -0,0 +1,814 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.ui; + + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.ActivityInfo; +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.Guideline; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.transition.TransitionManager; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.button.MaterialButton; + +import java.io.IOException; + +import de.cotech.hw.SecurityKeyCallback; +import de.cotech.hw.SecurityKeyException; +import de.cotech.hw.SecurityKeyManager; +import de.cotech.hw.exceptions.SecurityKeyDisconnectedException; +import de.cotech.hw.fido2.Fido2SecurityKey; +import de.cotech.hw.fido2.Fido2SecurityKeyConnectionMode; +import de.cotech.hw.fido2.Fido2SecurityKeyConnectionModeConfig; +import de.cotech.hw.fido2.PublicKeyCredential; +import de.cotech.hw.fido2.PublicKeyCredentialCreate; +import de.cotech.hw.fido2.PublicKeyCredentialGet; +import de.cotech.hw.ui.R; +import de.cotech.hw.fido2.WebauthnCallback; +import de.cotech.hw.fido2.domain.UserVerificationRequirement; +import de.cotech.hw.fido2.exceptions.FidoClientPinBlockedException; +import de.cotech.hw.fido2.exceptions.FidoClientPinInvalidException; +import de.cotech.hw.fido2.exceptions.FidoClientPinLastAttemptException; +import de.cotech.hw.fido2.exceptions.FidoClientPinNotSetException; +import de.cotech.hw.fido2.exceptions.FidoClientPinNotSupportedException; +import de.cotech.hw.fido2.exceptions.FidoClientPinRequiredException; +import de.cotech.hw.fido2.exceptions.FidoClientPinTooShortException; +import de.cotech.hw.fido2.exceptions.FidoInvalidCredentialException; +import de.cotech.hw.fido2.exceptions.FidoResidentKeyNoCredentialException; +import de.cotech.hw.fido2.internal.webauthn.WebauthnCommand; +import de.cotech.hw.secrets.ByteSecret; +import de.cotech.hw.ui.internal.ErrorView; +import de.cotech.hw.ui.internal.KeyboardPinInput; +import de.cotech.hw.ui.internal.KeyboardPreferenceRepository; +import de.cotech.hw.ui.internal.KeypadPinInput; +import de.cotech.hw.ui.internal.NfcFullscreenView; +import de.cotech.hw.ui.internal.PinInput; +import de.cotech.hw.ui.internal.SecurityKeyFormFactor; +import de.cotech.hw.ui.internal.SmartcardFormFactor; +import de.cotech.hw.util.HwTimber; + + +public class WebauthnDialogFragment extends BottomSheetDialogFragment + implements SecurityKeyCallback, SecurityKeyFormFactor.SelectTransportCallback, PinInput.PinInputCallback { + private static final String FRAGMENT_TAG = "hwsecurity-webauthn-fragment"; + private static final String ARG_WEBAUTHN_COMMAND = "ARG_WEBAUTHN_COMMAND"; + private static final String ARG_WEBAUTHN_OPTIONS = "de.cotech.hw.fido.ARG_WEBAUTHN_OPTIONS"; + + private static final long TIME_DELAYED_SCREEN_CHANGE = 3000; + + static { + AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); + } + + private OnMakeCredentialCallback onMakeCredentialCallback; + private OnGetAssertionCallback onGetAssertionCallback; + + private CoordinatorLayout coordinator; + private FrameLayout bottomSheet; + private ConstraintLayout innerBottomSheet; + private Guideline guidelineForceHeight; + + private MaterialButton buttonLeft; + private MaterialButton buttonRight; + private MaterialButton buttonPinInputSwitch; + + private SecurityKeyFormFactor securityKeyFormFactor; + private SmartcardFormFactor smartcardFormFactor; + + private KeypadPinInput keypadPinInput; + private KeyboardPinInput keyboardPinInput; + private PinInput currentPinInput; + + private TextView textTitle; + private TextView textDescription; + + private ErrorView errorView; + + private WebauthnDialogOptions options; + private WebauthnCommand webauthnCommand; + + private NfcFullscreenView nfcFullscreenView; + private ByteSecret currentClientPin; + + private KeyboardPreferenceRepository keyboardPreferenceRepository; + + private enum Screen { + START_SECURITY_KEY, + START_ENTER_PIN, + START_ENTER_PIN_SKIP, + USB_INSERT, + USB_PRESS_BUTTON, + USB_SELECT_AND_PRESS_BUTTON, + ERROR, + } + + private Screen currentScreen; + + public void setOnMakeCredentialCallback(OnMakeCredentialCallback onMakeCredentialCallback) { + this.onMakeCredentialCallback = onMakeCredentialCallback; + } + + public void setOnGetAssertionCallback(OnGetAssertionCallback onGetAssertionCallback) { + this.onGetAssertionCallback = onGetAssertionCallback; + } + + public interface OnMakeCredentialCallback { + @UiThread + void onMakeCredentialResponse(@NonNull PublicKeyCredential publicKeyCredential); + + @UiThread + default void onMakeCredentialCancel() { + } + + @UiThread + default void onMakeCredentialTimeout() { + } + } + + public interface OnGetAssertionCallback { + @UiThread + void onGetAssertionResponse(@NonNull PublicKeyCredential response); + + @UiThread + default void onGetAssertionCancel() { + } + + @UiThread + default void onGetAssertionTimeout() { + } + } + + public static WebauthnDialogFragment newInstance(@NonNull WebauthnCommand webauthnCommand, @NonNull WebauthnDialogOptions options) { + Bundle args = new Bundle(); + args.putParcelable(ARG_WEBAUTHN_COMMAND, webauthnCommand); + args.putParcelable(ARG_WEBAUTHN_OPTIONS, options); + + WebauthnDialogFragment fragment = new WebauthnDialogFragment(); + fragment.setArguments(args); + return fragment; + } + + public static WebauthnDialogFragment newInstance(@NonNull WebauthnCommand webauthnCommand) { + return newInstance(webauthnCommand, WebauthnDialogOptions.builder().build()); + } + + public void show(FragmentManager fragmentManager) { + Fragment fragment = fragmentManager.findFragmentByTag(FRAGMENT_TAG); + if (fragment == null) { + show(fragmentManager, FRAGMENT_TAG); + } + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState); + + dialog.setOnShowListener(d -> { + BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) d; + + coordinator = bottomSheetDialog.findViewById(com.google.android.material.R.id.coordinator); + bottomSheet = bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet); + + if (bottomSheet == null) { + throw new IllegalStateException("bottomSheet is null"); + } + + // never just "peek", always fully expand the bottom sheet + BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet); + bottomSheetBehavior.setSkipCollapsed(true); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + }); + + return dialog; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Bundle arguments = getArguments(); + if (arguments == null) { + throw new IllegalStateException("Do not create this dialog directly, use static .newInstance() methods!"); + } + options = arguments.getParcelable(ARG_WEBAUTHN_OPTIONS); + if (options == null) { + throw new IllegalStateException("Do not create this dialog directly, use static .newInstance() methods!"); + } + + setStyle(STYLE_NORMAL, options.getTheme()); + + Context context = getContext(); + if (onMakeCredentialCallback == null) { + if (context instanceof OnMakeCredentialCallback) { + setOnMakeCredentialCallback((OnMakeCredentialCallback) context); + } + } + + if (onGetAssertionCallback == null) { + if (context instanceof OnGetAssertionCallback) { + setOnGetAssertionCallback((OnGetAssertionCallback) context); + } + } + + if (onMakeCredentialCallback == null && onGetAssertionCallback == null) { + if (savedInstanceState != null) { + HwTimber.e("Dismissing WebAuthnDialogFragment left without callbacks after configuration change!"); + dismiss(); + return; + } + throw new IllegalStateException("Activity must implement WebAuthnDialogFragment.onMakeCredentialCallback " + + "or WebAuthnDialogFragment.onGetAssertionCallback!"); + } + + Fido2SecurityKeyConnectionModeConfig config = Fido2SecurityKeyConnectionModeConfig.builder() + .setForceU2f(options.isForceU2f()) + .build(); + SecurityKeyManager.getInstance().registerCallback( + Fido2SecurityKeyConnectionMode.getInstance(config), this, this); + + keyboardPreferenceRepository = new KeyboardPreferenceRepository(context); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.hwsecurity_security_key_dialog, container, false); + } + + @Override + public void onDetach() { + super.onDetach(); + FragmentActivity activity = getActivity(); + if (activity != null) { + int changingConfigurations = activity.getChangingConfigurations(); + int keyboardChanges = ActivityInfo.CONFIG_KEYBOARD | ActivityInfo.CONFIG_KEYBOARD_HIDDEN; + boolean isKeyboardConfigChange = (changingConfigurations & keyboardChanges) != 0; + if (isKeyboardConfigChange) { + HwTimber.e("Activity is recreated due to a keyboard config change, which may cause UI flickering!\n" + + "To fix this issue, the Activity's configChanges attribute " + + "in AndroidManifest.xml should include keyboard|keyboardHidden"); + } + } + } + + private void initTimeout(long timeoutMs) { + final Handler handler = new Handler(); + handler.postDelayed(() -> { + HwTimber.d("Timeout after %s milliseconds.", timeoutMs); + + errorView.setText(R.string.hwsecurity_fido_error_timeout); + gotoScreen(Screen.ERROR); + bottomSheet.postDelayed(() -> { + if (!isAdded()) { + return; + } + dismissAllowingStateLoss(); + + if (webauthnCommand instanceof PublicKeyCredentialCreate) { + onMakeCredentialCallback.onMakeCredentialTimeout(); + } else if (webauthnCommand instanceof PublicKeyCredentialGet) { + onGetAssertionCallback.onGetAssertionTimeout(); + } + }, TIME_DELAYED_SCREEN_CHANGE); + }, timeoutMs); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + Bundle arguments = getArguments(); + if (arguments == null) { + throw new IllegalStateException("Do not create this dialog directly, use static .newInstance() methods!"); + } + webauthnCommand = arguments.getParcelable(ARG_WEBAUTHN_COMMAND); + + if (webauthnCommand instanceof PublicKeyCredentialCreate && onMakeCredentialCallback == null) { + throw new IllegalStateException("Activity must implement onMakeCredentialCallback to perform makeCredential " + + "operation with WebAuthnDialogFragment!"); + } + if (webauthnCommand instanceof PublicKeyCredentialGet && onGetAssertionCallback == null) { + throw new IllegalStateException("Activity must implement onGetAssertionCallback to perform getAssertion " + + "operation with WebAuthnDialogFragment!"); + } + + if (options.getPreventScreenshots()) { + // prevent screenshots + getDialog().getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + } + + if (options.getTimeoutMs() != null) { + initTimeout(options.getTimeoutMs()); + } + + innerBottomSheet = view.findViewById(R.id.hwSecurityDialogBottomSheet); + guidelineForceHeight = view.findViewById(R.id.guidelineForceHeight); + buttonLeft = view.findViewById(R.id.buttonLeft); + buttonRight = view.findViewById(R.id.buttonRight); + buttonPinInputSwitch = view.findViewById(de.cotech.hw.ui.R.id.buttonKeyboardSwitch); + buttonPinInputSwitch.setOnClickListener(v -> switchBetweenPinInputs()); + + textTitle = view.findViewById(R.id.textTitle); + textDescription = view.findViewById(R.id.textDescription); + + smartcardFormFactor = new SmartcardFormFactor(view.findViewById(R.id.includeSmartcardFormFactor), this); + securityKeyFormFactor = new SecurityKeyFormFactor(view.findViewById(R.id.includeSecurityKeyFormFactor), this, this, innerBottomSheet, options.getShowSdkLogo()); + + errorView = new ErrorView(view.findViewById(de.cotech.hw.ui.R.id.includeError)); + + nfcFullscreenView = new NfcFullscreenView(view.findViewById(de.cotech.hw.ui.R.id.includeNfcFullscreen), innerBottomSheet); + + keypadPinInput = new KeypadPinInput(view.findViewById(de.cotech.hw.ui.R.id.includeKeypadInput)); + keypadPinInput.reset(null); + keypadPinInput.setPinInputCallback(this); + + keyboardPinInput = new KeyboardPinInput(view.findViewById(de.cotech.hw.ui.R.id.includeKeyboardInput)); + keyboardPinInput.setPinInputCallback(this); + + buttonLeft.setOnClickListener(v -> getDialog().cancel()); + + gotoScreenOnCreate(); + } + + public void switchBetweenPinInputs() { + boolean isKeyboardPreferred = keyboardPreferenceRepository.isKeyboardPreferred(); + + if (isKeyboardPreferred) { + keyboardPreferenceRepository.setIsKeyboardPreferred(false); + showKeypadPinInput(); + } else { + keyboardPreferenceRepository.setIsKeyboardPreferred(true); + showKeyboardPinInput(); + } + } + + private void showKeyboardPinInput() { + currentPinInput = keyboardPinInput; + + keyboardPinInput.setVisibility(View.VISIBLE); + keypadPinInput.setVisibility(View.GONE); + buttonPinInputSwitch.setIcon(getResources().getDrawable(de.cotech.hw.ui.R.drawable.hwsecurity_ic_keyboard_numeric)); + keyboardPinInput.openKeyboard(); + } + + private void showKeypadPinInput() { + currentPinInput = keypadPinInput; + + keyboardPinInput.setVisibility(View.GONE); + keypadPinInput.setVisibility(View.VISIBLE); + buttonPinInputSwitch.setIcon(getResources().getDrawable(de.cotech.hw.ui.R.drawable.hwsecurity_ic_keyboard_alphabetical)); + } + + @Override + public void onPinEntered(ByteSecret pinSecret) { + if (pinSecret == null) { + gotoErrorScreenAndDelayedScreen(getString(R.string.hwsecurity_fido_error_pin_required), + Screen.ERROR, Screen.START_ENTER_PIN); + return; + } + + currentClientPin = pinSecret; + switch (currentScreen) { + case START_ENTER_PIN_SKIP: + case START_ENTER_PIN: { + gotoScreen(Screen.START_SECURITY_KEY); + break; + } + default: { + // do nothing + break; + } + } + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + if (webauthnCommand instanceof PublicKeyCredentialCreate) { + onMakeCredentialCallback.onMakeCredentialCancel(); + } else if (webauthnCommand instanceof PublicKeyCredentialGet) { + onGetAssertionCallback.onGetAssertionCancel(); + } + } + + private String getStartTitle() { + if (options.getTitle() != null) { + return options.getTitle(); + } else if (webauthnCommand instanceof PublicKeyCredentialCreate) { + return getResources().getString(R.string.hwsecurity_fido_title_default_register); + } else if (webauthnCommand instanceof PublicKeyCredentialGet) { + return getResources().getString(R.string.hwsecurity_fido_title_default_authenticate); + } else { + return ""; + } + } + + private void gotoScreenOnCreate() { + UserVerificationRequirement uvr; + if (webauthnCommand instanceof PublicKeyCredentialGet) { + uvr = ((PublicKeyCredentialGet) webauthnCommand).options().userVerification(); + } else if (webauthnCommand instanceof PublicKeyCredentialCreate) { + uvr = ((PublicKeyCredentialCreate) webauthnCommand).options().authenticatorSelection().userVerification(); + } else { + throw new IllegalStateException("Expected either PublicKeyCredentialGet or PublicKeyCredentialCreate command type!"); + } + + // - if explicitly required, ask for pin and don't allow skipping + // - if preferred, ask for pin but allow "Skip" + // - if discouraged (i.e. otherwise), don't ask for pin + if (uvr == UserVerificationRequirement.REQUIRED) { + gotoScreen(Screen.START_ENTER_PIN); + } else if (uvr == UserVerificationRequirement.PREFERRED) { + gotoScreen(Screen.START_ENTER_PIN_SKIP); + } else { + gotoScreen(Screen.START_SECURITY_KEY); + } + } + + private void gotoScreen(Screen newScreen) { + switch (newScreen) { + case START_SECURITY_KEY: { + animateStart(); + SecurityKeyManager.getInstance().rediscoverConnectedSecurityKeys(); + break; + } + case START_ENTER_PIN: { + showPinInput(false); + break; + } + case START_ENTER_PIN_SKIP: { + boolean allowSkipPin = options.getAllowSkipPin(); + showPinInput(allowSkipPin); + break; + } + case USB_INSERT: { + buttonPinInputSwitch.setVisibility(View.GONE); + securityKeyFormFactor.animateSelectUsb(); + break; + } + case USB_PRESS_BUTTON: { + buttonPinInputSwitch.setVisibility(View.GONE); + securityKeyFormFactor.animateUsbPressButton(); + break; + } + case USB_SELECT_AND_PRESS_BUTTON: { + buttonPinInputSwitch.setVisibility(View.GONE); + securityKeyFormFactor.animateSelectUsbAndPressButton(); + break; + } + case ERROR: { + showError(); + break; + } + } + currentScreen = newScreen; + } + + @Override + public void screeFullscreenNfc() { + textTitle.setVisibility(View.GONE); + textDescription.setVisibility(View.GONE); + errorView.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.GONE); + smartcardFormFactor.setVisibility(View.GONE); + buttonRight.setVisibility(View.GONE); + keyboardPinInput.setVisibility(View.GONE); + keypadPinInput.setVisibility(View.GONE); + + nfcFullscreenView.setVisibility(View.VISIBLE); + nfcFullscreenView.animateNfcFullscreen(getDialog()); + } + + @Override + public void onSecurityKeyFormFactorClickUsb() { + currentScreen = Screen.USB_INSERT; + } + + private void animateStart() { + buttonLeft.setText(R.string.hwsecurity_ui_button_cancel); + textTitle.setText(getStartTitle()); + switch (options.getFormFactor()) { + case SMART_CARD: { + textDescription.setText(""); + break; + } + case SECURITY_KEY: { + textDescription.setText(R.string.hwsecurity_ui_description_start); + break; + } + } + + buttonRight.setVisibility(View.GONE); + buttonPinInputSwitch.setVisibility(View.GONE); + textTitle.setVisibility(View.VISIBLE); + textDescription.setVisibility(View.VISIBLE); + errorView.setVisibility(View.GONE); + keyboardPinInput.setVisibility(View.GONE); + keypadPinInput.setVisibility(View.GONE); + switch (options.getFormFactor()) { + case SMART_CARD: { + smartcardFormFactor.setVisibility(View.VISIBLE); + securityKeyFormFactor.setVisibility(View.GONE); + break; + } + case SECURITY_KEY: { + smartcardFormFactor.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.VISIBLE); + break; + } + } + } + + public void showPinInput(boolean allowSkip) { + buttonLeft.setText(R.string.hwsecurity_ui_button_cancel); + textTitle.setText(getStartTitle()); + textDescription.setText(R.string.hwsecurity_ui_description_enter_pin); + + textTitle.setVisibility(View.VISIBLE); + textDescription.setVisibility(View.VISIBLE); + errorView.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.GONE); + smartcardFormFactor.setVisibility(View.GONE); + buttonPinInputSwitch.setVisibility(options.getAllowKeyboard() ? View.VISIBLE : View.INVISIBLE); + + if (allowSkip) { + buttonRight.setText(R.string.hwsecurity_fido_button_pin_skip); + buttonRight.setOnClickListener(view -> gotoScreen(Screen.START_SECURITY_KEY)); + buttonRight.setVisibility(View.VISIBLE); + } else { + buttonRight.setVisibility(View.INVISIBLE); + } + + boolean isKeyboardPreferred = keyboardPreferenceRepository.isKeyboardPreferred(); + + keypadPinInput.reset(null); + if (isKeyboardPreferred) { + showKeyboardPinInput(); + } else { + showKeypadPinInput(); + } + } + + private void showError() { + TransitionManager.beginDelayedTransition(innerBottomSheet); + textTitle.setVisibility(View.GONE); + textDescription.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.GONE); + smartcardFormFactor.setVisibility(View.GONE); + nfcFullscreenView.setVisibility(View.GONE); + errorView.setVisibility(View.VISIBLE); + buttonRight.setVisibility(View.GONE); + keyboardPinInput.setVisibility(View.GONE); + keypadPinInput.setVisibility(View.GONE); + buttonPinInputSwitch.setVisibility(View.GONE); + } + + @SuppressLint("WrongThread") + @UiThread + @Override + public void onSecurityKeyDiscovered(@NonNull Fido2SecurityKey securityKey) { + switch (currentScreen) { + case START_ENTER_PIN_SKIP: + // fall-through + case START_ENTER_PIN: { + // Only stay in this screen for USB. Automatically proceed with entered PIN on NFC connection + if (!securityKey.isTransportNfc()) { + break; + } + currentPinInput.confirmPin(); + // fall-through + } + case START_SECURITY_KEY: { + if (securityKey.isTransportUsb()) { + gotoScreen(Screen.USB_SELECT_AND_PRESS_BUTTON); + } + sendWebAuthnCommands(securityKey); + break; + } + case USB_INSERT: { + if (securityKey.isTransportUsb()) { + gotoScreen(Screen.USB_PRESS_BUTTON); + } + sendWebAuthnCommands(securityKey); + break; + } + default: { + HwTimber.d("onSecurityKeyDiscovered unhandled screen: %s", currentScreen.name()); + } + } + } + + private void sendWebAuthnCommands(@NonNull Fido2SecurityKey securityKey) { + if (currentClientPin != null) { + String clientPin = new String(currentClientPin.getByteCopyAndClear()); + currentClientPin = null; + + // TODO: implement UI for last PIN attempt + webauthnCommand = webauthnCommand.withClientPin(clientPin, true); + } + + if (webauthnCommand instanceof PublicKeyCredentialCreate) { + securityKey.webauthnCommandAsync(webauthnCommand, + new WebauthnCallback() { + @Override + public void onResponse(PublicKeyCredential publicKeyCredential) { + onMakeCredentialCallback.onMakeCredentialResponse(publicKeyCredential); + if (securityKey.isTransportNfc()) { + securityKey.release(); + } + dismiss(); + } + + @Override + public void onIoException(IOException e) { + handleErrorPublicKeyCredentialCreateError(e); + } + }, this); + } + + if (webauthnCommand instanceof PublicKeyCredentialGet) { + securityKey.webauthnCommandAsync(webauthnCommand, + new WebauthnCallback() { + @Override + public void onResponse(PublicKeyCredential response) { + onGetAssertionCallback.onGetAssertionResponse(response); + if (securityKey.isTransportNfc()) { + securityKey.release(); + } + dismiss(); + } + + @Override + public void onIoException(IOException e) { + handleErrorPublicKeyCredentialGet(e); + } + }, this); + } + } + + private void handleErrorPublicKeyCredentialGet(IOException exception) { + try { + throw exception; + } catch (FidoClientPinNotSetException e) { + errorView.setText(R.string.hwsecurity_fido_error_pin_not_set); + gotoScreen(Screen.ERROR); + } catch (FidoClientPinInvalidException e) { + // if invalid pin was given + // attempts left in ((FidoClientPinInvalidException) e).retriesLeft + // if zero, key is now blocked + gotoErrorScreenAndDelayedScreen(getString(R.string.hwsecurity_fido_error_wrong_pin, e.getRetriesLeft()), + Screen.ERROR, Screen.START_ENTER_PIN); + } catch (FidoClientPinLastAttemptException e) { + // thrown if only one attempt left, but "lastAttemptOk" above was false. + // "hold again to make attempt" or something? + // alternatively, just set lastAttemptOk to true and don't bother :) + // TODO + } catch (FidoClientPinBlockedException e) { + // fido key was already blocked, and must be reset + errorView.setText(R.string.hwsecurity_fido_error_blocked); + gotoScreen(Screen.ERROR); + } catch (FidoClientPinNotSupportedException e) { + // if UV was requested, but not supported by authenticator + gotoErrorScreenAndDelayedScreen(getString(R.string.hwsecurity_fido_error_pin_not_supported), + Screen.ERROR, Screen.START_ENTER_PIN_SKIP); + } catch (FidoClientPinRequiredException e) { + // if UV was requested, but no PIN provided (shouldn't happen) + gotoErrorScreenAndDelayedScreen(getString(R.string.hwsecurity_fido_error_pin_required), + Screen.ERROR, Screen.START_ENTER_PIN); + } catch (FidoResidentKeyNoCredentialException e) { + // if resident key was requested, but none stored for domain on FIDO key + gotoErrorScreenAndDelayedScreen(getString(R.string.hwsecurity_fido_error_wrong_security_key), + Screen.ERROR, Screen.START_ENTER_PIN_SKIP); + } catch (FidoInvalidCredentialException e) { + // if credential isn't valid (probably means wrong authenticator?) + gotoErrorScreenAndDelayedScreen(getString(R.string.hwsecurity_fido_error_wrong_security_key), + Screen.ERROR, Screen.START_ENTER_PIN_SKIP); + } catch (FidoClientPinTooShortException e) { + gotoErrorScreenAndDelayedScreen(getString(R.string.hwsecurity_fido_error_pin_too_short), + Screen.ERROR, Screen.START_ENTER_PIN_SKIP); + } catch (SecurityKeyException e) { + errorView.setText(getString(R.string.hwsecurity_fido_error_internal, e.getShortErrorName())); + gotoScreen(Screen.ERROR); + } catch (SecurityKeyDisconnectedException e) { + // do nothing if we loose connection + } catch (IOException e) { + errorView.setText(getString(R.string.hwsecurity_fido_error_internal, e.getMessage())); + gotoScreen(Screen.ERROR); + } + } + + private void handleErrorPublicKeyCredentialCreateError(IOException exception) { + try { + throw exception; + } catch (FidoClientPinNotSetException e) { + errorView.setText(R.string.hwsecurity_fido_error_pin_not_set); + gotoScreen(Screen.ERROR); + } catch (FidoClientPinInvalidException e) { + // if invalid pin was given + // attempts left in ((FidoClientPinInvalidException) e).retriesLeft + // if zero, key is now blocked + gotoErrorScreenAndDelayedScreen(getString(R.string.hwsecurity_fido_error_wrong_pin, e.getRetriesLeft()), + Screen.ERROR, Screen.START_ENTER_PIN); + } catch (FidoClientPinLastAttemptException e) { + // thrown if only one attempt left, but "lastAttemptOk" above was false. + // "hold again to make attempt" or something? + // alternatively, just set lastAttemptOk to true and don't bother :) + // TODO + } catch (FidoClientPinBlockedException e) { + // fido key was already blocked, and must be reset + errorView.setText(R.string.hwsecurity_fido_error_blocked); + gotoScreen(Screen.ERROR); + } catch (FidoClientPinRequiredException e) { + // if authenticator has a PIN set, but none was given. once a PIN + // is set, MakeCredential *always* requires it. + gotoErrorScreenAndDelayedScreen(getString(R.string.hwsecurity_fido_error_pin_required), + Screen.ERROR, Screen.START_ENTER_PIN); + } catch (FidoClientPinTooShortException e) { + gotoErrorScreenAndDelayedScreen(getString(R.string.hwsecurity_fido_error_pin_too_short), + Screen.ERROR, Screen.START_ENTER_PIN_SKIP); + } catch (SecurityKeyException e) { + errorView.setText(getString(R.string.hwsecurity_fido_error_internal, e.getShortErrorName())); + gotoScreen(Screen.ERROR); + } catch (SecurityKeyDisconnectedException e) { + // do nothing if we loose connection + } catch (IOException e) { + errorView.setText(getString(R.string.hwsecurity_fido_error_internal, e.getMessage())); + gotoScreen(Screen.ERROR); + } + } + + @Override + public void onSecurityKeyDisconnected(@NonNull Fido2SecurityKey securityKey) { + HwTimber.d("onSecurityKeyDisconnected"); + + switch (currentScreen) { + case USB_PRESS_BUTTON: + case USB_SELECT_AND_PRESS_BUTTON: + gotoScreen(Screen.START_SECURITY_KEY); + default: + HwTimber.d("onSecurityKeyDisconnected unhandled screen: %s", currentScreen.name()); + } + } + + @Override + public void onSecurityKeyDiscoveryFailed(@NonNull IOException exception) { + handleErrorPublicKeyCredentialCreateError(exception); + } + + private void gotoErrorScreenAndDelayedScreen(String text, Screen errorScreen, Screen delayedScreen) { + errorView.setText(text); + gotoScreen(errorScreen); + bottomSheet.postDelayed(() -> { + if (!isAdded()) { + return; + } + gotoScreen(delayedScreen); + }, TIME_DELAYED_SCREEN_CHANGE); + } + +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/ui/WebauthnDialogOptions.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/ui/WebauthnDialogOptions.java new file mode 100644 index 0000000..c893601 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/ui/WebauthnDialogOptions.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.ui; + +import android.os.Parcelable; + +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; + +import com.google.auto.value.AutoValue; + +import de.cotech.hw.ui.R; + +@AutoValue +public abstract class WebauthnDialogOptions implements Parcelable { + + public enum FormFactor { + SECURITY_KEY, + SMART_CARD + } + + @Nullable + public abstract String getTitle(); + + @Nullable + public abstract Long getTimeoutMs(); + + public abstract boolean getPreventScreenshots(); + + @StyleRes + public abstract int getTheme(); + + public abstract boolean isForceU2f(); + + public abstract FormFactor getFormFactor(); + + public abstract boolean getAllowKeyboard(); + + public abstract boolean getAllowSkipPin(); + + public abstract boolean getShowSdkLogo(); + + // default values + public static Builder builder() { + return new AutoValue_WebauthnDialogOptions.Builder() + .setPreventScreenshots(false) + .setForceU2f(false) + .setAllowKeyboard(false) + .setAllowSkipPin(false) + .setShowSdkLogo(false) + .setFormFactor(FormFactor.SECURITY_KEY) + .setTheme(R.style.HwSecurity_Dialog); + } + + @AutoValue.Builder + public abstract static class Builder { + /** + * Title shown inside the dialog + *

+ * Default: "Register your Security Key" or "Login with your Security Key" + */ + public abstract Builder setTitle(String title); + + /** + * Automatically aborts the authentication after a certain time. + * For native Android apps, we do not recommend setting a timeout. + *

+ * Default: No timeout + */ + public abstract Builder setTimeoutMs(Long timeoutMs); + + /** + * Sets FLAG_SECURE on the dialog fragment. + *

+ * This sets the content of the window as 'secure', preventing it from appearing in screenshots, + * screen recordings or from being viewed on non-secure displays. + *

+ * Default: false + */ + public abstract Builder setPreventScreenshots(boolean preventScreenshots); + + /** + * Set your own custom theme for the dialog to change colors: + *

{@code
+         * 
+         * }
+ */ + public abstract Builder setTheme(@StyleRes int theme); + + public abstract Builder setForceU2f(boolean forceU2f); + + /** + * Option to choose the form factor displayed after the PIN input. + *

+ * Default: SECURITY_KEY + */ + public abstract Builder setFormFactor(FormFactor formFactor); + + /** + * Shows a button to switch between numeric keypad and full soft-keyboard. + *

+ * Default: false + */ + public abstract Builder setAllowKeyboard(boolean allowKeyboard); + + /** + * Shows a button to that allows to skip the PIN input when RP sets + * UserVerificationRequirement to "preferred" + *

+ * Default: false + */ + public abstract Builder setAllowSkipPin(boolean allowSkipPin); + + /** + * Shows the Hardware Security SDK Logo with a clickable link + *

+ * Default: false + */ + public abstract Builder setShowSdkLogo(boolean showLogo); + + public abstract WebauthnDialogOptions build(); + } + +} diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/Fido2SecurityKeyTest.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/Fido2SecurityKeyTest.java new file mode 100644 index 0000000..fd1e6d3 --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/Fido2SecurityKeyTest.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2; + + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.Collections; + +import de.cotech.hw.SecurityKeyManagerConfig; +import de.cotech.hw.fido2.domain.AuthenticatorTransport; +import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; +import de.cotech.hw.fido2.domain.PublicKeyCredentialParameters; +import de.cotech.hw.fido2.domain.PublicKeyCredentialRpEntity; +import de.cotech.hw.fido2.domain.PublicKeyCredentialType; +import de.cotech.hw.fido2.domain.PublicKeyCredentialUserEntity; +import de.cotech.hw.fido2.domain.UserVerificationRequirement; +import de.cotech.hw.fido2.domain.create.AttestationConveyancePreference; +import de.cotech.hw.fido2.domain.create.AuthenticatorAttestationResponse; +import de.cotech.hw.fido2.domain.create.AuthenticatorSelectionCriteria; +import de.cotech.hw.fido2.domain.create.PublicKeyCredentialCreationOptions; +import de.cotech.hw.fido2.domain.get.AuthenticatorAssertionResponse; +import de.cotech.hw.fido2.domain.get.PublicKeyCredentialRequestOptions; +import de.cotech.hw.fido2.internal.FakeFido2AppletConnection; +import de.cotech.hw.fido2.internal.async.Fido2AsyncOperationManager; +import de.cotech.hw.fido2.internal.operations.WebauthnSecurityKeyOperationFactory; +import de.cotech.hw.fido2.internal.pinauth.PinAuthCryptoUtil; +import de.cotech.hw.fido2.internal.pinauth.PinProtocolV1; +import de.cotech.hw.fido2.internal.utils.WebsafeBase64; +import de.cotech.hw.util.Hex; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +@SuppressWarnings("WeakerAccess") +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 24) +public class Fido2SecurityKeyTest { + static final String ORIGIN = "https://webauthn.hwsecurity.dev"; + + static final String MAKE_ATTESTATION_REQUEST = "80100000e201a4015820c11f694f75b520b404ae975766483d52eb6e34e7628e6ee28aba967dba520c0502a262696477776562617574686e2e687773656375726974792e646576646e616d656441636d6503a462696450313039383233373233353430393837326469636f6e782868747470733a2f2f706963732e61636d652e636f6d2f30302f702f61426a6a6a707150622e706e67646e616d6578186a6f686e2e702e736d697468406578616d706c652e636f6d6b646973706c61794e616d656d4a6f686e20502e20536d6974680481a263616c672664747970656a7075626c69632d6b6579"; + static final String MAKE_ATTESTATION_RESPONSE = "00a301667061636b65640258c4964491febf301ee4bcb88cb5dc1fa95df27f1643a0c7cef9f71ceef6689e067a41000000026d44ba9bf6ec2e49b9300c8fe920cb730040eb6e2f838e654e6666e8f6f63cc076b3b73eb80b5ccc461c2258f95335447c82f72d1b0bc171d907877a79b07886ea567a7910edfd346d0f4b2200641e425dbea5010203262001215820dbe952bfc071b1115266146a0a212177d0ba205871e5c4e88f3c70ceeafe0e2e2258201fc5f020e608ca048569682b2afad4b444a4a51bc0e97ec7736f81fb3cf9a00703a363616c67266373696758473045022100d262feb0b6576857fe7b78e7144b0ffb287dbe4dbd797f7250a550f063b2917d02206b11aeef426fc94f2a0eea7d8c3736da65bd6f45a1f7a21d3d1bca49331bc14363783563815902c1308202bd308201a5a00302010202042ae76263300d06092a864886f70d01010b0500302e312c302a0603550403132359756269636f2055324620526f6f742043412053657269616c203435373230303633313020170d3134303830313030303030305a180f32303530303930343030303030305a306e310b300906035504061302534531123010060355040a0c0959756269636f20414231223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3127302506035504030c1e59756269636f205532462045452053657269616c203731393830373037353059301306072a8648ce3d020106082a8648ce3d030107034200042a03865e6043d99e11ff10aa25545784bf09af8e6b1e3b321729216f55121a8cd910d399ddc768bdfe4a7bc7e3dabc62e6d2469ff5675b8ffa890cca74869e3fa36c306a302206092b0601040182c40a020415312e332e362e312e342e312e34313438322e312e313013060b2b0601040182e51c0201010404030204303021060b2b0601040182e51c010104041204106d44ba9bf6ec2e49b9300c8fe920cb73300c0603551d130101ff04023000300d06092a864886f70d01010b050003820101007257d03cdcc3e115698490d6f80ef95b53072373d9e64732632b11dcdc778aec6bd5926d07e17b9c5430788de32f4e47b45876ff8c5522029be9387879572331e7277016828a711b39c74fd6c1258bd1e4d9e566d570790347da5235c03f0ffe40b1428e05c6e91799e47554716901eeb88bb057893588ad88aad30d714f2f2fac36d54dbad7109c5b027ab0cab8cf354ef1008a09086ca08696af0cfcdbc18398fcdf02316c2622c95ee622bdd1c00a4789e4a1ccb749a354ba5f27604230beffae06e6bf02fbc015bcd151df35d70a98a8e42dca4488fcf071042188dfea1d5f74b7ea788b2af9c514a9bab94fe7389cda195190b2b1307dbf9118e1a929f89000"; + + static final String GET_ATTESTATION_REQUEST = "80100000ae02a30177776562617574686e2e687773656375726974792e646576025820577e406c1b2b403d4d564151de3eb21acd54e78ac9c11aef53f1a950f90f3b700381a36269645840eb6e2f838e654e6666e8f6f63cc076b3b73eb80b5ccc461c2258f95335447c82f72d1b0bc171d907877a79b07886ea567a7910edfd346d0f4b2200641e425dbe64747970656a7075626c69632d6b65796a7472616e73706f7274738363757362636e666363626c65"; + static final String GET_ATTESTATION_RESPONSE = "00a301a26269645840eb6e2f838e654e6666e8f6f63cc076b3b73eb80b5ccc461c2258f95335447c82f72d1b0bc171d907877a79b07886ea567a7910edfd346d0f4b2200641e425dbe64747970656a7075626c69632d6b6579025825964491febf301ee4bcb88cb5dc1fa95df27f1643a0c7cef9f71ceef6689e067a01000000030358473045022074b37f0ebd2934c74106032f2e15e322cb505b52f624c6e635bc07e5c70528980221009db77c087db7882a9b0343dd0ea3acc9efa08367031ff38a34103f8da870574e9000"; + + static final String GET_ATTESTATION_USERNAMELESS_REQUEST = "8010000000003b02a201747777772e70617373776f72646c6573732e6465760258202365959d0ee11f2435d495bb1ae45fa009791231ca7924014a6c6d57f2ace3320000"; + static final String GET_ATTESTATION_USERNAMELESS_RESPONSE = "00a501a262696450569f7b7441d8a21477c89666de4d04de64747970656a7075626c69632d6b6579025825e628e3d57a75e0a221131be840c2dbd5c6d779c12fad3e631fba871494612d6e010000002a03584730450220571dd10ec977ff31ce1ee58e8eb97391187082a4bcf20016df869037d415cde5022100efe23a62efc504feca19cd6440c180952a39395880c4f568857a5f5e9fa7523804a162696458342028557365726e616d656c6573732075736572206372656174656420617420362f392f323032302031323a33363a323620504d2905039000"; + + static final String CLIENT_PIN_GET_RETRIES = "801000000606a201010201"; + static final String CLIENT_PIN_GET_RETRIES_RESPONSE_EIGHT = "00a103089000"; + static final String CLIENT_PIN_GET_AGREEMENT = "801000000606a201010202"; + static final String CLIENT_PIN_GET_AGREEMENT_RESPONSE = "00a101a501020338182001215820f370174f29f360cff04035c4b46daf2a93468398521ac7fc7bf8f8f986d9e08f2258202ba85af4992d37ef3977eea283b65a00e7c06dc473aeecc24b70f9b9d22d5e8d9000"; + static final byte[] AUTHENTICATOR_KEY_AGREEMENT = Hex.decodeHexOrFail( + "a501020338182001215820f370174f29f360cff04035c4b46daf2a93468398521ac7fc7bf8f8f986d9e08f2258202ba85af4992d37ef3977eea283b65a00e7c06dc473aeecc24b70f9b9d22d5e8d"); + static final byte[] PLATFORM_KEY_AGREEMENT = Hex.decodeHexOrFail( + "a501020326200121582099401f6ffa9446585074d1058578f4c68ab46953ccf8cb7910d8b3c9350e1040225820a3c962cddad166b1fdea8a95a377dbf8efbedd82778a8004b884f0009bd6df9506"); + static final byte[] PIN_HASH_ENC = Hex.decodeHexOrFail("2eb756ae474f7ca032b5111b2eef6959"); + static final byte[] PIN_TOKEN_ENC = Hex.decodeHexOrFail("554df3802226935b2bf49f30aae3096f"); + static final String CLIENT_PIN_GET_TOKEN = "801000006606a40101020503a501020326200121582099401f6ffa9446585074d1058578f4c68ab46953ccf8cb7910d8b3c9350e1040225820a3c962cddad166b1fdea8a95a377dbf8efbedd82778a8004b884f0009bd6df9506502eb756ae474f7ca032b5111b2eef6959"; + static final String CLIENT_PIN_GET_TOKEN_RESPONSE = "00a10250554df3802226935b2bf49f30aae3096f9000"; + public static final String MAKE_ATTESTATION_REQUEST_PIN_AUTH = + "80100000f601a6015820c11f694f75b520b404ae975766483d52eb6e34e7628e6ee28aba967dba520c0502a262696477776562617574686e2e687773656375726974792e646576646e616d656441636d6503a462696450313039383233373233353430393837326469636f6e782868747470733a2f2f706963732e61636d652e636f6d2f30302f702f61426a6a6a707150622e706e67646e616d6578186a6f686e2e702e736d697468406578616d706c652e636f6d6b646973706c61794e616d656d4a6f686e20502e20536d6974680481a263616c672664747970656a7075626c69632d6b65790850313233343536373839404142434546470901"; + + static final byte[] SHARED_SECRET = new byte[1]; + static final byte[] PIN_TOKEN = new byte[2]; + static final byte[] CLIENT_DATA_HASH = Hex.decodeHexOrFail("c11f694f75b520b404ae975766483d52eb6e34e7628e6ee28aba967dba520c05"); + static final byte[] PIN_AUTH = Hex.decodeHexOrFail("31323334353637383940414243454647"); + + public static final byte[] USER_ID = Hex.decodeHexOrFail("31303938323337323335343039383732"); + public static final String USER_NAME = "john.p.smith@example.com"; + public static final String USER_ICON = "https://pics.acme.com/00/p/aBjjjpqPb.png"; + public static final String USER_DISPLAYNAME = "John P. Smith"; + public static final byte[] CREDENTIAL_ID = WebsafeBase64 + .decode("624vg45lTmZm6Pb2PMB2s7c-uAtczEYcIlj5UzVEfIL3LRsLwXHZB4d6ebB4hupWenkQ7f00bQ9LIgBkHkJdvg"); + + Fido2SecurityKey fido2SecurityKey, fido2SecurityKeyWithPin; + FakeFido2AppletConnection fakeFidoConnection, fakeFidoConnectionWithPin; + Fido2AsyncOperationManager fido2AsyncOperationManager; + PinAuthCryptoUtil pinAuthCryptoUtil; + + @Before + public void setup() throws Exception { + fakeFidoConnection = FakeFido2AppletConnection.create(false); + fakeFidoConnectionWithPin = FakeFido2AppletConnection.create(true); + fido2AsyncOperationManager = new Fido2AsyncOperationManager(); + pinAuthCryptoUtil = mock(PinAuthCryptoUtil.class); + WebauthnSecurityKeyOperationFactory operationFactory = new WebauthnSecurityKeyOperationFactory(new PinProtocolV1(pinAuthCryptoUtil)); + + fido2SecurityKey = new Fido2SecurityKey( + new SecurityKeyManagerConfig.Builder().build(), + fakeFidoConnection.connection, null, fido2AsyncOperationManager, + operationFactory); + fido2SecurityKeyWithPin = new Fido2SecurityKey( + new SecurityKeyManagerConfig.Builder().build(), + fakeFidoConnectionWithPin.connection, null, fido2AsyncOperationManager, + operationFactory); + } + + @Test + public void makeCredential() throws Exception { + byte[] challenge = WebsafeBase64.decode("GNxfVQfEVOoi9uU1W_jM-w"); + PublicKeyCredentialCreate createParameters = PublicKeyCredentialCreate.create(ORIGIN, + PublicKeyCredentialCreationOptions.create( + PublicKeyCredentialRpEntity.create("webauthn.hwsecurity.dev", "Acme", null), + PublicKeyCredentialUserEntity.create(USER_ID, USER_NAME, USER_DISPLAYNAME, USER_ICON), + challenge, + Collections.singletonList(PublicKeyCredentialParameters.createDefaultEs256()), + null, + AuthenticatorSelectionCriteria.create(null, false, UserVerificationRequirement.PREFERRED), + null, + AttestationConveyancePreference.NONE + ) + ); + fakeFidoConnection.expect(MAKE_ATTESTATION_REQUEST, MAKE_ATTESTATION_RESPONSE); + + PublicKeyCredential publicKeyCredential = fido2SecurityKey.webauthnCommand(createParameters); + + fakeFidoConnection.verify(); + assertArrayEquals(CREDENTIAL_ID, publicKeyCredential.rawId()); + assertEquals(WebsafeBase64.encodeToString(CREDENTIAL_ID), publicKeyCredential.id()); + assertEquals("public-key", publicKeyCredential.type()); + assertTrue(publicKeyCredential.response() instanceof AuthenticatorAttestationResponse); + assertEquals("{\"type\":\"webauthn.create\",\"origin\":\"https:\\/\\/webauthn.hwsecurity.dev\",\"challenge\":\"GNxfVQfEVOoi9uU1W_jM-w\",\"hashAlgorithm\":\"SHA-256\"}", + new String(publicKeyCredential.response().clientDataJson())); + } + + @Test + public void getAssertion() throws Exception { + byte[] challenge = WebsafeBase64.decode("BCNrbzS9WfmkDbISaw6WQg"); + PublicKeyCredentialGet getParameters = PublicKeyCredentialGet.create(ORIGIN, + PublicKeyCredentialRequestOptions.create( + challenge, + null, + "webauthn.hwsecurity.dev", + Collections.singletonList(PublicKeyCredentialDescriptor.create( + PublicKeyCredentialType.PUBLIC_KEY, CREDENTIAL_ID, Arrays.asList( + AuthenticatorTransport.USB, + AuthenticatorTransport.NFC, + AuthenticatorTransport.BLE + ) + )), UserVerificationRequirement.PREFERRED) + ); + fakeFidoConnection.expect(GET_ATTESTATION_REQUEST, GET_ATTESTATION_RESPONSE); + + PublicKeyCredential publicKeyCredential = fido2SecurityKey.webauthnCommand(getParameters); + + fakeFidoConnection.verify(); + assertArrayEquals(CREDENTIAL_ID, publicKeyCredential.rawId()); + assertEquals(WebsafeBase64.encodeToString(CREDENTIAL_ID), publicKeyCredential.id()); + assertEquals("public-key", publicKeyCredential.type()); + assertTrue(publicKeyCredential.response() instanceof AuthenticatorAssertionResponse); + AuthenticatorAssertionResponse response = (AuthenticatorAssertionResponse) publicKeyCredential.response(); + assertNull(response.userHandle()); + assertEquals("{\"type\":\"webauthn.get\",\"origin\":\"https:\\/\\/webauthn.hwsecurity.dev\",\"challenge\":\"BCNrbzS9WfmkDbISaw6WQg\",\"hashAlgorithm\":\"SHA-256\"}", + new String(publicKeyCredential.response().clientDataJson())); + } + + @Test + public void getAssertion_usernameless() throws Exception { + byte[] challenge = WebsafeBase64.decode("n46bFSgRdToqeoIeef252g"); + PublicKeyCredentialGet getParameters = PublicKeyCredentialGet.create( + "https://www.passwordless.dev", + PublicKeyCredentialRequestOptions.create( + challenge, + null, + "www.passwordless.dev", + Collections.emptyList(), UserVerificationRequirement.PREFERRED) + ); + fakeFidoConnection.expect(GET_ATTESTATION_USERNAMELESS_REQUEST, GET_ATTESTATION_USERNAMELESS_RESPONSE); + + PublicKeyCredential publicKeyCredential = fido2SecurityKey.webauthnCommand(getParameters); + + fakeFidoConnection.verify(); + byte[] rawId = WebsafeBase64.decode("Vp97dEHYohR3yJZm3k0E3g"); + assertArrayEquals(rawId, publicKeyCredential.rawId()); + assertEquals(WebsafeBase64.encodeToString(rawId), publicKeyCredential.id()); + assertEquals("public-key", publicKeyCredential.type()); + assertTrue(publicKeyCredential.response() instanceof AuthenticatorAssertionResponse); + AuthenticatorAssertionResponse response = (AuthenticatorAssertionResponse) publicKeyCredential.response(); + assertEquals("IChVc2VybmFtZWxlc3MgdXNlciBjcmVhdGVkIGF0IDYvOS8yMDIwIDEyOjM2OjI2IFBNKQ", WebsafeBase64.encodeToString(response.userHandle())); + assertEquals("{\"type\":\"webauthn.get\",\"origin\":\"https:\\/\\/www.passwordless.dev\",\"challenge\":\"n46bFSgRdToqeoIeef252g\",\"hashAlgorithm\":\"SHA-256\"}", + new String(publicKeyCredential.response().clientDataJson())); + } + + @Test + public void makeCredential_withPin() throws Exception { + byte[] challenge = WebsafeBase64.decode("GNxfVQfEVOoi9uU1W_jM-w"); + PublicKeyCredentialCreate createParameters = PublicKeyCredentialCreate.create(ORIGIN, + PublicKeyCredentialCreationOptions.create( + PublicKeyCredentialRpEntity.create("webauthn.hwsecurity.dev", "Acme", null), + PublicKeyCredentialUserEntity.create(USER_ID, USER_NAME, USER_DISPLAYNAME, USER_ICON), + challenge, + Collections.singletonList(PublicKeyCredentialParameters.createDefaultEs256()), + null, + AuthenticatorSelectionCriteria.create(null, false, UserVerificationRequirement.PREFERRED), + null, + AttestationConveyancePreference.NONE + ) + ).withClientPin("1234", false); + fakeFidoConnectionWithPin.expect(CLIENT_PIN_GET_RETRIES, CLIENT_PIN_GET_RETRIES_RESPONSE_EIGHT); + fakeFidoConnectionWithPin.expect(CLIENT_PIN_GET_AGREEMENT, CLIENT_PIN_GET_AGREEMENT_RESPONSE); + fakeFidoConnectionWithPin.expect(CLIENT_PIN_GET_TOKEN, CLIENT_PIN_GET_TOKEN_RESPONSE); + fakeFidoConnectionWithPin.expect(MAKE_ATTESTATION_REQUEST_PIN_AUTH, MAKE_ATTESTATION_RESPONSE); + + PublicKey publicKey = mock(PublicKey.class); + PrivateKey privateKey = mock(PrivateKey.class); + KeyPair keyPair = new KeyPair(publicKey, privateKey); + PublicKey authenticatorPublicKey = mock(PublicKey.class); + + when(pinAuthCryptoUtil.generatePlatformKeyPair()).thenReturn(keyPair); + when(pinAuthCryptoUtil.cosePublicKeyFromPublicKey(publicKey)) + .thenReturn(PLATFORM_KEY_AGREEMENT); + when(pinAuthCryptoUtil.publicKeyFromCosePublicKey(aryEq(AUTHENTICATOR_KEY_AGREEMENT))) + .thenReturn(authenticatorPublicKey); + when(pinAuthCryptoUtil.calculatePinHashEnc(aryEq(SHARED_SECRET), eq("1234"))) + .thenReturn(PIN_HASH_ENC); + when(pinAuthCryptoUtil.generateSharedSecret(privateKey, authenticatorPublicKey)) + .thenReturn(SHARED_SECRET); + when(pinAuthCryptoUtil.decryptPinToken(SHARED_SECRET, PIN_TOKEN_ENC)).thenReturn(PIN_TOKEN); + when(pinAuthCryptoUtil.calculatePinAuth(same(PIN_TOKEN), aryEq(CLIENT_DATA_HASH))).thenReturn(PIN_AUTH); + + PublicKeyCredential publicKeyCredential = fido2SecurityKeyWithPin.webauthnCommand(createParameters); + + fakeFidoConnectionWithPin.verify(); + assertArrayEquals(CREDENTIAL_ID, publicKeyCredential.rawId()); + assertEquals(WebsafeBase64.encodeToString(CREDENTIAL_ID), publicKeyCredential.id()); + assertEquals("public-key", publicKeyCredential.type()); + assertTrue(publicKeyCredential.response() instanceof AuthenticatorAttestationResponse); + assertEquals("{\"type\":\"webauthn.create\",\"origin\":\"https:\\/\\/webauthn.hwsecurity.dev\",\"challenge\":\"GNxfVQfEVOoi9uU1W_jM-w\",\"hashAlgorithm\":\"SHA-256\"}", + new String(publicKeyCredential.response().clientDataJson())); + } +} \ No newline at end of file diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/FakeFido2AppletConnection.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/FakeFido2AppletConnection.java new file mode 100644 index 0000000..9c8a727 --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/FakeFido2AppletConnection.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal; + + +import java.io.IOException; + +import de.cotech.hw.internal.transport.FakeTransport; +import org.junit.Assert; + + +@SuppressWarnings("WeakerAccess") +public class FakeFido2AppletConnection { + public static final String GET_VERSION_COMMAND = "00030000"; + public static final String GET_VERSION_RESPONSE = "5532465f56329000"; + public static final String GET_INFO_COMMAND = "801000000104"; + public static final String GET_INFO_RESPONSE_PIN_NO = "00a60182665532465f5632684649444f5f325f3002816b686d61632d73656372657403506d44ba9bf6ec2e49b9300c8fe920cb7304a462726bf5627570f564706c6174f469636c69656e7450696ef4051904b00681019000"; + public static final String GET_INFO_RESPONSE_PIN_YES ="00a60182665532465f5632684649444f5f325f3002816b686d61632d73656372657403506d44ba9bf6ec2e49b9300c8fe920cb7304a462726bf5627570f564706c6174f469636c69656e7450696ef5051904b00681019000"; + public Fido2AppletConnection connection; + FakeTransport fakeTransport; + + FakeFido2AppletConnection(Fido2AppletConnection connection, FakeTransport fakeTransport) { + this.connection = connection; + this.fakeTransport = fakeTransport; + } + + public static FakeFido2AppletConnection create(boolean hasClientPin) throws Exception { + FakeTransport fakeTransport = new FakeTransport(); + Fido2AppletConnection connection = Fido2AppletConnection.getInstanceForTransport(fakeTransport); + + fakeTransport.expect(GET_VERSION_COMMAND, GET_VERSION_RESPONSE); + fakeTransport.expect(GET_INFO_COMMAND, hasClientPin ? GET_INFO_RESPONSE_PIN_YES : GET_INFO_RESPONSE_PIN_NO); + connection.connectIfNecessary(); + + return new FakeFido2AppletConnection(connection, fakeTransport); + } + + public void expect(String command, String reply) throws IOException { + fakeTransport.expect(command, reply); + } + + public void verify() { + fakeTransport.verify(); + } +} diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/async/Fido2AsyncOperationManagerTest.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/async/Fido2AsyncOperationManagerTest.java new file mode 100644 index 0000000..9823c4a --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/async/Fido2AsyncOperationManagerTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.async; + + +import java.util.concurrent.CountDownLatch; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLooper; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 24) +public class Fido2AsyncOperationManagerTest { + + private Fido2AsyncOperationManager fido2AsyncOperationManager; + + @Before + public void setup() { + fido2AsyncOperationManager = new Fido2AsyncOperationManager(); + } + + @Test + public void startAsyncOperation() throws Exception { + TestFido2OperationThread thread = new TestFido2OperationThread(); + fido2AsyncOperationManager.startAsyncOperation(null, thread); + FidoAsyncOperationManagerUtil.joinRunningThread(fido2AsyncOperationManager); + assertTrue(ShadowLooper.getShadowMainLooper().getScheduler().runOneTask()); + thread.assertLatchOk(); + } + + @Test + public void startAsyncOperation_thenClear() throws Exception { + CountDownLatch delayLatch = new CountDownLatch(1); + TestFido2OperationThread thread = new TestFido2OperationThread(delayLatch); + fido2AsyncOperationManager.startAsyncOperation(null, thread); + fido2AsyncOperationManager.clearAsyncOperation(); + FidoAsyncOperationManagerUtil.joinRunningThread(fido2AsyncOperationManager); + assertFalse(ShadowLooper.getShadowMainLooper().getScheduler().runOneTask()); + } + + +} \ No newline at end of file diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/async/Fido2OperationThreadTest.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/async/Fido2OperationThreadTest.java new file mode 100644 index 0000000..54d920e --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/async/Fido2OperationThreadTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.async; + + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLooper; + +import static org.junit.Assert.assertTrue; + + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 24) +public class Fido2OperationThreadTest { + + @Test + public void testThread() throws Exception { + TestFido2OperationThread thread = new TestFido2OperationThread(); + + thread.start(); + thread.join(); + assertTrue(ShadowLooper.getShadowMainLooper().getScheduler().runOneTask()); + + thread.assertLatchOk(); + } + +} \ No newline at end of file diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/async/FidoAsyncOperationManagerUtil.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/async/FidoAsyncOperationManagerUtil.java new file mode 100644 index 0000000..ba8a605 --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/async/FidoAsyncOperationManagerUtil.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.async; + + +public class FidoAsyncOperationManagerUtil { + public static void joinRunningThread(Fido2AsyncOperationManager asyncOperationManager) throws InterruptedException { + Thread thread = asyncOperationManager.asyncOperationThread; + if (thread == null) { + return; + } + thread.join(); + } + +} diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/async/TestFido2OperationThread.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/async/TestFido2OperationThread.java new file mode 100644 index 0000000..8662a80 --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/async/TestFido2OperationThread.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.internal.async; + + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import android.os.Handler; + +import de.cotech.hw.fido2.internal.FakeFido2AppletConnection; +import de.cotech.hw.fido2.internal.Fido2AppletConnection; +import org.mockito.Mockito; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +@SuppressWarnings("unused") +class TestFido2OperationThread extends Fido2OperationThread { + private final CountDownLatch countDownLatch; + private final CountDownLatch delayLatch; + + TestFido2OperationThread() throws Exception { + this(FakeFido2AppletConnection.create(false).connection, new Handler(), null); + } + + TestFido2OperationThread(CountDownLatch delayLatch) throws Exception { + this(FakeFido2AppletConnection.create(false).connection, new Handler(), delayLatch); + } + + private TestFido2OperationThread(Fido2AppletConnection fido2AppletConnection, Handler handler, + CountDownLatch delayLatch) { + super(fido2AppletConnection, handler, 10); + this.countDownLatch = new CountDownLatch(3); + this.delayLatch = delayLatch; + setFido2AsyncOperationManager(Mockito.mock(Fido2AsyncOperationManager.class)); + } + + @Override + void prepareOperation() { + countDownLatch.countDown(); + } + + @Override + Integer performOperation() throws InterruptedException { + if (delayLatch != null) { + delayLatch.await(); + } + countDownLatch.countDown(); + return 5; + } + + @Override + void deliverResponse(Integer response) { + countDownLatch.countDown(); + assertEquals(5, response.intValue()); + } + + @Override + void deliverIoException(IOException e) { + fail(); + } + + void assertLatchOk() { + try { + assertTrue("Timeout waiting for thread!", countDownLatch.await(500, TimeUnit.MILLISECONDS)); + } catch (InterruptedException e) { + fail("got interrupted unexpectedly"); + } + } + + void assertLatchTimeout() { + try { + assertFalse("Thread unexpectedly returned!", countDownLatch.await(500, TimeUnit.MILLISECONDS)); + } catch (InterruptedException e) { + fail("got interrupted unexpectedly"); + } + } +} diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/cose/CosePublicKeyUtilsTest.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/cose/CosePublicKeyUtilsTest.java new file mode 100644 index 0000000..68353d4 --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/cose/CosePublicKeyUtilsTest.java @@ -0,0 +1,28 @@ +package de.cotech.hw.fido2.internal.cose; + + +import de.cotech.hw.util.Hex; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; + + +public class CosePublicKeyUtilsTest { + + private static final byte[] PUBLIC_KEY_X9_62 = Hex.decodeHexOrFail( + "04a64c3f0601c440ce2061186419804fa5c2d505be6976822f8190602ffd125613da39ab5e4079338052a33bd4f05f2b9ce176b2df607e45a93405e77eabb2f1db"); + private static final byte[] PUBLIC_KEY_COSE = Hex.decodeHexOrFail( + "a5010203262001215820a64c3f0601c440ce2061186419804fa5c2d505be6976822f8190602ffd125613225820da39ab5e4079338052a33bd4f05f2b9ce176b2df607e45a93405e77eabb2f1db"); + + @Test + public void encodex9PublicKeyAsCose() throws Exception { + byte[] coseEncodedKey = CosePublicKeyUtils.encodex962PublicKeyAsCose(PUBLIC_KEY_X9_62); + assertArrayEquals(PUBLIC_KEY_COSE, coseEncodedKey); + } + + @Test + public void encodeCosePublicKeyAsX9() throws Exception { + byte[] x9EncodedKey = CosePublicKeyUtils.encodeCosePublicKeyAsX962(PUBLIC_KEY_COSE); + assertArrayEquals(PUBLIC_KEY_X9_62, x9EncodedKey); + } +} \ No newline at end of file diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/crypto/P256Test.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/crypto/P256Test.java new file mode 100644 index 0000000..52f21d9 --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/crypto/P256Test.java @@ -0,0 +1,176 @@ +// Copyright 2017 The CrunchyCrypt Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This class is adapted from Google Crunchy (https://github.com/google/crunchy) + +package de.cotech.hw.fido2.internal.crypto; + + +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.ECPoint; +import java.util.Arrays; + +import de.cotech.hw.util.Hex; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** Unit tests for {@link P256}. */ +@SuppressWarnings("ConstantConditions") +@RunWith(JUnit4.class) +public class P256Test { + @Test + public void testPointOnCurve() throws GeneralSecurityException { + // Valid point + String xHex = "2442A5CC0ECD015FA3CA31DC8E2BBC70BF42D60CBCA20085E0822CB04235E970"; + String yHex = "6FC98BD7E50211A4A27102FA3549DF79EBCB4BF246B80945CDDFE7D509BBFD7D"; + BigInteger x = new BigInteger(1 /* positive */, Hex.decodeHexOrFail(xHex)); + BigInteger y = new BigInteger(1 /* positive */, Hex.decodeHexOrFail(yHex)); + ECPoint point = new ECPoint(x, y); + assertTrue(P256.isPointOnCurve(point)); + + // Invalid point + xHex = "2442A5CC0ECD015FA3CA31DC8E2BBC70BF42D60CBCA20085E0822CB04235E971"; + yHex = "6FC98BD7E50211A4A27102FA3549DF79EBCB4BF246B80945CDDFE7D509BBFD7D"; + x = new BigInteger(1 /* positive */, Hex.decodeHexOrFail(xHex)); + y = new BigInteger(1 /* positive */, Hex.decodeHexOrFail(yHex)); + point = new ECPoint(x, y); + assertFalse(P256.isPointOnCurve(point)); + + // Point at infinity + try { + P256.isPointOnCurve(ECPoint.POINT_INFINITY); + fail(); + } catch (GeneralSecurityException expected) { + assertTrue(expected.getMessage().contains("point is at infinity")); + } + + // Negative x + try { + point = new ECPoint(x.subtract(P256.FIELD.getP()), y); + P256.isPointOnCurve(point); + fail(); + } catch (GeneralSecurityException expected) { + assertTrue(expected.getMessage().contains("x is out of range")); + } + + // Negative y + try { + point = new ECPoint(x, y.subtract(P256.FIELD.getP())); + P256.isPointOnCurve(point); + fail(); + } catch (GeneralSecurityException expected) { + assertTrue(expected.getMessage().contains("y is out of range")); + } + + // Large x + try { + point = new ECPoint(x.add(P256.FIELD.getP()), y); + P256.isPointOnCurve(point); + fail(); + } catch (GeneralSecurityException expected) { + assertTrue(expected.getMessage().contains("x is out of range")); + } + + // Large y + try { + point = new ECPoint(x, y.add(P256.FIELD.getP())); + P256.isPointOnCurve(point); + fail(); + } catch (GeneralSecurityException expected) { + assertTrue(expected.getMessage().contains("y is out of range")); + } + } + + @Test + public void ecdhTest() throws GeneralSecurityException { + // RFC 5114 section A.6. + // https://tools.ietf.org/html/rfc5114.html#appendix-A.6 + { + byte[] publicKey = + Hex.decodeHexOrFail( + "04b120de4aa36492795346e8de6c2c8646ae06aaea279fa775b3ab0715f6ce51b0" + + "9f1b7eece20d7b5ed8ec685fa3f071d83727027092a8411385c34dde5708b2b6"); + byte[] privateKey = + Hex.decodeHexOrFail("814264145f2f56f2e96a8e337a1284993faf432a5abce59e867b7291d507a3af"); + String expectedEcdhResultHex = + "dd0f5396219d1ea393310412d19a08f1f5811e9dc8ec8eea7f80d21c820c2788"; + byte[] computedEcdhResult = P256.ecdh(publicKey, privateKey); + assertEquals(expectedEcdhResultHex, Hex.encodeHexString(computedEcdhResult)); + } + { + byte[] publicKey = + Hex.decodeHexOrFail( + "042af502f3be8952f2c9b5a8d4160d09e97165be50bc42ae4a5e8d3b4ba83aeb15" + + "eb0faf4ca986c4d38681a0f9872d79d56795bd4bff6e6de3c0f5015ece5efd85"); + byte[] privateKey = + Hex.decodeHexOrFail("2ce1788ec197e096db95a200cc0ab26a19ce6bccad562b8eee1b593761cf7f41"); + String expectedEcdhResultHex = + "dd0f5396219d1ea393310412d19a08f1f5811e9dc8ec8eea7f80d21c820c2788"; + byte[] computedEcdhResult = P256.ecdh(publicKey, privateKey); + assertEquals(expectedEcdhResultHex, Hex.encodeHexString(computedEcdhResult)); + } + } + + @Test + public void testSerializePublicKey() throws GeneralSecurityException { + PublicKey key = P256.newKeyPair().getPublic(); + byte[] serialized = P256.serializePublicKey(key); + PublicKey deserialized = P256.deserializePublicKey(serialized); + assertEquals(key, deserialized); + + // Small array + try { + P256.deserializePublicKey(Arrays.copyOfRange(serialized, 0, serialized.length - 1)); + fail(); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("publicKey is the wrong size")); + } + + // Invalid Point + try { + P256.deserializePublicKey( + Hex.decodeHexOrFail( + "042442A5CC0ECD015FA3CA31DC8E2BBC70BF42D60CBCA20085E0822CB04235E971" + + "6FC98BD7E50211A4A27102FA3549DF79EBCB4BF246B80945CDDFE7D509BBFD7D")); + fail(); + } catch (GeneralSecurityException expected) { + assertTrue(expected.getMessage().contains("point is not on the curve")); + } + } + + @Test + public void testSerializePrivateKey() throws GeneralSecurityException { + PrivateKey key = P256.newKeyPair().getPrivate(); + byte[] serialized = P256.serializePrivateKey(key); + PrivateKey deserialized = P256.deserializePrivateKey(serialized); + assertEquals(key, deserialized); + + // Small array + try { + P256.deserializePrivateKey(Arrays.copyOfRange(serialized, 0, serialized.length - 1)); + fail(); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("privateKey is the wrong size")); + } + } +} \ No newline at end of file diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/jsapi/JsonWebauthnOptionsParserTest.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/jsapi/JsonWebauthnOptionsParserTest.java new file mode 100644 index 0000000..941d4ed --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/jsapi/JsonWebauthnOptionsParserTest.java @@ -0,0 +1,36 @@ +package de.cotech.hw.fido2.internal.jsapi; + + +import de.cotech.hw.fido2.domain.create.PublicKeyCredentialCreationOptions; +import de.cotech.hw.fido2.internal.json.JsonWebauthnOptionsParser; +import de.cotech.hw.util.Hex; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + + +@RunWith(RobolectricTestRunner.class) +public class JsonWebauthnOptionsParserTest { + private JsonWebauthnOptionsParser parser = new JsonWebauthnOptionsParser(); + + @Test + public void fromJsonMakeCredential() throws JSONException { + String x = "{\"publicKey\":{\"challenge\":{\"0\":219,\"1\":30,\"2\":239,\"3\":9,\"4\":130,\"5\":70,\"6\":62,\"7\":3,\"8\":35,\"9\":154,\"10\":109,\"11\":159,\"12\":150,\"13\":76,\"14\":216,\"15\":63},\"rp\":{\"name\":\"Acme\"},\"user\":{\"id\":{\"0\":49,\"1\":48,\"2\":57,\"3\":56,\"4\":50,\"5\":51,\"6\":55,\"7\":50,\"8\":51,\"9\":53,\"10\":52,\"11\":48,\"12\":57,\"13\":56,\"14\":55,\"15\":50},\"name\":\"john.p.smith@example.com\",\"displayName\":\"John P. Smith\",\"icon\":\"https://pics.acme.com/00/p/aBjjjpqPb.png\"},\"pubKeyCredParams\":[{\"alg\":-7,\"type\":\"public-key\"}],\"authenticatorSelection\":{\"authenticatorAttachment\":\"cross-platform\",\"requireResidentKey\":false,\"userVerification\":\"preferred\"},\"timeout\":60000,\"excludeCredentials\":[],\"extensions\":{\"exts\":true}}}\n"; + + PublicKeyCredentialCreationOptions authenticatorMakeCredential = + parser.fromOptionsJsonMakeCredential(x); + + // test data from https://webauthn.hwsecurity.dev/driver.js + assertEquals("john.p.smith@example.com", authenticatorMakeCredential.user().name()); + assertEquals("John P. Smith", authenticatorMakeCredential.user().displayName()); + assertEquals("https://pics.acme.com/00/p/aBjjjpqPb.png", authenticatorMakeCredential.user().icon()); + assertEquals(Hex.encodeHexString("1098237235409872".getBytes()), Hex.encodeHexString(authenticatorMakeCredential.user().id())); + assertEquals("Acme", authenticatorMakeCredential.rp().name()); + assertNull(authenticatorMakeCredential.rp().id()); + assertNull(authenticatorMakeCredential.excludeCredentials()); + } +} \ No newline at end of file diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/operations/ctap1/U2FRegisterResponseTest.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/operations/ctap1/U2FRegisterResponseTest.java new file mode 100644 index 0000000..e5d3778 --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/operations/ctap1/U2FRegisterResponseTest.java @@ -0,0 +1,45 @@ +package de.cotech.hw.fido2.internal.operations.ctap1; + + +import de.cotech.hw.util.Hex; +import org.bouncycastle.util.encoders.Base64; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +@RunWith(RobolectricTestRunner.class) +public class U2FRegisterResponseTest { + private byte[] U2F_RESPONSE_DATA = Hex.decodeHexOrFail("050418d25e92eea125f492a6b3aa1cb6ca908a4140cc9ce410c61e05677f7ebc3fc85d1b64d2c6aeb0aebd7fa6d3abf68d54f6bc174ed668809d3f93d49f958ba3ff3037fa0f52c4028cbd61def850e0ef6c2c1bee8929423f6a7e7b0e7919e4767b46a424f5d076d67f5805c43d56db1ec859308202e93082028ea003020102020101300a06082a8648ce3d040302308182310b30090603550406130255533111300f06035504080c084d6172796c616e6431143012060355040a0c0b534f4c4f204841434b45523110300e060355040b0c07526f6f742043413115301306035504030c0c736f6c6f6b6579732e636f6d3121301f06092a864886f70d010901161268656c6c6f40736f6c6f6b6579732e636f6d3020170d3138313231313032323031325a180f32303638313132383032323031325a308194310b30090603550406130255533111300f06035504080c084d6172796c616e6431143012060355040a0c0b534f4c4f204841434b455231223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3115301306035504030c0c736f6c6f6b6579732e636f6d3121301f06092a864886f70d010901161268656c6c6f40736f6c6f6b6579732e636f6d3059301306072a8648ce3d020106082a8648ce3d030107034200047d78f6beca40763bc75ce3acf42712c394981337a6410e92f69a3b15478db6ced9d34f3913ed127b81143be8f94c9638fee3d6cb1b5393a274f7139a0f9d5ea6a381de3081db301d0603551d0e041604149afba2210923b5e47a2a1d7a6c4e038992a30ec23081a10603551d23048199308196a18188a48185308182310b30090603550406130255533111300f06035504080c084d6172796c616e6431143012060355040a0c0b534f4c4f204841434b45523110300e060355040b0c07526f6f742043413115301306035504030c0c736f6c6f6b6579732e636f6d3121301f06092a864886f70d010901161268656c6c6f40736f6c6f6b6579732e636f6d820900ebd4845014abd15730090603551d1304023000300b0603551d0f0404030204f0300a06082a8648ce3d0403020349003046022100a17b2a1d4e42a8686d65611ef5fe6dc699ae7c208316bad6e50fd70d7e05dac90221009249f30b57d11972f2755aa2e0b6bd0f0738d0e5a24fa0f3876182d8cd48fc57304402206b8729e64c4756a458bcacb428333e4a2ea134a3e612007ff4c82978d53c5fc602200f3cb353e10bcd9c649f7c02612157bd819799313589d8d9cc0bbbae235f426d"); + // byte[] U2F_RESPONSE_DATA = Hex.decodeHexOrFail("05048227f392a231df366f909ab1f5991da19cd292a8c6d910b19cd007db19a4c7f342f29d24bcc4f49584de173ac4a4668f4651fbe6ae057684e362869b6e8fced040f39fa17ce0ea8b278047b396c37b9a55452d3782f814bf2b6a9cace7e966e1b5a27a554ff03928323bd54d648941cb22e23a0e414446c5d1b0b5fd357f546f7e3082024f30820137a00302010202044c8526dc300d06092a864886f70d01010b0500302e312c302a0603550403132359756269636f2055324620526f6f742043412053657269616c203435373230303633313020170d3134303830313030303030305a180f32303530303930343030303030305a3031312f302d06035504030c2659756269636f205532462045452053657269616c2032333932353733353038313435333237363059301306072a8648ce3d020106082a8648ce3d03010703420004507f8c04c280c954a67ad07bb9f60e6a66c0d919e1599ab83090bb4f6e823f456e861013648e4484e05fc65bfee86efe63ee6af9de1be02e91d16b5fda0de3fda33b3039302206092b0601040182c40a020415312e332e362e312e342e312e34313438322e312e323013060b2b0601040182e51c020101040403020430300d06092a864886f70d01010b050003820101003fb81882c0147e4428413837a2859da65648c5df814d3dc54db3b5a944b4135df2e78b661bd5aefd73e437c97cf2238cf44dc8a46682c055aa63d422e2d197877c94ea8afd9fc430e4bc64cb88c9b56566ea1f11be130593abcc0e399a3ed797194571fad9dc26dbeed0808b9217ce380fbea35e40c0f738aac698f15407343cbce1696c08105785e25f5230594efd8b9b35b4e3e43258d14655c54526e0436ce8d42af7a234ff5c018462f3b27d3d06d22e7df9596afa14475fa72283008f0fc8cf061ab31e6ad43497cfe903b60cfc290ed95e306f927102c69b771ac5534f56074758d0e5ceb407b35dec80b4d0acf2e4977797c142d7044cc1b8087fba7b3046022100f406898fb7a176bc3aa6a999e39fdbb805bf0085abb1a0e39e817ca9118c6e960221008baa8ed86430c19c8a8ffa843887e668e3ca902e5bbc05b284e29042618e47e8"); + private static final String PUBLIC_KEY_HEX = + "0418d25e92eea125f492a6b3aa1cb6ca908a4140cc9ce410c61e05677f7ebc3fc85d1b64d2c6aeb0aebd7fa6d3abf68d54f6bc174ed668809d3f93d49f958ba3ff"; + + private static final byte[] CERT_DATA = Base64.decode( + "MIIC6TCCAo6gAwIBAgIBATAKBggqhkjOPQQDAjCBgjELMAkGA1UEBhMCVVMx\n"+ + "ETAPBgNVBAgMCE1hcnlsYW5kMRQwEgYDVQQKDAtTT0xPIEhBQ0tFUjEQMA4G\n"+ + "A1UECwwHUm9vdCBDQTEVMBMGA1UEAwwMc29sb2tleXMuY29tMSEwHwYJKoZI\n"+ + "hvcNAQkBFhJoZWxsb0Bzb2xva2V5cy5jb20wIBcNMTgxMjExMDIyMDEyWhgP\n"+ + "MjA2ODExMjgwMjIwMTJaMIGUMQswCQYDVQQGEwJVUzERMA8GA1UECAwITWFy\n"+ + "eWxhbmQxFDASBgNVBAoMC1NPTE8gSEFDS0VSMSIwIAYDVQQLDBlBdXRoZW50\n"+ + "aWNhdG9yIEF0dGVzdGF0aW9uMRUwEwYDVQQDDAxzb2xva2V5cy5jb20xITAf\n"+ + "BgkqhkiG9w0BCQEWEmhlbGxvQHNvbG9rZXlzLmNvbTBZMBMGByqGSM49AgEG\n"+ + "CCqGSM49AwEHA0IABH149r7KQHY7x1zjrPQnEsOUmBM3pkEOkvaaOxVHjbbO\n"+ + "2dNPORPtEnuBFDvo+UyWOP7j1ssbU5OidPcTmg+dXqajgd4wgdswHQYDVR0O\n"+ + "BBYEFJr7oiEJI7XkeiodemxOA4mSow7CMIGhBgNVHSMEgZkwgZahgYikgYUw\n"+ + "gYIxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNYXJ5bGFuZDEUMBIGA1UECgwL\n"+ + "U09MTyBIQUNLRVIxEDAOBgNVBAsMB1Jvb3QgQ0ExFTATBgNVBAMMDHNvbG9r\n"+ + "ZXlzLmNvbTEhMB8GCSqGSIb3DQEJARYSaGVsbG9Ac29sb2tleXMuY29tggkA\n"+ + "69SEUBSr0VcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwCgYIKoZIzj0EAwID\n"+ + "SQAwRgIhAKF7Kh1OQqhobWVhHvX+bcaZrnwggxa61uUP1w1+BdrJAiEAkknz\n"+ + "C1fRGXLydVqi4La9Dwc40OWiT6Dzh2GC2M1I/Fc="); + + @Test + public void fromBytes() throws Exception { + U2fRegisterResponse u2fResponse = U2fRegisterResponse.fromBytes(U2F_RESPONSE_DATA); + assertEquals(PUBLIC_KEY_HEX, Hex.encodeHexString(u2fResponse.publicKey())); + assertArrayEquals(CERT_DATA, u2fResponse.attestationCertificate()); + } +} \ No newline at end of file diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/pinauth/PinAuthCryptoUtilTest.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/pinauth/PinAuthCryptoUtilTest.java new file mode 100644 index 0000000..e4f470f --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/pinauth/PinAuthCryptoUtilTest.java @@ -0,0 +1,80 @@ +package de.cotech.hw.fido2.internal.pinauth; + + +import java.io.IOException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; + +import de.cotech.hw.fido2.internal.cose.CosePublicKeyUtils; +import de.cotech.hw.fido2.internal.crypto.P256; +import de.cotech.hw.util.Hex; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + + +@SuppressWarnings("WeakerAccess") +public class PinAuthCryptoUtilTest { + static final Provider PROVIDER = new BouncyCastleProvider(); + + static final byte[] EC_PRIV = Hex.decodeHexOrFail("7452E599FEE739D8A653F6A507343D12D382249108A651402520B72F24FE7684"); + static final byte[] EC_PUB = Hex.decodeHexOrFail("0444D78D7989B97E62EA993496C9EF6E8FD58B8B00715F9A89153DDD9C4657E47FEC802EE7D22BD4E100F12E48537EB4E7E96ED3A47A0A3BD5F5EEAB65001664F9"); + static final byte[] DEV_PUB = Hex.decodeHexOrFail("040501D5BC78DA9252560A26CB08FCC60CBE0B6D3B8E1D1FCEE514FAC0AF675168D551B3ED46F665731F95B4532939C25D91DB7EB844BD96D4ABD4083785F8DF47"); + static final byte[] SHARED = Hex.decodeHexOrFail("c42a039d548100dfba521e487debcbbb8b66bb7496f8b1862a7a395ed83e1a1c"); + static final byte[] PIN_HASH = Hex.decodeHexOrFail("03ac674216f3e15c761ee1a5e255f067"); + static final byte[] PIN_HASH_ENC = Hex.decodeHexOrFail("afe8327ce416da8ee3d057589c2ce1a9"); + static final byte[] TOKEN_ENC = Hex.decodeHexOrFail("7A9F98E31B77BE90F9C64D12E9635040"); + static final byte[] TOKEN = Hex.decodeHexOrFail("aff12c6dcfbf9df52f7a09211e8865cd"); + static final String PADDED_PIN_1234 = "31323334000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + private final PinAuthCryptoUtil pinAuthCryptoUtil = new PinAuthCryptoUtil(); + + @Test + public void calculatePinHashEnc() throws Exception { + byte[] pinHashEnc = pinAuthCryptoUtil.calculatePinHashEnc(SHARED, "1234"); + assertArrayEquals(PIN_HASH_ENC, pinHashEnc); + } + + @Test + public void calculatePinHash() throws Exception { + byte[] pinHash = pinAuthCryptoUtil.calculatePinHash("1234"); + assertArrayEquals(PIN_HASH, pinHash); + } + + @Test + public void padPin() throws IOException { + byte[] paddedPin = pinAuthCryptoUtil.padPin("1234"); + assertEquals(PADDED_PIN_1234, Hex.encodeHexString(paddedPin)); + } + + @Test + public void decryptPinToken() throws Exception { + byte[] pinToken = pinAuthCryptoUtil.decryptPinToken(SHARED, TOKEN_ENC); + assertArrayEquals(TOKEN, pinToken); + } + + @Test + public void publicKeyFromCosePublicKey_to_cosePublicKeyFromPublicKey() throws IOException { + byte[] cosePublicKey = CosePublicKeyUtils.encodex962PublicKeyAsCose(EC_PUB); + + PublicKey publicKey = pinAuthCryptoUtil.publicKeyFromCosePublicKey(cosePublicKey); + byte[] cosePublicKey2 = pinAuthCryptoUtil.cosePublicKeyFromPublicKey(publicKey); + + assertArrayEquals(cosePublicKey, cosePublicKey2); + } + + @Test + public void generateSharedSecret() throws Exception { + PrivateKey platformPrivateKey = P256.deserializePrivateKey(EC_PRIV); + + byte[] cosePublicKey = CosePublicKeyUtils.encodex962PublicKeyAsCose(DEV_PUB); + PublicKey authenticatorPublicKey = pinAuthCryptoUtil.publicKeyFromCosePublicKey(cosePublicKey); + + byte[] secret = pinAuthCryptoUtil.generateSharedSecret(platformPrivateKey, authenticatorPublicKey); + + assertArrayEquals(secret, SHARED); + } +} \ No newline at end of file diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/pinauth/PinProtocolV1Test.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/pinauth/PinProtocolV1Test.java new file mode 100644 index 0000000..2b18cda --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/pinauth/PinProtocolV1Test.java @@ -0,0 +1,144 @@ +package de.cotech.hw.fido2.internal.pinauth; + + +import java.io.IOException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; + +import de.cotech.hw.fido2.exceptions.FidoClientPinBlockedException; +import de.cotech.hw.fido2.exceptions.FidoClientPinInvalidException; +import de.cotech.hw.fido2.exceptions.FidoClientPinLastAttemptException; +import de.cotech.hw.fido2.internal.FakeFido2AppletConnection; +import de.cotech.hw.util.Hex; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertSame; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +@SuppressWarnings("WeakerAccess") +public class PinProtocolV1Test { + static final String CLIENT_PIN_GET_RETRIES = "801000000606a201010201"; + static final String CLIENT_PIN_GET_RETRIES_RESPONSE_EIGHT = "00a103089000"; + static final String CLIENT_PIN_GET_RETRIES_RESPONSE_ONE = "00a103019000"; + static final String CLIENT_PIN_GET_RETRIES_RESPONSE_ZERO = "00a103009000"; + static final String CLIENT_PIN_GET_AGREEMENT = "801000000606a201010202"; + static final String CLIENT_PIN_GET_RESPONSE = "00a101a501020338182001215820f370174f29f360cff04035c4b46daf2a93468398521ac7fc7bf8f8f986d9e08f2258202ba85af4992d37ef3977eea283b65a00e7c06dc473aeecc24b70f9b9d22d5e8d9000"; + static final byte[] AUTHENTICATOR_KEY_AGREEMENT = Hex.decodeHexOrFail( + "a501020338182001215820f370174f29f360cff04035c4b46daf2a93468398521ac7fc7bf8f8f986d9e08f2258202ba85af4992d37ef3977eea283b65a00e7c06dc473aeecc24b70f9b9d22d5e8d"); + static final byte[] PLATFORM_KEY_AGREEMENT = Hex.decodeHexOrFail( + "a501020326200121582099401f6ffa9446585074d1058578f4c68ab46953ccf8cb7910d8b3c9350e1040225820a3c962cddad166b1fdea8a95a377dbf8efbedd82778a8004b884f0009bd6df9506"); + static final byte[] PIN_HASH_ENC = Hex.decodeHexOrFail("2eb756ae474f7ca032b5111b2eef6959"); + static final byte[] PIN_TOKEN_ENC = Hex.decodeHexOrFail("554df3802226935b2bf49f30aae3096f"); + static final String CLIENT_PIN_GET_TOKEN = "801000006606a40101020503a501020326200121582099401f6ffa9446585074d1058578f4c68ab46953ccf8cb7910d8b3c9350e1040225820a3c962cddad166b1fdea8a95a377dbf8efbedd82778a8004b884f0009bd6df9506502eb756ae474f7ca032b5111b2eef6959"; + static final String CLIENT_PIN_GET_TOKEN_RESPONSE = "00a10250554df3802226935b2bf49f30aae3096f9000"; + static final String CLIENT_PIN_GET_TOKEN_RESPONSE_INVALID = "329000"; + static final String CLIENT_PIN_GET_TOKEN_RESPONSE_BLOCKED = "319000"; + + static final byte[] SHARED_SECRET = new byte[1]; + static final byte[] PIN_TOKEN = new byte[1]; + + private FakeFido2AppletConnection fakeFidoConnection; + + @Before + public void setup() throws Exception { + fakeFidoConnection = FakeFido2AppletConnection.create(false); + } + + @Test + public void pinAuth() throws Exception { + PinProtocolV1 pinProtocolV1 = setupPinProtocol(); + + fakeFidoConnection.expect(CLIENT_PIN_GET_RETRIES, CLIENT_PIN_GET_RETRIES_RESPONSE_EIGHT); + fakeFidoConnection.expect(CLIENT_PIN_GET_AGREEMENT, CLIENT_PIN_GET_RESPONSE); + fakeFidoConnection.expect(CLIENT_PIN_GET_TOKEN, CLIENT_PIN_GET_TOKEN_RESPONSE); + + PinToken pinToken = pinProtocolV1.clientPinAuthenticate(fakeFidoConnection.connection, "1234", false); + + assertSame(PIN_TOKEN, pinToken.pinToken()); + fakeFidoConnection.verify(); + } + + @Test(expected = FidoClientPinInvalidException.class) + public void pinAuth_invalid() throws Exception { + PinProtocolV1 pinProtocolV1 = setupPinProtocol(); + + fakeFidoConnection.expect(CLIENT_PIN_GET_RETRIES, CLIENT_PIN_GET_RETRIES_RESPONSE_EIGHT); + fakeFidoConnection.expect(CLIENT_PIN_GET_AGREEMENT, CLIENT_PIN_GET_RESPONSE); + fakeFidoConnection.expect(CLIENT_PIN_GET_TOKEN, CLIENT_PIN_GET_TOKEN_RESPONSE_INVALID); + + pinProtocolV1.clientPinAuthenticate(fakeFidoConnection.connection, "1234", false); + } + + @Test(expected = FidoClientPinInvalidException.class) + public void pinAuth_invalid_blocked() throws Exception { + PinProtocolV1 pinProtocolV1 = setupPinProtocol(); + + fakeFidoConnection.expect(CLIENT_PIN_GET_RETRIES, CLIENT_PIN_GET_RETRIES_RESPONSE_ONE); + fakeFidoConnection.expect(CLIENT_PIN_GET_AGREEMENT, CLIENT_PIN_GET_RESPONSE); + fakeFidoConnection.expect(CLIENT_PIN_GET_TOKEN, CLIENT_PIN_GET_TOKEN_RESPONSE_BLOCKED); + + pinProtocolV1.clientPinAuthenticate(fakeFidoConnection.connection, "1234", true); + } + + @Test(expected = FidoClientPinLastAttemptException.class) + public void pinAuth_lastAttempt_fail() throws Exception { + PinProtocolV1 pinProtocolV1 = setupPinProtocol(); + + fakeFidoConnection.expect(CLIENT_PIN_GET_RETRIES, CLIENT_PIN_GET_RETRIES_RESPONSE_ONE); + + pinProtocolV1.clientPinAuthenticate(fakeFidoConnection.connection, "1234", false); + } + + @Test + public void pinAuth_lastAttempt_ok() throws Exception { + PinProtocolV1 pinProtocolV1 = setupPinProtocol(); + + fakeFidoConnection.expect(CLIENT_PIN_GET_RETRIES, CLIENT_PIN_GET_RETRIES_RESPONSE_ONE); + fakeFidoConnection.expect(CLIENT_PIN_GET_AGREEMENT, CLIENT_PIN_GET_RESPONSE); + fakeFidoConnection.expect(CLIENT_PIN_GET_TOKEN, CLIENT_PIN_GET_TOKEN_RESPONSE); + + PinToken pinToken = pinProtocolV1.clientPinAuthenticate(fakeFidoConnection.connection, "1234", true); + + assertSame(PIN_TOKEN, pinToken.pinToken()); + fakeFidoConnection.verify(); + } + + @Test(expected = FidoClientPinBlockedException.class) + public void pinAuth_blocked() throws Exception { + PinProtocolV1 pinProtocolV1 = setupPinProtocol(); + + fakeFidoConnection.expect(CLIENT_PIN_GET_RETRIES, CLIENT_PIN_GET_RETRIES_RESPONSE_ZERO); + + pinProtocolV1.clientPinAuthenticate(fakeFidoConnection.connection, "1234", true); + + } + + private PinProtocolV1 setupPinProtocol() throws IOException { + PinAuthCryptoUtil pinAuthCryptoUtil = mock(PinAuthCryptoUtil.class); + PinProtocolV1 pinProtocolV1 = new PinProtocolV1(pinAuthCryptoUtil); + + // These values are opaque to PinProtocolV1. we just generate empty mock objects for them + PublicKey publicKey = mock(PublicKey.class); + PrivateKey privateKey = mock(PrivateKey.class); + KeyPair keyPair = new KeyPair(publicKey, privateKey); + PublicKey authenticatorPublicKey = mock(PublicKey.class); + + when(pinAuthCryptoUtil.generatePlatformKeyPair()).thenReturn(keyPair); + when(pinAuthCryptoUtil.cosePublicKeyFromPublicKey(publicKey)) + .thenReturn(PLATFORM_KEY_AGREEMENT); + when(pinAuthCryptoUtil.publicKeyFromCosePublicKey(aryEq(AUTHENTICATOR_KEY_AGREEMENT))) + .thenReturn(authenticatorPublicKey); + when(pinAuthCryptoUtil.calculatePinHashEnc(aryEq(SHARED_SECRET), eq("1234"))) + .thenReturn(PIN_HASH_ENC); + when(pinAuthCryptoUtil.generateSharedSecret(privateKey, authenticatorPublicKey)) + .thenReturn(SHARED_SECRET); + when(pinAuthCryptoUtil.decryptPinToken(SHARED_SECRET, PIN_TOKEN_ENC)).thenReturn(PIN_TOKEN); + return pinProtocolV1; + } +} \ No newline at end of file diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/utils/DerUtilsTest.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/utils/DerUtilsTest.java new file mode 100644 index 0000000..e2ebd11 --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/utils/DerUtilsTest.java @@ -0,0 +1,22 @@ +package de.cotech.hw.fido2.internal.utils; + + +import java.nio.ByteBuffer; + +import de.cotech.hw.fido2.internal.utils.DerUtils; +import org.bouncycastle.util.encoders.Base64; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + + +public class DerUtilsTest { + private static final byte[] DATA = Base64.decode("MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEUASUVlTkm9TXaTahsccnT9CpKaY1Nhlel0mx/wXoIP+uap0V6nXMrLHyUSv3OT2GxoOKU5kZqTeODCymQiMrVrpEd5q7aqACTdpjN0QBZz1YV2uY2LzDyzLCKt9Ey6ut"); + + @Test + public void findDerEncodedLength() throws Exception { + int derEncodedLength = DerUtils.findDerEncodedLength(ByteBuffer.wrap(DATA)); + + assertEquals(DATA.length, derEncodedLength); + } +} \ No newline at end of file diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/utils/WebsafeBase64Test.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/utils/WebsafeBase64Test.java new file mode 100644 index 0000000..3183031 --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/utils/WebsafeBase64Test.java @@ -0,0 +1,41 @@ +package de.cotech.hw.fido2.internal.utils; + + +import android.util.Base64; + +import de.cotech.hw.util.Hex; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.*; + + +@SuppressWarnings("WeakerAccess") +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 24) +public class WebsafeBase64Test { + static final byte[] DECODED_HEX = Hex.decodeHexOrFail("18dc5f5507c454ea22f6e5355bf8ccfb"); + static final byte[] DECODED = Hex.decodeHexOrFail("18dc5f5507c454ea22f6e5355bf8ccfb"); + static final String ENCODED_WB64 = "GNxfVQfEVOoi9uU1W_jM-w"; + static final String ENCODED_B64 = "GNxfVQfEVOoi9uU1W/jM+w"; + + @Test + public void encodeToString() { + String encoded = WebsafeBase64.encodeToString(DECODED); + assertEquals(ENCODED_WB64, encoded); + } + + @Test + public void decode_websafe() { + byte[] decoded = WebsafeBase64.decode(ENCODED_WB64); + assertArrayEquals(DECODED, decoded); + } + + @Test + public void decode_default() { + byte[] decoded = Base64.decode(ENCODED_B64, Base64.DEFAULT); + assertArrayEquals(DECODED, decoded); + } +} \ No newline at end of file diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/webauthn/AuthenticatorDataParserTest.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/webauthn/AuthenticatorDataParserTest.java new file mode 100644 index 0000000..2c317f7 --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/internal/webauthn/AuthenticatorDataParserTest.java @@ -0,0 +1,48 @@ +package de.cotech.hw.fido2.internal.webauthn; + + +import de.cotech.hw.fido2.domain.create.AuthenticatorData; +import de.cotech.hw.util.Hex; +import org.junit.Assert; +import org.junit.Test; + + +@SuppressWarnings("WeakerAccess") +public class AuthenticatorDataParserTest { + + static final String DATA_HEX = "964491febf301ee4bcb88cb5dc1fa95df27f1643a0c7cef9f71ceef6689e067a41000001408876631bd4a0427f57730ec71c9e027900462a858dfaa38c5b6b20272b8f457cd2f6affe19fa39314260d4487c63f1b3c1dcdb7a964491febf301ee4bcb88cb5dc1fa95df27f1643a0c7cef9f71ceef6689e067a40010000a50102032620012158208a649c9f8a7ccf6e11ebec5753ec6edf42f8ac6565c44fab0a87cb180d981b972258207111ab96f3bf664405025f74509bc689e4686a9ec9ba94c5684ac98b9a5257a2"; + static final byte[] DATA = Hex.decodeHexOrFail(DATA_HEX); + + static final String DATA_SHORT_HEX = "1194228da8fdbdeefd261bd7b6595cfd70a50d70c6407bcf013de96d4efb17de010000003b"; + static final byte[] DATA_SHORT = Hex.decodeHexOrFail(DATA_SHORT_HEX); + + AuthenticatorDataParser parser = new AuthenticatorDataParser(); + + @Test + public void decode_encode_short() throws Exception { + AuthenticatorData authenticatorData = parser.fromBytes(DATA_SHORT); + byte[] bytes = parser.toBytes(authenticatorData); + Assert.assertEquals(DATA_SHORT_HEX, Hex.encodeHexString(bytes)); + } + + @Test + public void decode_encode() throws Exception { + AuthenticatorData authenticatorData = parser.fromBytes(DATA); + byte[] bytes = parser.toBytes(authenticatorData); + Assert.assertEquals(DATA_HEX, Hex.encodeHexString(bytes)); + } + + @Test + public void decode_anonymize_encode() throws Exception { + AuthenticatorData authenticatorData = parser.fromBytes(DATA); + String hexAaguid = Hex.encodeHexString(authenticatorData.attestedCredentialData().aaguid()); + // noinspection ReplaceAllDot, yes this is what we want + String hexAaguidAnonymized = hexAaguid.replaceAll(".", "0"); + String expectedHex = DATA_HEX.replace(hexAaguid, hexAaguidAnonymized); + + AuthenticatorData anonymizedAuthenticatorData = authenticatorData.withEmptyAaguid(); + byte[] bytes = parser.toBytes(anonymizedAuthenticatorData); + + Assert.assertEquals(expectedHex, Hex.encodeHexString(bytes)); + } +} diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/internal/iso7816/ResponseApduUtils.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/internal/iso7816/ResponseApduUtils.java new file mode 100644 index 0000000..081c6b1 --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/internal/iso7816/ResponseApduUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.internal.iso7816; + + +import java.io.IOException; + +import androidx.annotation.NonNull; + +import static org.junit.Assert.fail; + + +public class ResponseApduUtils { + @NonNull + public static ResponseApdu createError(int errorSw) { + try { + return ResponseApdu.fromBytes(new byte[] { (byte) ((errorSw >> 8) & 0xff), (byte) (errorSw & 0xff) }); + } catch (IOException e) { + fail(); + return null; + } + } +} diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/internal/transport/FakeTransport.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/internal/transport/FakeTransport.java new file mode 100644 index 0000000..4aae876 --- /dev/null +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/internal/transport/FakeTransport.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.internal.transport; + + +import java.io.IOException; +import java.util.LinkedList; + +import androidx.annotation.Nullable; + +import de.cotech.hw.internal.iso7816.CommandApdu; +import de.cotech.hw.internal.iso7816.ResponseApdu; +import de.cotech.hw.internal.transport.SecurityKeyInfo.SecurityKeyType; +import de.cotech.hw.internal.transport.SecurityKeyInfo.TransportType; +import de.cotech.hw.util.Hex; +import org.junit.Assert; + +import static org.junit.Assert.assertEquals; + + +@SuppressWarnings("WeakerAccess") +public class FakeTransport implements Transport { + LinkedList expectCommands = new LinkedList<>(); + LinkedList expectResponses = new LinkedList<>(); + LinkedList expectExceptions = new LinkedList<>(); + boolean extendedLengthSupported = false; + + @Override + public ResponseApdu transceive(CommandApdu data) throws IOException { + CommandApdu expected = expectCommands.poll(); + assertEquals(expected, data); + + if (!expectExceptions.isEmpty()) { + throw expectExceptions.poll(); + } + + return expectResponses.poll(); + } + + @Override + public void release() { + + } + + @Override + public boolean isConnected() { + return true; + } + + @Override + public boolean isReleased() { + return false; + } + + @Override + public boolean isPersistentConnectionAllowed() { + return false; + } + + @Override + public void connect() throws IOException { + + } + + @Override + public boolean ping() { + return true; + } + + @Override + public TransportType getTransportType() { + return TransportType.USB_CTAPHID; + } + + @Nullable + @Override + public SecurityKeyType getSecurityKeyTypeIfAvailable() { + return null; + } + + @Override + public boolean isExtendedLengthSupported() { + return extendedLengthSupported; + } + + public void setExtendedLengthSupported(boolean extendedLengthSupported) { + this.extendedLengthSupported = extendedLengthSupported; + } + + public void expect(String commandBytesHex, String responseBytesHex) throws IOException { + CommandApdu commandApdu = CommandApdu.fromBytes(Hex.decodeHexOrFail(commandBytesHex)); + commandApdu = commandApdu.withExtendedApduNe(); + expectCommands.add(commandApdu); + expectResponses.add(ResponseApdu.fromBytes(Hex.decodeHexOrFail(responseBytesHex))); + } + + public void expect(CommandApdu commandApdu, ResponseApdu responseApdu) { + expectCommands.add(commandApdu); + expectResponses.add(responseApdu); + } + + public void expect(CommandApdu commandApdu, IOException exception) { + expectCommands.add(commandApdu); + expectExceptions.add(exception); + } + + public void verify() { + Assert.assertEquals(0, expectCommands.size()); + Assert.assertEquals(0, expectResponses.size()); + Assert.assertEquals(0, expectExceptions.size()); + } +} diff --git a/hwsecurity/intent-nfc/build.gradle b/hwsecurity/intent-nfc/build.gradle index e703244..adb5a68 100644 --- a/hwsecurity/intent-nfc/build.gradle +++ b/hwsecurity/intent-nfc/build.gradle @@ -1,5 +1,5 @@ apply plugin: 'com.android.library' -apply plugin: 'digital.wup.android-maven-publish' +apply plugin: 'maven-publish' dependencies { api project(':hwsecurity:core') @@ -24,51 +24,54 @@ android { } } -publishing { - publications { - mavenAar(MavenPublication) { - groupId = 'de.cotech' - artifactId = 'hwsecurity-intent-nfc' - version = android.defaultConfig.versionName +// https://developer.android.com/studio/build/maven-publish-plugin +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release - from components.android + groupId = 'de.cotech' + artifactId = 'hwsecurity-intent-nfc' + version = android.defaultConfig.versionName - pom { - url = 'https://hwsecurity.dev' - licenses { - license { - name = 'Commercial' - url = 'https://hwsecurity.dev/sales/' - distribution = 'repo' + pom { + url = 'https://hwsecurity.dev' + licenses { + license { + name = 'Commercial' + url = 'https://hwsecurity.dev/sales/' + distribution = 'repo' + } + license { + name = 'GNU General Public License, version 3' + url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + } } - license { - name = 'GNU General Public License, version 3' - url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + organization { + name = 'Confidential Technologies GmbH' + url = 'https://www.cotech.de' } } - organization { - name = 'Confidential Technologies GmbH' - url = 'https://www.cotech.de' - } } } - } - /* - * To upload release, create file gradle.properties in ~/.gradle/ with this content: - * - * cotechMavenName=xxx - * cotechMavenPassword=xxx - */ - if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { - println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" + /* + * To upload release, create file gradle.properties in ~/.gradle/ with this content: + * + * cotechMavenName=xxx + * cotechMavenPassword=xxx + */ + if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { + println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" - repositories { - maven { - credentials { - username cotechMavenName - password cotechMavenPassword + repositories { + maven { + credentials { + username cotechMavenName + password cotechMavenPassword + } + url = "https://maven.cotech.de" } - url = "https://maven.cotech.de" } } } diff --git a/hwsecurity/intent-usb/build.gradle b/hwsecurity/intent-usb/build.gradle index b0cc8e3..f3996fb 100644 --- a/hwsecurity/intent-usb/build.gradle +++ b/hwsecurity/intent-usb/build.gradle @@ -1,5 +1,5 @@ apply plugin: 'com.android.library' -apply plugin: 'digital.wup.android-maven-publish' +apply plugin: 'maven-publish' dependencies { api project(':hwsecurity:core') @@ -24,51 +24,54 @@ android { } } -publishing { - publications { - mavenAar(MavenPublication) { - groupId = 'de.cotech' - artifactId = 'hwsecurity-intent-usb' - version = android.defaultConfig.versionName +// https://developer.android.com/studio/build/maven-publish-plugin +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release - from components.android + groupId = 'de.cotech' + artifactId = 'hwsecurity-intent-usb' + version = android.defaultConfig.versionName - pom { - url = 'https://hwsecurity.dev' - licenses { - license { - name = 'Commercial' - url = 'https://hwsecurity.dev/sales/' - distribution = 'repo' + pom { + url = 'https://hwsecurity.dev' + licenses { + license { + name = 'Commercial' + url = 'https://hwsecurity.dev/sales/' + distribution = 'repo' + } + license { + name = 'GNU General Public License, version 3' + url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + } } - license { - name = 'GNU General Public License, version 3' - url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + organization { + name = 'Confidential Technologies GmbH' + url = 'https://www.cotech.de' } } - organization { - name = 'Confidential Technologies GmbH' - url = 'https://www.cotech.de' - } } } - } - /* - * To upload release, create file gradle.properties in ~/.gradle/ with this content: - * - * cotechMavenName=xxx - * cotechMavenPassword=xxx - */ - if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { - println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" + /* + * To upload release, create file gradle.properties in ~/.gradle/ with this content: + * + * cotechMavenName=xxx + * cotechMavenPassword=xxx + */ + if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { + println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" - repositories { - maven { - credentials { - username cotechMavenName - password cotechMavenPassword + repositories { + maven { + credentials { + username cotechMavenName + password cotechMavenPassword + } + url = "https://maven.cotech.de" } - url = "https://maven.cotech.de" } } } diff --git a/hwsecurity/openpgp/build.gradle b/hwsecurity/openpgp/build.gradle index 16b9254..1c2d44c 100644 --- a/hwsecurity/openpgp/build.gradle +++ b/hwsecurity/openpgp/build.gradle @@ -1,14 +1,17 @@ apply plugin: 'com.android.library' -apply plugin: 'digital.wup.android-maven-publish' +apply plugin: 'maven-publish' apply plugin: 'org.jetbrains.dokka-android' dependencies { api project(':hwsecurity:core') implementation project(':hwsecurity:provider') - implementation 'org.bouncycastle:bcprov-jdk15on:1.64' + compileOnly project(':hwsecurity:ui') + compileOnly 'com.google.android.material:material:1.1.0' - compileOnly 'androidx.annotation:annotation:1.0.0' + implementation 'org.bouncycastle:bcprov-jdk15on:1.65' + + compileOnly 'androidx.annotation:annotation:1.1.0' api 'com.google.auto.value:auto-value-annotations:1.6.2' annotationProcessor 'com.google.auto.value:auto-value:1.6.2' @@ -39,59 +42,63 @@ android { } } -publishing { - publications { - mavenAar(MavenPublication) { - groupId = 'de.cotech' - artifactId = 'hwsecurity-openpgp' - version = android.defaultConfig.versionName - - from components.android - - pom { - url = 'https://hwsecurity.dev' - licenses { - license { - name = 'Commercial' - url = 'https://hwsecurity.dev/sales/' - distribution = 'repo' +// https://developer.android.com/studio/build/maven-publish-plugin +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release + + groupId = 'de.cotech' + artifactId = 'hwsecurity-openpgp' + version = android.defaultConfig.versionName + + pom { + url = 'https://hwsecurity.dev' + licenses { + license { + name = 'Commercial' + url = 'https://hwsecurity.dev/sales/' + distribution = 'repo' + } + license { + name = 'GNU General Public License, version 3' + url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + } } - license { - name = 'GNU General Public License, version 3' - url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + organization { + name = 'Confidential Technologies GmbH' + url = 'https://www.cotech.de' } } - organization { - name = 'Confidential Technologies GmbH' - url = 'https://www.cotech.de' - } } } - } - /* - * To upload release, create file gradle.properties in ~/.gradle/ with this content: - * - * cotechMavenName=xxx - * cotechMavenPassword=xxx - */ - if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { - println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" - - repositories { - maven { - credentials { - username cotechMavenName - password cotechMavenPassword + /* + * To upload release, create file gradle.properties in ~/.gradle/ with this content: + * + * cotechMavenName=xxx + * cotechMavenPassword=xxx + */ + if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { + println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" + + repositories { + maven { + credentials { + username cotechMavenName + password cotechMavenPassword + } + url = "https://maven.cotech.de" } - url = "https://maven.cotech.de" } } } } dokka { + moduleName = 'hwsecurity-openpgp' outputFormat = "hugo" - outputDirectory = "$buildDir/dokka/reference" + outputDirectory = "$projectDir/../../hwsecurity.dev/content/reference" sourceDirs = files('src/main/java') packageOptions { diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/CardCapabilities.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/CardCapabilities.java index d7eec11..0b50b4c 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/CardCapabilities.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/CardCapabilities.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpCapabilities.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpCapabilities.java index e2a9688..5137e6e 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpCapabilities.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpCapabilities.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpCardUtils.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpCardUtils.java index b16e300..af98170 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpCardUtils.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpCardUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKey.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKey.java index 4d781fd..0acf039 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKey.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKey.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyAuthenticator.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyAuthenticator.java index 3c6d176..edf3855 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyAuthenticator.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyAuthenticator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyConnectionMode.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyConnectionMode.java index 2047c2e..274b9cc 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyConnectionMode.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyConnectionMode.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -65,8 +65,8 @@ private OpenPgpSecurityKeyConnectionMode(OpenPgpSecurityKeyConnectionModeConfig @WorkerThread public OpenPgpSecurityKey establishSecurityKeyConnection(SecurityKeyManagerConfig securityKeyManagerConfig, Transport transport) throws IOException { - if (transport.getTransportType() == SecurityKeyInfo.TransportType.USB_U2FHID) { - HwTimber.d("USB U2FHID is available but not supported by OPENPGP."); + if (transport.getTransportType() == SecurityKeyInfo.TransportType.USB_CTAPHID) { + HwTimber.d("USB CTAPHID is available but not supported by OPENPGP."); return null; } @@ -79,7 +79,7 @@ public OpenPgpSecurityKey establishSecurityKeyConnection(SecurityKeyManagerConfi @Override protected boolean isRelevantTransport(Transport transport) { - return transport.getTransportType() != SecurityKeyInfo.TransportType.USB_U2FHID; + return transport.getTransportType() != SecurityKeyInfo.TransportType.USB_CTAPHID; } @Override diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyConnectionModeConfig.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyConnectionModeConfig.java index d690492..d1ba4cb 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyConnectionModeConfig.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyConnectionModeConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogFactory.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyDialogFragment.java similarity index 50% rename from hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogFactory.java rename to hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyDialogFragment.java index f7b1c77..fc183d6 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogFactory.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/OpenPgpSecurityKeyDialogFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -22,33 +22,39 @@ * along with this program. If not, see . */ -package de.cotech.hw.ui; +package de.cotech.hw.openpgp; +import android.content.Context; import android.os.Bundle; import androidx.annotation.NonNull; -import de.cotech.hw.openpgp.OpenPgpSecurityKey; -import de.cotech.hw.openpgp.OpenPgpSecurityKeyConnectionModeConfig; -import de.cotech.hw.piv.PivSecurityKey; -import de.cotech.hw.ui.internal.OpenPgpSecurityKeyDialogFragment; -import de.cotech.hw.ui.internal.PivSecurityKeyDialogFragment; +import java.io.IOException; -public class SecurityKeyDialogFactory { +import de.cotech.hw.SecurityKey; +import de.cotech.hw.SecurityKeyManager; +import de.cotech.hw.openpgp.internal.OpenPgpSecurityKeyDialogPresenter; +import de.cotech.hw.secrets.ByteSecret; +import de.cotech.hw.ui.SecurityKeyDialogFragment; +import de.cotech.hw.ui.SecurityKeyDialogOptions; +import de.cotech.hw.ui.internal.SecurityKeyDialogPresenter; - public static SecurityKeyDialogFragment newOpenPgpInstance() { - return newOpenPgpInstance(SecurityKeyDialogOptions.builder().build(), new OpenPgpSecurityKeyConnectionModeConfig.Builder().build()); +public class OpenPgpSecurityKeyDialogFragment extends SecurityKeyDialogFragment { + public static final String ARG_OPENPGP_CONFIG = "de.cotech.hw.openpgp.ARG_OPENPGP_CONFIG"; + + public static SecurityKeyDialogFragment newInstance() { + return newInstance(SecurityKeyDialogOptions.builder().build(), new OpenPgpSecurityKeyConnectionModeConfig.Builder().build()); } - public static SecurityKeyDialogFragment newOpenPgpInstance(@NonNull SecurityKeyDialogOptions options) { - return newOpenPgpInstance(options, new OpenPgpSecurityKeyConnectionModeConfig.Builder().build()); + public static SecurityKeyDialogFragment newInstance(@NonNull SecurityKeyDialogOptions options) { + return newInstance(options, new OpenPgpSecurityKeyConnectionModeConfig.Builder().build()); } - public static SecurityKeyDialogFragment newOpenPgpInstance(@NonNull SecurityKeyDialogOptions options, @NonNull OpenPgpSecurityKeyConnectionModeConfig openpgpConfig) { + public static SecurityKeyDialogFragment newInstance(@NonNull SecurityKeyDialogOptions options, @NonNull OpenPgpSecurityKeyConnectionModeConfig openpgpConfig) { try { - Class.forName("de.cotech.hw.openpgp.OpenPgpSecurityKey"); + Class.forName("de.cotech.hw.ui.SecurityKeyDialogFragment"); } catch (ClassNotFoundException e) { - throw new IllegalArgumentException("You must include the hwsecurity-openpgp Maven artifact!"); + throw new IllegalArgumentException("You must include the hwsecurity-ui Maven artifact!"); } Bundle args = new Bundle(); @@ -60,23 +66,15 @@ public static SecurityKeyDialogFragment newOpenPgpInstance(@ return fragment; } - public static SecurityKeyDialogFragment newPivInstance() { - return newPivInstance(SecurityKeyDialogOptions.builder().build()); + @Override + public void initSecurityKeyConnectionMode(Bundle arguments) { + OpenPgpSecurityKeyConnectionModeConfig openpgpConfig = arguments.getParcelable(ARG_OPENPGP_CONFIG); + SecurityKeyManager.getInstance().registerCallback(OpenPgpSecurityKeyConnectionMode.getInstance(openpgpConfig), this, this); } - public static SecurityKeyDialogFragment newPivInstance(@NonNull SecurityKeyDialogOptions options) { - try { - Class.forName("de.cotech.hw.piv.PivSecurityKey"); - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException("You must include the hwsecurity-piv Maven artifact!"); - } - - Bundle args = new Bundle(); - args.putParcelable(PivSecurityKeyDialogFragment.ARG_DIALOG_OPTIONS, options); - - PivSecurityKeyDialogFragment fragment = new PivSecurityKeyDialogFragment(); - fragment.setArguments(args); - return fragment; + @Override + public SecurityKeyDialogPresenter initPresenter(SecurityKeyDialogPresenter.View view, Context context, SecurityKeyDialogOptions options) { + return new OpenPgpSecurityKeyDialogPresenter(this, getActivity(), options); } } diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpLockedException.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpLockedException.java index cde7942..416b293 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpLockedException.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpLockedException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpPinTooShortException.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpPinTooShortException.java index 6b7a09c..f1481c3 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpPinTooShortException.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpPinTooShortException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpPublicKeyUnavailableException.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpPublicKeyUnavailableException.java index 1eea955..4fb887d 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpPublicKeyUnavailableException.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpPublicKeyUnavailableException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpWrongPinException.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpWrongPinException.java index 474b171..1bb0373 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpWrongPinException.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/OpenPgpWrongPinException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/SecurityKeyTerminatedException.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/SecurityKeyTerminatedException.java index c02b593..f412555 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/SecurityKeyTerminatedException.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/exceptions/SecurityKeyTerminatedException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpAppletConnection.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpAppletConnection.java index a169b02..5d45193 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpAppletConnection.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpAppletConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -294,8 +294,10 @@ public ResponseApdu communicateOrThrow(CommandApdu commandApdu) throws IOExcepti case OpenPgpWrongPinException.SW_WRONG_PIN: case OpenPgpWrongPinException.SW_WRONG_PIN_YKNEO_1: case OpenPgpWrongPinException.SW_WRONG_PIN_YKNEO_2: - int pinRetriesLeft = getOpenPgpCapabilities().getPw1TriesLeft() - 1; - int pukRetriesLeft = getOpenPgpCapabilities().getPw3TriesLeft() - 1; + // get current number of retries (capabilities must be refreshed for USB!) + refreshConnectionCapabilities(); + int pinRetriesLeft = getOpenPgpCapabilities().getPw1TriesLeft(); + int pukRetriesLeft = getOpenPgpCapabilities().getPw3TriesLeft(); throw new OpenPgpWrongPinException(pinRetriesLeft, pukRetriesLeft); case OpenPgpLockedException.SW_OPENPGP_LOCKED: case OpenPgpLockedException.SW_OPENPGP_LOCKED_YKNEO: diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpCommandApduDescriber.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpCommandApduDescriber.java index 182aeff..44fedda 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpCommandApduDescriber.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpCommandApduDescriber.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpCommandApduFactory.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpCommandApduFactory.java index 96515af..7309c67 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpCommandApduFactory.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpCommandApduFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -28,7 +28,9 @@ import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; + import de.cotech.hw.internal.iso7816.CommandApdu; + import org.bouncycastle.util.Arrays; import org.bouncycastle.util.encoders.Hex; @@ -42,11 +44,7 @@ public class OpenPgpCommandApduFactory { // The spec allows 255, but for compatibility with non-compliant security keys we use 254 here // See https://github.com/open-keychain/open-keychain/issues/2049 - private static final int MAX_APDU_NC = 254; - private static final int MAX_APDU_NC_EXT = 65535; - - private static final int MAX_APDU_NE = 256; - private static final int MAX_APDU_NE_EXT = 65536; + private static final int MAX_APDU_NC_SHORT_OPENPGP_WORKAROUND = CommandApdu.MAX_APDU_NC_SHORT - 1; private static final int CLA = 0x00; private static final int MASK_CLA_CHAINING = 1 << 4; @@ -110,28 +108,16 @@ public CommandApdu createVerifyPw1ForOtherCommand(byte[] pin) { return CommandApdu.create(CLA, INS_VERIFY, P1_EMPTY, P2_VERIFY_PW1_OTHER, pin).withDescriber(DESCRIBER); } - // ISO/IEC 7816-4 - @NonNull - public CommandApdu createSelectFileCommand(byte[] fileAid) { - return CommandApdu.create(CLA, INS_SELECT_FILE, P1_SELECT_FILE, P2_EMPTY, fileAid).withDescriber(DESCRIBER); - } - @NonNull public CommandApdu createGetDataCommand(int p1, int p2) { - return CommandApdu.create(CLA, INS_GET_DATA, p1, p2, MAX_APDU_NE_EXT).withDescriber(DESCRIBER); + return CommandApdu.create(CLA, INS_GET_DATA, p1, p2, CommandApdu.MAX_APDU_NE_EXTENDED).withDescriber(DESCRIBER); } @NonNull public CommandApdu createGetDataCommand(int dataObject) { int p1 = (dataObject & 0xFF00) >> 8; int p2 = dataObject & 0xFF; - return CommandApdu.create(CLA, INS_GET_DATA, p1, p2, MAX_APDU_NE_EXT).withDescriber(DESCRIBER); - } - - // ISO/IEC 7816-4 par.7.6.1 - @NonNull - public CommandApdu createGetResponseCommand(int lastResponseSw2) { - return CommandApdu.create(CLA, INS_GET_RESPONSE, P1_EMPTY, P2_EMPTY, lastResponseSw2).withDescriber(DESCRIBER); + return CommandApdu.create(CLA, INS_GET_DATA, p1, p2, CommandApdu.MAX_APDU_NE_EXTENDED).withDescriber(DESCRIBER); } @NonNull @@ -150,7 +136,7 @@ public CommandApdu createPutKeyCommand(byte[] keyBytes) { @NonNull public CommandApdu createComputeDigitalSignatureCommand(byte[] data) { return CommandApdu.create(CLA, INS_PERFORM_SECURITY_OPERATION, P1_PSO_COMPUTE_DIGITAL_SIGNATURE, - P2_PSO_COMPUTE_DIGITAL_SIGNATURE, data, MAX_APDU_NE_EXT).withDescriber(DESCRIBER); + P2_PSO_COMPUTE_DIGITAL_SIGNATURE, data, CommandApdu.MAX_APDU_NE_EXTENDED).withDescriber(DESCRIBER); } @NonNull @@ -194,31 +180,31 @@ public CommandApdu createReactivateCommand() { @NonNull public CommandApdu createInternalAuthForSecureMessagingCommand(byte[] authData) { return CommandApdu.create(CLA, INS_INTERNAL_AUTHENTICATE, P1_INTERNAL_AUTH_SECURE_MESSAGING, P2_EMPTY, authData, - MAX_APDU_NE_EXT).withDescriber(DESCRIBER); + CommandApdu.MAX_APDU_NE_EXTENDED).withDescriber(DESCRIBER); } @NonNull public CommandApdu createInternalAuthCommand(byte[] authData) { - return CommandApdu.create(CLA, INS_INTERNAL_AUTHENTICATE, P1_EMPTY, P2_EMPTY, authData, MAX_APDU_NE_EXT).withDescriber(DESCRIBER); + return CommandApdu.create(CLA, INS_INTERNAL_AUTHENTICATE, P1_EMPTY, P2_EMPTY, authData, CommandApdu.MAX_APDU_NE_EXTENDED).withDescriber(DESCRIBER); } @NonNull public CommandApdu createGenerateKeyCommand(int slot) { return CommandApdu.create(CLA, INS_GENERATE_RETRIEVE_ASYMMETRIC_KEY, - P1_GAKP_GENERATE, P2_EMPTY, new byte[] { (byte) slot, 0x00 }, MAX_APDU_NE_EXT).withDescriber(DESCRIBER); + P1_GAKP_GENERATE, P2_EMPTY, new byte[]{(byte) slot, 0x00}, CommandApdu.MAX_APDU_NE_EXTENDED).withDescriber(DESCRIBER); } @NonNull public CommandApdu createRetrievePublicKey(int slot) { return CommandApdu.create(CLA, INS_GENERATE_RETRIEVE_ASYMMETRIC_KEY, - P1_GAKP_READ_PUBKEY_TEMPLATE, P2_EMPTY, new byte[] { (byte) slot, 0x00 }, MAX_APDU_NE_EXT).withDescriber(DESCRIBER); + P1_GAKP_READ_PUBKEY_TEMPLATE, P2_EMPTY, new byte[]{(byte) slot, 0x00}, CommandApdu.MAX_APDU_NE_EXTENDED).withDescriber(DESCRIBER); } @NonNull public CommandApdu createRetrieveSecureMessagingPublicKeyCommand() { // see https://github.com/ANSSI-FR/SmartPGP/blob/master/secure_messaging/smartpgp_sm.pdf return CommandApdu.create(CLA, INS_GENERATE_RETRIEVE_ASYMMETRIC_KEY, P1_GAKP_READ_PUBKEY_TEMPLATE, P2_EMPTY, - CRT_GAKP_SECURE_MESSAGING, MAX_APDU_NE_EXT).withDescriber(DESCRIBER); + CRT_GAKP_SECURE_MESSAGING, CommandApdu.MAX_APDU_NE_EXTENDED).withDescriber(DESCRIBER); } @NonNull @@ -249,11 +235,24 @@ public CommandApdu createGetDataApplicationRelatedData() { return createGetDataCommand(DO_GET_DATA_APPLICATION_RELATED_DATA).withDescriber(DESCRIBER); } + // ISO/IEC 7816-4 + // SELECT command always as short APDU + @NonNull + public CommandApdu createSelectFileCommand(byte[] fileAid) { + return CommandApdu.create(CLA, INS_SELECT_FILE, P1_SELECT_FILE, P2_EMPTY, fileAid, CommandApdu.MAX_APDU_NE_SHORT).withDescriber(DESCRIBER); + } + + // ISO/IEC 7816-4 par.7.6.1 + @NonNull + public CommandApdu createGetResponseCommand(int lastResponseSw2) { + return CommandApdu.create(CLA, INS_GET_RESPONSE, P1_EMPTY, P2_EMPTY, lastResponseSw2).withDescriber(DESCRIBER); + } + // ISO/IEC 7816-4 @NonNull public CommandApdu createShortApdu(CommandApdu apdu) { - int ne = Math.min(apdu.getNe(), MAX_APDU_NE); - return CommandApdu.create(apdu.getCLA(), apdu.getINS(), apdu.getP1(), apdu.getP2(), apdu.getData(), ne).withDescriber(DESCRIBER); + int ne = Math.min(apdu.getNe(), CommandApdu.MAX_APDU_NE_SHORT); + return apdu.withNe(ne); } // ISO/IEC 7816-4 @@ -264,13 +263,13 @@ public List createChainedApdus(CommandApdu apdu) { int offset = 0; byte[] data = apdu.getData(); while (offset < data.length) { - int curLen = Math.min(MAX_APDU_NC, data.length - offset); + int curLen = Math.min(MAX_APDU_NC_SHORT_OPENPGP_WORKAROUND, data.length - offset); boolean last = offset + curLen >= data.length; int cla = apdu.getCLA() + (last ? 0 : MASK_CLA_CHAINING); CommandApdu cmd; if (last) { - int ne = Math.min(apdu.getNe(), MAX_APDU_NE); + int ne = Math.min(apdu.getNe(), CommandApdu.MAX_APDU_NE_SHORT); cmd = CommandApdu.create(cla, apdu.getINS(), apdu.getP1(), apdu.getP2(), data, offset, curLen, ne, DESCRIBER); } else { cmd = CommandApdu.create(cla, apdu.getINS(), apdu.getP1(), apdu.getP2(), data, offset, curLen, 0, DESCRIBER); @@ -284,6 +283,6 @@ public List createChainedApdus(CommandApdu apdu) { } public boolean isSuitableForShortApdu(CommandApdu apdu) { - return apdu.getData().length <= MAX_APDU_NC; + return apdu.getNc() <= MAX_APDU_NC_SHORT_OPENPGP_WORKAROUND; } } diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpSecurityKeyDialogPresenter.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpSecurityKeyDialogPresenter.java new file mode 100644 index 0000000..f7c6a20 --- /dev/null +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/OpenPgpSecurityKeyDialogPresenter.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.openpgp.internal; + +import android.content.Context; + +import androidx.annotation.RestrictTo; + +import java.io.IOException; + +import de.cotech.hw.SecurityKey; +import de.cotech.hw.SecurityKeyException; +import de.cotech.hw.exceptions.SecurityKeyDisconnectedException; +import de.cotech.hw.openpgp.OpenPgpSecurityKey; +import de.cotech.hw.openpgp.R; +import de.cotech.hw.openpgp.exceptions.OpenPgpLockedException; +import de.cotech.hw.openpgp.exceptions.OpenPgpPinTooShortException; +import de.cotech.hw.openpgp.exceptions.OpenPgpPublicKeyUnavailableException; +import de.cotech.hw.openpgp.exceptions.OpenPgpWrongPinException; +import de.cotech.hw.secrets.ByteSecret; +import de.cotech.hw.ui.SecurityKeyDialogOptions; +import de.cotech.hw.ui.internal.SecurityKeyDialogPresenter; +import de.cotech.hw.util.HwTimber; + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class OpenPgpSecurityKeyDialogPresenter extends SecurityKeyDialogPresenter { + + public OpenPgpSecurityKeyDialogPresenter(View view, Context context, SecurityKeyDialogOptions options) { + super(view, context, options); + } + + @Override + public void updateSecurityKeyPinUsingPuk(SecurityKey securityKey, ByteSecret puk, ByteSecret newPin) throws IOException { + OpenPgpSecurityKey openPgpSecurityKey = (OpenPgpSecurityKey) securityKey; + openPgpSecurityKey.updatePinUsingPuk(puk, newPin); + } + + @Override + public boolean isSecurityKeyEmpty(SecurityKey securityKey) throws IOException { + OpenPgpSecurityKey openPgpSecurityKey = (OpenPgpSecurityKey) securityKey; + return openPgpSecurityKey.isSecurityKeyEmpty(); + } + + @Override + public void handleError(IOException exception) { + HwTimber.d(exception); + + switch (currentScreen) { + case NORMAL_SECURITY_KEY: + case NORMAL_SECURITY_KEY_HOLD: { + try { + throw exception; + } catch (OpenPgpLockedException e) { + view.updateErrorViewText(R.string.hwsecurity_openpgp_error_no_pin_tries); + gotoScreen(Screen.NORMAL_ERROR); + } catch (OpenPgpWrongPinException e) { + if (e.getPinRetriesLeft() == 0) { + view.updateErrorViewText(R.string.hwsecurity_openpgp_error_no_pin_tries); + gotoScreen(Screen.NORMAL_ERROR); + } else { + gotoErrorScreenAndDelayedScreen(context.getString(R.string.hwsecurity_openpgp_error_wrong_pin, e.getPinRetriesLeft()), + Screen.NORMAL_ERROR, Screen.NORMAL_ENTER_PIN); + } + } catch (OpenPgpPinTooShortException e) { + gotoErrorScreenAndDelayedScreen(context.getString(R.string.hwsecurity_openpgp_error_too_short_pin), + Screen.NORMAL_ERROR, Screen.NORMAL_ENTER_PIN); + } catch (OpenPgpPublicKeyUnavailableException e) { + gotoErrorScreenAndDelayedScreen(context.getString(R.string.hwsecurity_openpgp_error_no_pubkey), + Screen.NORMAL_ERROR, Screen.NORMAL_SECURITY_KEY); + } catch (SecurityKeyException e) { + view.updateErrorViewText(context.getString(R.string.hwsecurity_openpgp_error_internal, e.getMessage())); + gotoScreen(Screen.NORMAL_ERROR); + } catch (SecurityKeyDisconnectedException e) { + // go back to start if we loose connection + gotoScreen(Screen.NORMAL_SECURITY_KEY); + } catch (IOException e) { + view.updateErrorViewText(context.getString(R.string.hwsecurity_openpgp_error_internal, e.getMessage())); + gotoScreen(Screen.NORMAL_ERROR); + } + break; + } + case RESET_PIN_SECURITY_KEY: { + try { + throw exception; + } catch (OpenPgpLockedException e) { + view.updateErrorViewText(R.string.hwsecurity_openpgp_error_no_puk_tries); + gotoScreen(Screen.RESET_PIN_ERROR); + } catch (OpenPgpWrongPinException e) { + if (e.getPukRetriesLeft() == 0) { + view.updateErrorViewText(R.string.hwsecurity_openpgp_error_no_puk_tries); + gotoScreen(Screen.RESET_PIN_ERROR); + } else { + gotoErrorScreenAndDelayedScreen(context.getString(R.string.hwsecurity_openpgp_error_wrong_puk, e.getPukRetriesLeft()), + Screen.RESET_PIN_ERROR, Screen.RESET_PIN_ENTER_PUK); + } + } catch (OpenPgpPinTooShortException e) { + gotoErrorScreenAndDelayedScreen(context.getString(R.string.hwsecurity_openpgp_error_too_short_puk), + Screen.RESET_PIN_ERROR, Screen.RESET_PIN_ENTER_PUK); + } catch (SecurityKeyException e) { + view.updateErrorViewText(context.getString(R.string.hwsecurity_openpgp_error_internal, e.getMessage())); + gotoScreen(Screen.RESET_PIN_ERROR); + } catch (SecurityKeyDisconnectedException e) { + // go back to start if we loose connection + gotoScreen(Screen.RESET_PIN_SECURITY_KEY); + } catch (IOException e) { + view.updateErrorViewText(context.getString(R.string.hwsecurity_openpgp_error_internal, e.getMessage())); + gotoScreen(Screen.RESET_PIN_ERROR); + } + break; + } + default: + HwTimber.d("handleError unhandled screen: %s", currentScreen.name()); + } + } + +} diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/ECKeyFormat.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/ECKeyFormat.java index 5c60ce5..2d0ca78 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/ECKeyFormat.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/ECKeyFormat.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/ECKeyFormatParser.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/ECKeyFormatParser.java index 13b47c7..3d7ff10 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/ECKeyFormatParser.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/ECKeyFormatParser.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/EdDSAKeyFormat.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/EdDSAKeyFormat.java index af74c71..532f502 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/EdDSAKeyFormat.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/EdDSAKeyFormat.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/KeyFormat.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/KeyFormat.java index 0025451..bc7d3b2 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/KeyFormat.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/KeyFormat.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/KeyFormatParser.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/KeyFormatParser.java index ea5406b..81105a3 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/KeyFormatParser.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/KeyFormatParser.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/KeyType.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/KeyType.java index a983d6c..a7575c0 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/KeyType.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/KeyType.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/OpenPgpAid.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/OpenPgpAid.java index 3f7e2e0..3d71f59 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/OpenPgpAid.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/OpenPgpAid.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/PgpFingerprintCalculator.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/PgpFingerprintCalculator.java index dc6372b..c027666 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/PgpFingerprintCalculator.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/PgpFingerprintCalculator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/PublicKeyAlgorithmTags.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/PublicKeyAlgorithmTags.java index a21ae8d..f6f2134 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/PublicKeyAlgorithmTags.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/PublicKeyAlgorithmTags.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/RSAKeyFormat.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/RSAKeyFormat.java index b59c7fd..6d61ef2 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/RSAKeyFormat.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/RSAKeyFormat.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/RSAKeyFormatParser.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/RSAKeyFormatParser.java index e162b76..c120fa9 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/RSAKeyFormatParser.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/openpgp/RSAKeyFormatParser.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ChangeKeyEccOp.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ChangeKeyEccOp.java index e0e099e..4850bea 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ChangeKeyEccOp.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ChangeKeyEccOp.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ChangeKeyRsaOp.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ChangeKeyRsaOp.java index 9c7f6da..dd72f41 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ChangeKeyRsaOp.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ChangeKeyRsaOp.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/InternalAuthenticateOp.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/InternalAuthenticateOp.java index fcf43c2..b471c48 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/InternalAuthenticateOp.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/InternalAuthenticateOp.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ModifyPinOp.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ModifyPinOp.java index 1024d89..c436e61 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ModifyPinOp.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ModifyPinOp.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/OpenPgpSignatureUtils.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/OpenPgpSignatureUtils.java index a360e36..0b7e1fa 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/OpenPgpSignatureUtils.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/OpenPgpSignatureUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/PsoDecryptOp.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/PsoDecryptOp.java index 45acda8..57278fe 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/PsoDecryptOp.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/PsoDecryptOp.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ResetAndWipeOp.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ResetAndWipeOp.java index cc271cb..92c771e 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ResetAndWipeOp.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/operations/ResetAndWipeOp.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/securemessaging/SCP11bSecureMessaging.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/securemessaging/SCP11bSecureMessaging.java index 778a936..79bbd35 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/securemessaging/SCP11bSecureMessaging.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/securemessaging/SCP11bSecureMessaging.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/securemessaging/SecureMessaging.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/securemessaging/SecureMessaging.java index 11ba5ca..4ce1c85 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/securemessaging/SecureMessaging.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/securemessaging/SecureMessaging.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/securemessaging/SecureMessagingException.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/securemessaging/SecureMessagingException.java index d6ea20c..eb826a6 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/securemessaging/SecureMessagingException.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/internal/securemessaging/SecureMessagingException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedDecryptor.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedDecryptor.java index fdfda37..a72e803 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedDecryptor.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedDecryptor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedEncryptor.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedEncryptor.java index d0caa0b..4820c23 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedEncryptor.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedEncryptor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKey.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKey.java index f53050e..8e8e098 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKey.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKey.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKeyException.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKeyException.java index b565fd4..9e28394 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKeyException.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKeyException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKeySerializer.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKeySerializer.java index 6548af5..928c6d0 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKeySerializer.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKeySerializer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKeySerializerImpl.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKeySerializerImpl.java index 129f932..b57bdf3 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKeySerializerImpl.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/pairedkey/PairedSecurityKeySerializerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/secrets/OpenPgpByteSecretGenerator.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/secrets/OpenPgpByteSecretGenerator.java new file mode 100644 index 0000000..fe88640 --- /dev/null +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/secrets/OpenPgpByteSecretGenerator.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.openpgp.secrets; + + +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.generators.HKDFBytesGenerator; +import org.bouncycastle.crypto.params.HKDFParameters; + +import java.util.Arrays; + +import de.cotech.hw.secrets.ByteSecret; + + +/** + * A generator for {@link ByteSecret} instances. + */ +public class OpenPgpByteSecretGenerator { + + public static OpenPgpByteSecretGenerator getInstance() { + return new OpenPgpByteSecretGenerator(); + } + + private OpenPgpByteSecretGenerator() { + } + + /** + * Derives a ByteSecret using SHA256, compatible to RFC 5869 + */ + @Deprecated + public ByteSecret deriveWithSaltAndConsume(ByteSecret secret, String salt, int length) { + byte[] secretBytes = null; + try { + secretBytes = secret.getByteCopyAndClear(); + + SHA256Digest digest = new SHA256Digest(); + HKDFBytesGenerator kDF1BytesGenerator = new HKDFBytesGenerator(digest); + + kDF1BytesGenerator.init(new HKDFParameters(secretBytes, salt.getBytes(), null)); + + byte[] derivedSecret = new byte[length]; + kDF1BytesGenerator.generateBytes(derivedSecret, 0, length); + + return ByteSecret.fromByteArrayAndClear(derivedSecret); + } finally { + zeroArrayQuietly(secretBytes); + } + } + + private void zeroArrayQuietly(byte[] secretBytes) { + if (secretBytes != null) { + Arrays.fill(secretBytes, (byte) 0); + } + } + + private void zeroArrayQuietly(char[] secretChars) { + if (secretChars != null) { + Arrays.fill(secretChars, '\0'); + } + } + +} diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/AndroidPreferencePairedSecurityKeyStorage.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/AndroidPreferencePairedSecurityKeyStorage.java index 04c84d8..8da4da6 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/AndroidPreferencePairedSecurityKeyStorage.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/AndroidPreferencePairedSecurityKeyStorage.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/AndroidPreferencesEncryptedSessionStorage.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/AndroidPreferencesEncryptedSessionStorage.java index 319b967..099c22c 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/AndroidPreferencesEncryptedSessionStorage.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/AndroidPreferencesEncryptedSessionStorage.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/EncryptedSessionStorage.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/EncryptedSessionStorage.java index 0ea78be..6e0d5fc 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/EncryptedSessionStorage.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/EncryptedSessionStorage.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/PairedSecurityKeyStorage.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/PairedSecurityKeyStorage.java index 436e403..61398e8 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/PairedSecurityKeyStorage.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/storage/PairedSecurityKeyStorage.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/DecryptingFileInputStream.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/DecryptingFileInputStream.java index bd34cac..28a4690 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/DecryptingFileInputStream.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/DecryptingFileInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/EncryptingFileOutputStream.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/EncryptingFileOutputStream.java index 69d02d2..c436ff0 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/EncryptingFileOutputStream.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/EncryptingFileOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/EphemeralFilePfdUtil.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/EphemeralFilePfdUtil.java index 10e0f30..fb6ae0e 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/EphemeralFilePfdUtil.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/EphemeralFilePfdUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/MemoryFilePfdUtil.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/MemoryFilePfdUtil.java index 76be9f8..e067751 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/MemoryFilePfdUtil.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/MemoryFilePfdUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/ParcelFileDescriptorUtil.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/ParcelFileDescriptorUtil.java index d7e6689..b810d3f 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/ParcelFileDescriptorUtil.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/ParcelFileDescriptorUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/RsaEncryptionUtil.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/RsaEncryptionUtil.java index 8bfa517..88cfef3 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/RsaEncryptionUtil.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/RsaEncryptionUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/package-info.java b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/package-info.java index 8e63e02..d8ce2b9 100644 --- a/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/package-info.java +++ b/hwsecurity/openpgp/src/main/java/de/cotech/hw/openpgp/util/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/openpgp/src/main/res/values/strings.xml b/hwsecurity/openpgp/src/main/res/values/strings.xml new file mode 100644 index 0000000..e855f66 --- /dev/null +++ b/hwsecurity/openpgp/src/main/res/values/strings.xml @@ -0,0 +1,13 @@ + + + + Wrong PIN, you have %d remaining attempts. + PIN too short! + You have entered an incorrect PIN 3 times!\nYou must reset the PIN to use the Security Key again. + Wrong PUK, you have %d remaining attempts. + PUK too short! + You have entered an incorrect PUK 3 times!\nThis Security Key is deactivated. Generate new keys to reset it. + No cryptographic key on this Security Key! + Internal error: %1$s + + \ No newline at end of file diff --git a/hwsecurity/piv/build.gradle b/hwsecurity/piv/build.gradle index 2ccedd1..68a68e5 100644 --- a/hwsecurity/piv/build.gradle +++ b/hwsecurity/piv/build.gradle @@ -1,12 +1,15 @@ apply plugin: 'com.android.library' -apply plugin: 'digital.wup.android-maven-publish' +apply plugin: 'maven-publish' apply plugin: 'org.jetbrains.dokka-android' dependencies { api project(':hwsecurity:core') api project(':hwsecurity:provider') - compileOnly 'androidx.annotation:annotation:1.0.0' + compileOnly project(':hwsecurity:ui') + compileOnly 'com.google.android.material:material:1.1.0' + + compileOnly 'androidx.annotation:annotation:1.1.0' api 'com.google.auto.value:auto-value-annotations:1.6.2' annotationProcessor 'com.google.auto.value:auto-value:1.6.2' @@ -36,55 +39,63 @@ android { } } -publishing { - publications { - mavenAar(MavenPublication) { - groupId = 'de.cotech' - artifactId = 'hwsecurity-piv' - version = android.defaultConfig.versionName +// https://developer.android.com/studio/build/maven-publish-plugin +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release - from components.android + groupId = 'de.cotech' + artifactId = 'hwsecurity-piv' + version = android.defaultConfig.versionName - pom { - url = 'https://hwsecurity.dev' - licenses { - license { - name = 'Commercial' - url = 'https://hwsecurity.dev/sales/' - distribution = 'repo' + pom { + url = 'https://hwsecurity.dev' + licenses { + license { + name = 'Commercial' + url = 'https://hwsecurity.dev/sales/' + distribution = 'repo' + } + license { + name = 'GNU General Public License, version 3' + url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + } + } + organization { + name = 'Confidential Technologies GmbH' + url = 'https://www.cotech.de' } - } - organization { - name = 'Confidential Technologies GmbH' - url = 'https://www.cotech.de' } } } - } - /* - * To upload release, create file gradle.properties in ~/.gradle/ with this content: - * - * cotechMavenName=xxx - * cotechMavenPassword=xxx - */ - if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { - println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" + /* + * To upload release, create file gradle.properties in ~/.gradle/ with this content: + * + * cotechMavenName=xxx + * cotechMavenPassword=xxx + */ + if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { + println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" - repositories { - maven { - credentials { - username cotechMavenName - password cotechMavenPassword + repositories { + maven { + credentials { + username cotechMavenName + password cotechMavenPassword + } + url = "https://maven.cotech.de" } - url = "https://maven.cotech.de" } } } } dokka { + moduleName = 'hwsecurity-piv' outputFormat = "hugo" - outputDirectory = "$buildDir/dokka/reference" + outputDirectory = "$projectDir/../../hwsecurity.dev/content/reference" sourceDirs = files('src/main/java') packageOptions { diff --git a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivKeyReference.java b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivKeyReference.java index 09450ec..86fcf79 100644 --- a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivKeyReference.java +++ b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivKeyReference.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivSecurityKey.java b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivSecurityKey.java index 87b13a8..336e17e 100644 --- a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivSecurityKey.java +++ b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivSecurityKey.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivSecurityKeyAuthenticator.java b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivSecurityKeyAuthenticator.java index b6e8a49..2cf2b57 100644 --- a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivSecurityKeyAuthenticator.java +++ b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivSecurityKeyAuthenticator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivSecurityKeyConnectionMode.java b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivSecurityKeyConnectionMode.java index b2e01c2..f50d197 100644 --- a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivSecurityKeyConnectionMode.java +++ b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/PivSecurityKeyConnectionMode.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -44,8 +44,8 @@ public class PivSecurityKeyConnectionMode extends SecurityKeyConnectionMode. */ -package de.cotech.hw.ui.internal; +package de.cotech.hw.piv; +import android.content.Context; import android.os.Bundle; -import androidx.annotation.RestrictTo; -import de.cotech.hw.SecurityKey; + +import androidx.annotation.NonNull; + import de.cotech.hw.SecurityKeyManager; -import de.cotech.hw.piv.PivSecurityKey; -import de.cotech.hw.piv.PivSecurityKeyConnectionMode; -import de.cotech.hw.secrets.ByteSecret; +import de.cotech.hw.piv.internal.PivSecurityKeyDialogPresenter; import de.cotech.hw.ui.SecurityKeyDialogFragment; +import de.cotech.hw.ui.SecurityKeyDialogOptions; +import de.cotech.hw.ui.internal.SecurityKeyDialogPresenter; -import java.io.IOException; - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class PivSecurityKeyDialogFragment extends SecurityKeyDialogFragment { - @Override - public void initSecurityKeyConnectionMode(Bundle arguments) { - SecurityKeyManager.getInstance().registerCallback(new PivSecurityKeyConnectionMode(), this, this); + public static SecurityKeyDialogFragment newInstance() { + return newInstance(SecurityKeyDialogOptions.builder().build()); + } + + public static SecurityKeyDialogFragment newInstance(@NonNull SecurityKeyDialogOptions options) { + try { + Class.forName("de.cotech.hw.ui.SecurityKeyDialogFragment"); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("You must include the hwsecurity-ui Maven artifact!"); + } + + Bundle args = new Bundle(); + args.putParcelable(PivSecurityKeyDialogFragment.ARG_DIALOG_OPTIONS, options); + + PivSecurityKeyDialogFragment fragment = new PivSecurityKeyDialogFragment(); + fragment.setArguments(args); + return fragment; } @Override - public void updateSecurityKeyPinUsingPuk(SecurityKey securityKey, ByteSecret puk, ByteSecret newPin) throws IOException { - PivSecurityKey pivSecurityKey = (PivSecurityKey) securityKey; - pivSecurityKey.updatePinUsingPuk(puk, newPin); + public void initSecurityKeyConnectionMode(Bundle arguments) { + SecurityKeyManager.getInstance().registerCallback(new PivSecurityKeyConnectionMode(), this, this); } @Override - public boolean isSecurityKeyEmpty(SecurityKey securityKey) throws IOException { - throw new UnsupportedOperationException("SETUP mode is not supported in PIV mode"); + public SecurityKeyDialogPresenter initPresenter(SecurityKeyDialogPresenter.View view, Context context, SecurityKeyDialogOptions options) { + return new PivSecurityKeyDialogPresenter(this, getActivity(), options); } } diff --git a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/exceptions/PivWrongPinException.java b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/exceptions/PivWrongPinException.java index 8cb899b..9290921 100644 --- a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/exceptions/PivWrongPinException.java +++ b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/exceptions/PivWrongPinException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivAppletConnection.java b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivAppletConnection.java index 0962853..2cf3a52 100644 --- a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivAppletConnection.java +++ b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivAppletConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -33,7 +33,6 @@ import de.cotech.hw.exceptions.AppletFileNotFoundException; import de.cotech.hw.exceptions.ClaNotSupportedException; import de.cotech.hw.exceptions.ConditionsNotSatisfiedException; -import de.cotech.hw.exceptions.FileInTerminationStateException; import de.cotech.hw.exceptions.InsNotSupportedException; import de.cotech.hw.exceptions.SelectAppletException; import de.cotech.hw.internal.iso7816.CommandApdu; @@ -146,21 +145,6 @@ void determineSecurityKeyType() { return; } -// CommandApdu selectFidesmoApdu = commandFactory.createSelectFileCommand(AID_PREFIX_FIDESMO); -// if (communicate(selectFidesmoApdu).isSuccess()) { -// securityKeyType = SecurityKeyType.FIDESMO; -// return; -// } - - /* We could determine if this is a yubikey here. The info isn't used at the moment, so we save the roundtrip - // AID from https://github.com/Yubico/ykneo-oath/blob/master/build.xml#L16 - CommandApdu selectYubicoApdu = commandFactory.createSelectFileCommand("A000000527200101"); - if (communicate(selectYubicoApdu).isSuccess()) { - securityKeyType = SecurityKeyType.YUBIKEY_UNKNOWN; - return; - } - */ - securityKeyType = SecurityKeyType.UNKNOWN; } @@ -176,25 +160,6 @@ public byte[] getConnectedAppletAid() { // region communication - /** - * Transceives APDU - * Splits extended APDU into short APDUs and chains them if necessary - * Performs GET RESPONSE command(ISO/IEC 7816-4 par.7.6.1) on retrieving if necessary - * - * @param commandApdu short or extended APDU to transceive - * @return response from the card - */ - public ResponseApdu communicate(CommandApdu commandApdu) throws IOException { - ResponseApdu lastResponse; - - lastResponse = transceiveWithChaining(commandApdu); - if (lastResponse.getSw1() == RESPONSE_SW1_INCORRECT_LENGTH && lastResponse.getSw2() != 0) { - commandApdu = commandApdu.withNe(lastResponse.getSw2()); - lastResponse = transceiveWithChaining(commandApdu); - } - return readChainedResponseIfAvailable(lastResponse); - } - public ResponseApdu communicateOrThrow(CommandApdu commandApdu) throws IOException { ResponseApdu response = communicate(commandApdu); @@ -223,15 +188,30 @@ public ResponseApdu communicateOrThrow(CommandApdu commandApdu) throws IOExcepti } } + /** + * Transceives APDU + * Splits extended APDU into short APDUs and chains them if necessary + * Performs GET RESPONSE command(ISO/IEC 7816-4 par.7.6.1) on retrieving if necessary + * + * @param commandApdu short or extended APDU to transceive + * @return response from the card + */ + public ResponseApdu communicate(CommandApdu commandApdu) throws IOException { + ResponseApdu lastResponse; + + lastResponse = transceiveWithChaining(commandApdu); + if (lastResponse.getSw1() == RESPONSE_SW1_INCORRECT_LENGTH && lastResponse.getSw2() != 0) { + commandApdu = commandApdu.withNe(lastResponse.getSw2()); + lastResponse = transceiveWithChaining(commandApdu); + } + return readChainedResponseIfAvailable(lastResponse); + } + @NonNull private ResponseApdu transceiveWithChaining(CommandApdu commandApdu) throws IOException { - /* if (cardCapabilities.hasExtended()) { - return transport.transceive(commandApdu); - } else */ - - if (commandFactory.isSuitableForShortApdu(commandApdu)) { - CommandApdu shortApdu = commandFactory.createShortApdu(commandApdu); - return transport.transceive(shortApdu); + // NOTE: Currently always using short APDUs for PIV + if (commandFactory.isSuitableForSingleShortApdu(commandApdu)) { + return transport.transceive(commandApdu.withShortApduNe()); } ResponseApdu lastResponse = null; @@ -242,7 +222,7 @@ private ResponseApdu transceiveWithChaining(CommandApdu commandApdu) throws IOEx boolean isLastCommand = (i == totalCommands - 1); if (!isLastCommand && !lastResponse.isSuccess()) { - throw new IOException("Failed to chain apdu " + + throw new IOException("Failed to chain APDU " + "(" + i + "/" + (totalCommands-1) + ", last SW: " + Integer.toHexString(lastResponse.getSw()) + ")"); } } diff --git a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivCommandApduDescriber.java b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivCommandApduDescriber.java index bf679df..b8c3d1a 100644 --- a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivCommandApduDescriber.java +++ b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivCommandApduDescriber.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivCommandApduFactory.java b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivCommandApduFactory.java index 8e88550..6fe4fbc 100644 --- a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivCommandApduFactory.java +++ b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivCommandApduFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -38,12 +38,6 @@ public class PivCommandApduFactory { private static final PivCommandApduDescriber DESCRIBER = new PivCommandApduDescriber(); - private static final int MAX_APDU_NC = 255; - private static final int MAX_APDU_NC_EXT = 65535; - - private static final int MAX_APDU_NE = 256; - private static final int MAX_APDU_NE_EXT = 65536; - private static final int CLA = 0x00; private static final int MASK_CLA_CHAINING = 1 << 4; @@ -73,12 +67,6 @@ public class PivCommandApduFactory { private static final int P1_AUTHENTICATE_P384 = 0x14; private static final int P2_AUTHENTICATE = 0x9A; - // ISO/IEC 7816-4 - @NonNull - public CommandApdu createSelectFileCommand(byte[] fileAid) { - return CommandApdu.create(CLA, INS_SELECT_FILE, P1_SELECT_FILE, P2_EMPTY, fileAid).withDescriber(DESCRIBER); - } - @NonNull public CommandApdu createGetDataCommand(byte[] dataObject) { return CommandApdu.create(CLA, INS_GET_DATA, P1_GET_DATA, P2_GET_DATA, dataObject, 0).withDescriber(DESCRIBER); @@ -86,7 +74,7 @@ public CommandApdu createGetDataCommand(byte[] dataObject) { @NonNull public CommandApdu createVerifyCommand(int slot, byte[] data) { - return CommandApdu.create(CLA, INS_VERIFY, 0x00, slot, data).withDescriber(DESCRIBER); + return CommandApdu.create(CLA, INS_VERIFY, P1_EMPTY, slot, data).withDescriber(DESCRIBER); } @NonNull @@ -109,17 +97,17 @@ public CommandApdu createResetRetryCounter(byte[] data) { return CommandApdu.create(CLA, INS_RESET_RETRY_COUNTER, P1_EMPTY, P2_RESET_RETRY_COUNTER_CARD_APPLICATION_PIN, data).withDescriber(DESCRIBER); } - // ISO/IEC 7816-4 par.7.6.1 + // ISO/IEC 7816-4 + // SELECT command always as short APDU @NonNull - public CommandApdu createGetResponseCommand(int lastResponseSw2) { - return CommandApdu.create(CLA, INS_GET_RESPONSE, P1_EMPTY, P2_EMPTY, lastResponseSw2).withDescriber(DESCRIBER); + public CommandApdu createSelectFileCommand(byte[] fileAid) { + return CommandApdu.create(CLA, INS_SELECT_FILE, P1_SELECT_FILE, P2_EMPTY, fileAid, CommandApdu.MAX_APDU_NE_SHORT).withDescriber(DESCRIBER); } - // ISO/IEC 7816-4 + // ISO/IEC 7816-4 par.7.6.1 @NonNull - public CommandApdu createShortApdu(CommandApdu apdu) { - int ne = Math.min(apdu.getNe(), MAX_APDU_NE); - return CommandApdu.create(apdu.getCLA(), apdu.getINS(), apdu.getP1(), apdu.getP2(), apdu.getData(), ne).withDescriber(DESCRIBER); + public CommandApdu createGetResponseCommand(int lastResponseSw2) { + return CommandApdu.create(CLA, INS_GET_RESPONSE, P1_EMPTY, P2_EMPTY, lastResponseSw2).withDescriber(DESCRIBER); } // ISO/IEC 7816-4 @@ -130,13 +118,14 @@ public List createChainedApdus(CommandApdu apdu) { int offset = 0; byte[] data = apdu.getData(); while (offset < data.length) { - int curLen = Math.min(MAX_APDU_NC, data.length - offset); + int curLen = Math.min(CommandApdu.MAX_APDU_NC_SHORT, data.length - offset); boolean last = offset + curLen >= data.length; int cla = apdu.getCLA() + (last ? 0 : MASK_CLA_CHAINING); CommandApdu cmd; if (last) { - int ne = Math.min(apdu.getNe(), MAX_APDU_NE); + // TODO: check this! + int ne = Math.min(apdu.getNe(), CommandApdu.MAX_APDU_NE_SHORT); cmd = CommandApdu.create(cla, apdu.getINS(), apdu.getP1(), apdu.getP2(), data, offset, curLen, ne, DESCRIBER); } else { cmd = CommandApdu.create(cla, apdu.getINS(), apdu.getP1(), apdu.getP2(), data, offset, curLen, 0, DESCRIBER); @@ -149,7 +138,7 @@ public List createChainedApdus(CommandApdu apdu) { return result; } - public boolean isSuitableForShortApdu(CommandApdu apdu) { - return apdu.getData().length <= MAX_APDU_NC; + public boolean isSuitableForSingleShortApdu(CommandApdu apdu) { + return apdu.getNc() <= CommandApdu.MAX_APDU_NC_SHORT; } } diff --git a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivPinFormatter.java b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivPinFormatter.java index ffc8ff7..46aad31 100644 --- a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivPinFormatter.java +++ b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivPinFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivSecurityKeyDialogPresenter.java b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivSecurityKeyDialogPresenter.java new file mode 100644 index 0000000..5de0d01 --- /dev/null +++ b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/PivSecurityKeyDialogPresenter.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.piv.internal; + +import android.content.Context; + +import androidx.annotation.RestrictTo; + +import java.io.IOException; + +import de.cotech.hw.SecurityKey; +import de.cotech.hw.SecurityKeyException; +import de.cotech.hw.exceptions.SecurityKeyDisconnectedException; +import de.cotech.hw.piv.PivSecurityKey; +import de.cotech.hw.piv.R; +import de.cotech.hw.piv.exceptions.PivWrongPinException; +import de.cotech.hw.secrets.ByteSecret; +import de.cotech.hw.ui.SecurityKeyDialogOptions; +import de.cotech.hw.ui.internal.SecurityKeyDialogPresenter; +import de.cotech.hw.util.HwTimber; + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class PivSecurityKeyDialogPresenter extends SecurityKeyDialogPresenter { + + public PivSecurityKeyDialogPresenter(View view, Context context, SecurityKeyDialogOptions options) { + super(view, context, options); + } + + @Override + public void updateSecurityKeyPinUsingPuk(SecurityKey securityKey, ByteSecret puk, ByteSecret newPin) throws IOException { + PivSecurityKey pivSecurityKey = (PivSecurityKey) securityKey; + pivSecurityKey.updatePinUsingPuk(puk, newPin); + } + + @Override + public boolean isSecurityKeyEmpty(SecurityKey securityKey) throws IOException { + HwTimber.e("SETUP mode is not supported in PIV mode"); + return true; + } + + @Override + public void handleError(IOException exception) { + HwTimber.d(exception); + + switch (currentScreen) { + case NORMAL_SECURITY_KEY: + case NORMAL_SECURITY_KEY_HOLD: { + try { + throw exception; + } catch (PivWrongPinException e) { + gotoErrorScreenAndDelayedScreen(context.getString(R.string.hwsecurity_piv_error_wrong_pin, e.getRetriesLeft()), + Screen.NORMAL_ERROR, Screen.NORMAL_ENTER_PIN); + } catch (SecurityKeyException e) { + view.updateErrorViewText(context.getString(R.string.hwsecurity_piv_error_internal, e.getMessage())); + gotoScreen(Screen.NORMAL_ERROR); + } catch (SecurityKeyDisconnectedException e) { + // go back to start if we loose connection + gotoScreen(Screen.NORMAL_SECURITY_KEY); + } catch (IOException e) { + view.updateErrorViewText(context.getString(R.string.hwsecurity_piv_error_internal, e.getMessage())); + gotoScreen(Screen.NORMAL_ERROR); + } + break; + } + case RESET_PIN_SECURITY_KEY: { + try { + throw exception; + } catch (PivWrongPinException e) { + gotoErrorScreenAndDelayedScreen(context.getString(R.string.hwsecurity_piv_error_wrong_puk, e.getRetriesLeft()), + Screen.RESET_PIN_ERROR, Screen.RESET_PIN_ENTER_PUK); + } catch (SecurityKeyException e) { + view.updateErrorViewText(context.getString(R.string.hwsecurity_piv_error_internal, e.getMessage())); + gotoScreen(Screen.RESET_PIN_ERROR); + } catch (SecurityKeyDisconnectedException e) { + // go back to start if we loose connection + gotoScreen(Screen.RESET_PIN_SECURITY_KEY); + } catch (IOException e) { + view.updateErrorViewText(context.getString(R.string.hwsecurity_piv_error_internal, e.getMessage())); + gotoScreen(Screen.RESET_PIN_ERROR); + } + break; + } + default: + HwTimber.d("handleError unhandled screen: %s", currentScreen.name()); + } + } + +} diff --git a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/operations/GeneralAuthenticateOp.java b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/operations/GeneralAuthenticateOp.java index 5713d06..658228b 100644 --- a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/operations/GeneralAuthenticateOp.java +++ b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/operations/GeneralAuthenticateOp.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/operations/PivSignatureUtils.java b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/operations/PivSignatureUtils.java index ef1e0d1..0c1e95a 100644 --- a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/operations/PivSignatureUtils.java +++ b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/operations/PivSignatureUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/operations/ResetRetryCounterOp.java b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/operations/ResetRetryCounterOp.java index 4725d04..5bb05e0 100644 --- a/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/operations/ResetRetryCounterOp.java +++ b/hwsecurity/piv/src/main/java/de/cotech/hw/piv/internal/operations/ResetRetryCounterOp.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/piv/src/main/res/values/strings.xml b/hwsecurity/piv/src/main/res/values/strings.xml new file mode 100644 index 0000000..ddc6037 --- /dev/null +++ b/hwsecurity/piv/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + + + Wrong PIN, you have %d remaining attempts. + Wrong PUK, you have %d remaining attempts. + Internal error: %1$s + + \ No newline at end of file diff --git a/hwsecurity/provider/build.gradle b/hwsecurity/provider/build.gradle index d18d884..190a026 100644 --- a/hwsecurity/provider/build.gradle +++ b/hwsecurity/provider/build.gradle @@ -1,11 +1,11 @@ apply plugin: 'com.android.library' -apply plugin: 'digital.wup.android-maven-publish' +apply plugin: 'maven-publish' apply plugin: 'org.jetbrains.dokka-android' dependencies { api project(':hwsecurity:core') - compileOnly 'androidx.annotation:annotation:1.0.0' + compileOnly 'androidx.annotation:annotation:1.1.0' api 'com.google.auto.value:auto-value-annotations:1.6.2' annotationProcessor 'com.google.auto.value:auto-value:1.6.2' @@ -31,55 +31,63 @@ android { } } -publishing { - publications { - mavenAar(MavenPublication) { - groupId = 'de.cotech' - artifactId = 'hwsecurity-provider' - version = android.defaultConfig.versionName +// https://developer.android.com/studio/build/maven-publish-plugin +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release - from components.android + groupId = 'de.cotech' + artifactId = 'hwsecurity-provider' + version = android.defaultConfig.versionName - pom { - url = 'https://hwsecurity.dev' - licenses { - license { - name = 'Commercial' - url = 'https://hwsecurity.dev/sales/' - distribution = 'repo' + pom { + url = 'https://hwsecurity.dev' + licenses { + license { + name = 'Commercial' + url = 'https://hwsecurity.dev/sales/' + distribution = 'repo' + } + license { + name = 'GNU General Public License, version 3' + url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + } + } + organization { + name = 'Confidential Technologies GmbH' + url = 'https://www.cotech.de' } - } - organization { - name = 'Confidential Technologies GmbH' - url = 'https://www.cotech.de' } } } - } - /* - * To upload release, create file gradle.properties in ~/.gradle/ with this content: - * - * cotechMavenName=xxx - * cotechMavenPassword=xxx - */ - if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { - println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" + /* + * To upload release, create file gradle.properties in ~/.gradle/ with this content: + * + * cotechMavenName=xxx + * cotechMavenPassword=xxx + */ + if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { + println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" - repositories { - maven { - credentials { - username cotechMavenName - password cotechMavenPassword + repositories { + maven { + credentials { + username cotechMavenName + password cotechMavenPassword + } + url = "https://maven.cotech.de" } - url = "https://maven.cotech.de" } } } } dokka { + moduleName = 'hwsecurity-provider' outputFormat = "hugo" - outputDirectory = "$buildDir/dokka/reference" + outputDirectory = "$projectDir/../../hwsecurity.dev/content/reference" sourceDirs = files('src/main/java') packageOptions { diff --git a/hwsecurity/provider/src/main/java/de/cotech/hw/provider/CotechSecurityKeyProvider.java b/hwsecurity/provider/src/main/java/de/cotech/hw/provider/CotechSecurityKeyProvider.java index cf97631..b8fdb72 100644 --- a/hwsecurity/provider/src/main/java/de/cotech/hw/provider/CotechSecurityKeyProvider.java +++ b/hwsecurity/provider/src/main/java/de/cotech/hw/provider/CotechSecurityKeyProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/provider/src/main/java/de/cotech/hw/provider/SecurityKeyPrivateKey.java b/hwsecurity/provider/src/main/java/de/cotech/hw/provider/SecurityKeyPrivateKey.java index 78bdf69..5a96e83 100644 --- a/hwsecurity/provider/src/main/java/de/cotech/hw/provider/SecurityKeyPrivateKey.java +++ b/hwsecurity/provider/src/main/java/de/cotech/hw/provider/SecurityKeyPrivateKey.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/provider/src/main/java/de/cotech/hw/provider/SecurityKeySignature.java b/hwsecurity/provider/src/main/java/de/cotech/hw/provider/SecurityKeySignature.java index 5745bb3..3c5e7e9 100644 --- a/hwsecurity/provider/src/main/java/de/cotech/hw/provider/SecurityKeySignature.java +++ b/hwsecurity/provider/src/main/java/de/cotech/hw/provider/SecurityKeySignature.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/ssh/build.gradle b/hwsecurity/ssh/build.gradle index 44a380d..57f7edb 100644 --- a/hwsecurity/ssh/build.gradle +++ b/hwsecurity/ssh/build.gradle @@ -1,13 +1,13 @@ apply plugin: 'com.android.library' -apply plugin: 'digital.wup.android-maven-publish' +apply plugin: 'maven-publish' apply plugin: 'org.jetbrains.dokka-android' dependencies { api project(':hwsecurity:core') - implementation 'org.bouncycastle:bcprov-jdk15on:1.64' + implementation 'org.bouncycastle:bcprov-jdk15on:1.65' - compileOnly 'androidx.annotation:annotation:1.0.0' + compileOnly 'androidx.annotation:annotation:1.1.0' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.18.0' @@ -33,55 +33,63 @@ android { } } -publishing { - publications { - mavenAar(MavenPublication) { - groupId = 'de.cotech' - artifactId = 'hwsecurity-ssh' - version = android.defaultConfig.versionName +// https://developer.android.com/studio/build/maven-publish-plugin +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release - from components.android + groupId = 'de.cotech' + artifactId = 'hwsecurity-ssh' + version = android.defaultConfig.versionName - pom { - url = 'https://hwsecurity.dev' - licenses { - license { - name = 'Commercial' - url = 'https://hwsecurity.dev/sales/' - distribution = 'repo' + pom { + url = 'https://hwsecurity.dev' + licenses { + license { + name = 'Commercial' + url = 'https://hwsecurity.dev/sales/' + distribution = 'repo' + } + license { + name = 'GNU General Public License, version 3' + url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + } + } + organization { + name = 'Confidential Technologies GmbH' + url = 'https://www.cotech.de' } - } - organization { - name = 'Confidential Technologies GmbH' - url = 'https://www.cotech.de' } } } - } - /* - * To upload release, create file gradle.properties in ~/.gradle/ with this content: - * - * cotechMavenName=xxx - * cotechMavenPassword=xxx - */ - if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { - println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" + /* + * To upload release, create file gradle.properties in ~/.gradle/ with this content: + * + * cotechMavenName=xxx + * cotechMavenPassword=xxx + */ + if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { + println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" - repositories { - maven { - credentials { - username cotechMavenName - password cotechMavenPassword + repositories { + maven { + credentials { + username cotechMavenName + password cotechMavenPassword + } + url = "https://maven.cotech.de" } - url = "https://maven.cotech.de" } } } } dokka { + moduleName = 'hwsecurity-ssh' outputFormat = "hugo" - outputDirectory = "$buildDir/dokka/reference" + outputDirectory = "$projectDir/../../hwsecurity.dev/content/reference" sourceDirs = files('src/main/java') } diff --git a/hwsecurity/ssh/src/main/java/de/cotech/hw/ssh/SecurityKeySshAuthenticator.java b/hwsecurity/ssh/src/main/java/de/cotech/hw/ssh/SecurityKeySshAuthenticator.java index 93a64b9..fbfa23b 100644 --- a/hwsecurity/ssh/src/main/java/de/cotech/hw/ssh/SecurityKeySshAuthenticator.java +++ b/hwsecurity/ssh/src/main/java/de/cotech/hw/ssh/SecurityKeySshAuthenticator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/ssh/src/main/java/de/cotech/hw/ssh/SshEncodedData.java b/hwsecurity/ssh/src/main/java/de/cotech/hw/ssh/SshEncodedData.java index 46b6743..e3c1ba4 100644 --- a/hwsecurity/ssh/src/main/java/de/cotech/hw/ssh/SshEncodedData.java +++ b/hwsecurity/ssh/src/main/java/de/cotech/hw/ssh/SshEncodedData.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/ssh/src/main/java/de/cotech/hw/ssh/SshUtil.java b/hwsecurity/ssh/src/main/java/de/cotech/hw/ssh/SshUtil.java index 87aa0f1..2241540 100644 --- a/hwsecurity/ssh/src/main/java/de/cotech/hw/ssh/SshUtil.java +++ b/hwsecurity/ssh/src/main/java/de/cotech/hw/ssh/SshUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/ssh/src/test/java/de/cotech/hw/ssh/JcaTestUtils.java b/hwsecurity/ssh/src/test/java/de/cotech/hw/ssh/JcaTestUtils.java index 9b6a4fd..483930b 100644 --- a/hwsecurity/ssh/src/test/java/de/cotech/hw/ssh/JcaTestUtils.java +++ b/hwsecurity/ssh/src/test/java/de/cotech/hw/ssh/JcaTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/ssh/src/test/java/de/cotech/hw/ssh/SshUtilTest.java b/hwsecurity/ssh/src/test/java/de/cotech/hw/ssh/SshUtilTest.java index 483b138..77c103e 100644 --- a/hwsecurity/ssh/src/test/java/de/cotech/hw/ssh/SshUtilTest.java +++ b/hwsecurity/ssh/src/test/java/de/cotech/hw/ssh/SshUtilTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/ui/build.gradle b/hwsecurity/ui/build.gradle index ff3eadc..4d5bc29 100644 --- a/hwsecurity/ui/build.gradle +++ b/hwsecurity/ui/build.gradle @@ -1,17 +1,13 @@ apply plugin: 'com.android.library' -apply plugin: 'digital.wup.android-maven-publish' +apply plugin: 'maven-publish' apply plugin: 'org.jetbrains.dokka-android' dependencies { - compileOnly project(':hwsecurity:openpgp') - compileOnly project(':hwsecurity:piv') - - implementation 'de.cotech:nfc-sweetspot:1.1' - - implementation 'com.google.android.material:material:1.0.0' + api project(':hwsecurity:core') + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'androidx.appcompat:appcompat:1.0.2' api 'com.google.auto.value:auto-value-annotations:1.6.2' annotationProcessor 'com.google.auto.value:auto-value:1.6.2' @@ -38,55 +34,63 @@ android { } } -publishing { - publications { - mavenAar(MavenPublication) { - groupId = 'de.cotech' - artifactId = 'hwsecurity-ui' - version = android.defaultConfig.versionName +// https://developer.android.com/studio/build/maven-publish-plugin +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release - from components.android + groupId = 'de.cotech' + artifactId = 'hwsecurity-ui' + version = android.defaultConfig.versionName - pom { - url = 'https://hwsecurity.dev' - licenses { - license { - name = 'Commercial' - url = 'https://hwsecurity.dev/sales/' - distribution = 'repo' + pom { + url = 'https://hwsecurity.dev' + licenses { + license { + name = 'Commercial' + url = 'https://hwsecurity.dev/sales/' + distribution = 'repo' + } + license { + name = 'GNU General Public License, version 3' + url = 'https://www.gnu.org/licenses/gpl-3.0.txt' + } + } + organization { + name = 'Confidential Technologies GmbH' + url = 'https://www.cotech.de' } - } - organization { - name = 'Confidential Technologies GmbH' - url = 'https://www.cotech.de' } } } - } - /* - * To upload release, create file gradle.properties in ~/.gradle/ with this content: - * - * cotechMavenName=xxx - * cotechMavenPassword=xxx - */ - if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { - println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" + /* + * To upload release, create file gradle.properties in ~/.gradle/ with this content: + * + * cotechMavenName=xxx + * cotechMavenPassword=xxx + */ + if (project.hasProperty('cotechMavenName') && project.hasProperty('cotechMavenPassword')) { + println "Found cotechMavenName, cotechMavenPassword in gradle.properties!" - repositories { - maven { - credentials { - username cotechMavenName - password cotechMavenPassword + repositories { + maven { + credentials { + username cotechMavenName + password cotechMavenPassword + } + url = "https://maven.cotech.de" } - url = "https://maven.cotech.de" } } } } dokka { + moduleName = 'hwsecurity-ui' outputFormat = "hugo" - outputDirectory = "$buildDir/dokka/reference" + outputDirectory = "$projectDir/../../hwsecurity.dev/content/reference" sourceDirs = files('src/main/java') packageOptions { diff --git a/hwsecurity/core/src/main/assets/hwsecurity-nfc-sweetspots.json b/hwsecurity/ui/src/main/assets/hwsecurity-nfc-sweetspots.json similarity index 100% rename from hwsecurity/core/src/main/assets/hwsecurity-nfc-sweetspots.json rename to hwsecurity/ui/src/main/assets/hwsecurity-nfc-sweetspots.json diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogFragment.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogFragment.java index 32f5b12..46e53ca 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogFragment.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -35,7 +35,9 @@ import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.widget.*; +import android.widget.CheckBox; +import android.widget.FrameLayout; +import android.widget.TextView; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; @@ -60,18 +62,19 @@ import de.cotech.hw.SecurityKey; import de.cotech.hw.SecurityKeyCallback; -import de.cotech.hw.SecurityKeyException; import de.cotech.hw.SecurityKeyManager; -import de.cotech.hw.exceptions.SecurityKeyLostException; -import de.cotech.hw.openpgp.exceptions.OpenPgpLockedException; -import de.cotech.hw.openpgp.exceptions.OpenPgpPinTooShortException; -import de.cotech.hw.openpgp.exceptions.OpenPgpPublicKeyUnavailableException; -import de.cotech.hw.openpgp.exceptions.OpenPgpWrongPinException; -import de.cotech.hw.openpgp.secrets.ByteSecretGenerator; -import de.cotech.hw.piv.exceptions.PivWrongPinException; import de.cotech.hw.secrets.ByteSecret; -import de.cotech.hw.secrets.StaticPinProvider; -import de.cotech.hw.ui.internal.*; +import de.cotech.hw.secrets.PinProvider; +import de.cotech.hw.ui.internal.ErrorView; +import de.cotech.hw.ui.internal.KeyboardPinInput; +import de.cotech.hw.ui.internal.KeypadPinInput; +import de.cotech.hw.ui.internal.NfcFullscreenView; +import de.cotech.hw.ui.internal.PinInput; +import de.cotech.hw.ui.internal.ProgressView; +import de.cotech.hw.ui.internal.SecurityKeyDialogPresenter; +import de.cotech.hw.ui.internal.SecurityKeyFormFactor; +import de.cotech.hw.ui.internal.SmartcardFormFactor; +import de.cotech.hw.ui.internal.WipeConfirmView; import de.cotech.hw.util.HwTimber; /** @@ -81,48 +84,36 @@ * Use the SecurityKeyDialogFactory to instantiate this. */ public abstract class SecurityKeyDialogFragment extends BottomSheetDialogFragment - implements SecurityKeyCallback, PinInput.PinInputCallback, SecurityKeyDialogInterface { + implements SecurityKeyCallback, PinInput.PinInputCallback, SecurityKeyDialogInterface, SecurityKeyDialogPresenter.View, SecurityKeyFormFactor.SelectTransportCallback { @SuppressWarnings("WeakerAccess") // public API public static final String FRAGMENT_TAG = "security-key-dialog-fragment"; public static final String ARG_DIALOG_OPTIONS = "de.cotech.hw.ui.ARG_DIALOG_OPTIONS"; - private static final long TIME_DELAYED_STATE_CHANGE = 3000; - - private static final int SETUP_DEFAULT_PIN_LENGTH = 6; - private static final int SETUP_DEFAULT_PUK_LENGTH = 8; - static { AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); } + private SecurityKeyDialogPresenter presenter; + private SecurityKeyDialogOptions options; private SecurityKeyDialogInterface.SecurityKeyDialogCallback callback; - private KeyboardPreference keyboardPreference; - private CoordinatorLayout coordinator; private FrameLayout bottomSheet; private ConstraintLayout innerBottomSheet; - private TextView textViewTitle; - private TextView textViewDescription; - - private MaterialButton buttonNegative; - private MaterialButton buttonPositive; - private MaterialButton buttonKeyboardSwitch; + private TextView textTitle; + private TextView textDescription; - private StaticPinProvider staticPinProvider; - - private ByteSecret resetNewPinSecret; - private ByteSecret resetPukSecret; - private ByteSecret setupPinSecret; + private MaterialButton buttonLeft; + private MaterialButton buttonRight; + private MaterialButton buttonPinInputSwitch; private SecurityKeyFormFactor securityKeyFormFactor; private SmartcardFormFactor smartcardFormFactor; private KeypadPinInput keypadPinInput; private KeyboardPinInput keyboardPinInput; - private PinInput currentPinInput; private Guideline guidelineForceHeight; @@ -135,28 +126,11 @@ public abstract class SecurityKeyDialogFragment extends B private TextView textPuk; private CheckBox checkboxPuk; - private enum State { - NORMAL_ENTER_PIN, - NORMAL_SECURITY_KEY, - NORMAL_SECURITY_KEY_HOLD, - NORMAL_ERROR, - RESET_PIN_ENTER_PUK, - RESET_PIN_ENTER_NEW_PIN, - RESET_PIN_SECURITY_KEY, - RESET_PIN_SUCCESS, - RESET_PIN_ERROR, - SETUP_CHOOSE_PIN, - SETUP_SHOW_PUK, - SETUP_CONFIRM_WIPE, - } - - private State currentState; + private NfcFullscreenView nfcFullscreenView; abstract public void initSecurityKeyConnectionMode(Bundle arguments); - abstract public void updateSecurityKeyPinUsingPuk(SecurityKey securityKey, ByteSecret puk, ByteSecret newPin) throws IOException; - - abstract public boolean isSecurityKeyEmpty(SecurityKey securityKey) throws IOException; + abstract public SecurityKeyDialogPresenter initPresenter(SecurityKeyDialogPresenter.View view, Context context, SecurityKeyDialogOptions options); public void setSecurityKeyDialogCallback(SecurityKeyDialogInterface.SecurityKeyDialogCallback callback) { this.callback = callback; @@ -225,7 +199,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { throw new IllegalStateException("Activity must implement SecurityKeyDialogInterface.SecurityKeyDialogCallback!"); } - keyboardPreference = new KeyboardPreference(context); + presenter = initPresenter(this, getActivity(), options); } @Nullable @@ -255,27 +229,26 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat super.onViewCreated(view, savedInstanceState); if (options.getPreventScreenshots()) { - // prevent screenshots Window window = Objects.requireNonNull(getDialog().getWindow()); window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); } innerBottomSheet = view.findViewById(R.id.hwSecurityDialogBottomSheet); - buttonNegative = view.findViewById(R.id.buttonNegative); - buttonPositive = view.findViewById(R.id.buttonPositive); - buttonKeyboardSwitch = view.findViewById(R.id.buttonKeyboardSwitch); + buttonLeft = view.findViewById(R.id.buttonLeft); + buttonRight = view.findViewById(R.id.buttonRight); + buttonPinInputSwitch = view.findViewById(R.id.buttonKeyboardSwitch); - textViewTitle = view.findViewById(R.id.textTitle); - textViewDescription = view.findViewById(R.id.textDescription); + textTitle = view.findViewById(R.id.textTitle); + textDescription = view.findViewById(R.id.textDescription); guidelineForceHeight = view.findViewById(R.id.guidelineForceHeight); includeShowPuk = view.findViewById(R.id.includeShowPuk); textPuk = view.findViewById(R.id.textPuk); checkboxPuk = view.findViewById(R.id.checkBoxPuk); - checkboxPuk.setOnCheckedChangeListener((buttonView, isChecked) -> gotoState(State.NORMAL_SECURITY_KEY)); + checkboxPuk.setOnCheckedChangeListener((buttonView, isChecked) -> presenter.finishedWithPuk()); - buttonNegative.setOnClickListener(v -> cancel()); - buttonPositive.setOnClickListener(v -> gotoState(State.RESET_PIN_ENTER_PUK)); - buttonKeyboardSwitch.setOnClickListener(v -> showHidePinInput(!keyboardPreference.isKeyboardPreferred())); + buttonLeft.setOnClickListener(v -> presenter.cancel()); + buttonRight.setOnClickListener(v -> presenter.resetPinScreen()); + buttonPinInputSwitch.setOnClickListener(v -> presenter.switchBetweenPinInputs()); wipeConfirmView = new WipeConfirmView(view.findViewById(R.id.includeConfirmWipe)); @@ -289,49 +262,31 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat keyboardPinInput = new KeyboardPinInput(view.findViewById(R.id.includeKeyboardInput)); keyboardPinInput.setPinInputCallback(this); - smartcardFormFactor = new SmartcardFormFactor(view.findViewById(R.id.includeSmartcardFormFactor)); - securityKeyFormFactor = new SecurityKeyFormFactor(view.findViewById(R.id.includeSecurityKeyFormFactor), innerBottomSheet); + smartcardFormFactor = new SmartcardFormFactor(view.findViewById(R.id.includeSmartcardFormFactor), this); + securityKeyFormFactor = new SecurityKeyFormFactor(view.findViewById(R.id.includeSecurityKeyFormFactor), this, this, innerBottomSheet, options.getShowSdkLogo()); - switch (options.getPinMode()) { - case PIN_INPUT: { - gotoState(State.NORMAL_ENTER_PIN); - break; - } - case NO_PIN_INPUT: { - gotoState(State.NORMAL_SECURITY_KEY); - break; - } - case RESET_PIN: { - gotoState(State.RESET_PIN_ENTER_PUK); - break; - } - case SETUP: { - gotoState(State.SETUP_CHOOSE_PIN); - break; - } - default: { - throw new IllegalArgumentException("unknown PinMode!"); - } - } + nfcFullscreenView = new NfcFullscreenView(view.findViewById(R.id.includeNfcFullscreen), innerBottomSheet); + + presenter.gotoScreenOnCreate(); } - private void showHidePinInput(boolean showKeyboard) { - if (showKeyboard) { - currentPinInput = keyboardPinInput; + @Override + public void showKeypadPinInput() { + currentPinInput = keypadPinInput; - keyboardPinInput.setVisibility(View.VISIBLE); - keypadPinInput.setVisibility(View.GONE); - buttonKeyboardSwitch.setIcon(getResources().getDrawable(R.drawable.hwsecurity_ic_keyboard_numeric)); - keyboardPinInput.openKeyboard(); - keyboardPreference.setIsKeyboardPreferred(true); - } else { - currentPinInput = keypadPinInput; + keyboardPinInput.setVisibility(View.GONE); + keypadPinInput.setVisibility(View.VISIBLE); + buttonPinInputSwitch.setIcon(getResources().getDrawable(R.drawable.hwsecurity_ic_keyboard_alphabetical)); + } - keyboardPinInput.setVisibility(View.GONE); - keypadPinInput.setVisibility(View.VISIBLE); - buttonKeyboardSwitch.setIcon(getResources().getDrawable(R.drawable.hwsecurity_ic_keyboard_alphabetical)); - keyboardPreference.setIsKeyboardPreferred(false); - } + @Override + public void showKeyboardPinInput() { + currentPinInput = keyboardPinInput; + + keyboardPinInput.setVisibility(View.VISIBLE); + keypadPinInput.setVisibility(View.GONE); + buttonPinInputSwitch.setIcon(getResources().getDrawable(R.drawable.hwsecurity_ic_keyboard_numeric)); + keyboardPinInput.openKeyboard(); } @Override @@ -357,284 +312,230 @@ public void onDismiss(DialogInterface dialog) { @Override public void onPinEntered(ByteSecret pinSecret) { - switch (currentState) { - case NORMAL_ENTER_PIN: { - staticPinProvider = StaticPinProvider.getInstance(pinSecret); - gotoState(State.NORMAL_SECURITY_KEY); - break; - } - case RESET_PIN_ENTER_PUK: { - resetPukSecret = pinSecret; - gotoState(State.RESET_PIN_ENTER_NEW_PIN); - break; - } - case RESET_PIN_ENTER_NEW_PIN: { - resetNewPinSecret = pinSecret; - gotoState(State.RESET_PIN_SECURITY_KEY); - break; - } - case SETUP_CHOOSE_PIN: { - setupPinSecret = pinSecret; - gotoState(State.SETUP_SHOW_PUK); - break; - } - default: { - // do nothing - break; - } - } + presenter.onPinEntered(pinSecret); } - private void gotoState(State newState) { - gotoState(newState, true); + public void updateTitle(String title, int descriptionResId) { + updateTitle(title, getString(descriptionResId)); } - private String getTitle() { - if (options.getTitle() != null) { - return options.getTitle(); - } - switch (options.getPinMode()) { - case PIN_INPUT: { - return getString(R.string.hwsecurity_ui_title_login); - } - case NO_PIN_INPUT: { - return getString(R.string.hwsecurity_ui_title_add); - } - case RESET_PIN: { - return getString(R.string.hwsecurity_ui_title_reset_pin); - } - case SETUP: { - return getString(R.string.hwsecurity_ui_title_setup); - } - default: { - throw new IllegalArgumentException("unknown PinMode!"); - } - } + public void updateTitle(int titleResId, String description) { + updateTitle(getString(titleResId), description); } - private void gotoState(State newState, boolean isTransportNfc) { - switch (newState) { - case NORMAL_ENTER_PIN: { - keypadPinInput.reset(options.getPinLength()); + public void updateTitle(int titleResId, int descriptionResId) { + updateTitle(getString(titleResId), getString(descriptionResId)); + } - textViewTitle.setText(getTitle()); - textViewDescription.setText(R.string.hwsecurity_ui_description_enter_pin); + public void updateTitle(String title, String description) { + textTitle.setText(title); + textDescription.setText(description); + } - showHidePinInput(keyboardPreference.isKeyboardPreferred()); - buttonKeyboardSwitch.setVisibility(options.getAllowKeyboard() ? View.VISIBLE : View.GONE); - buttonPositive.setText(R.string.hwsecurity_ui_button_reset); - buttonPositive.setVisibility(options.getShowReset() ? View.VISIBLE : View.GONE); - smartcardFormFactor.setVisibility(View.GONE); + @Override + public void screenEnterPin(Integer pinLength, boolean showReset, boolean useKeyboard) { + keypadPinInput.reset(pinLength); + + presenter.showPinInput(); + buttonPinInputSwitch.setVisibility(useKeyboard ? View.VISIBLE : View.GONE); + buttonRight.setText(R.string.hwsecurity_ui_button_reset); + buttonRight.setVisibility(showReset ? View.VISIBLE : View.GONE); + smartcardFormFactor.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.GONE); + includeShowPuk.setVisibility(View.GONE); + wipeConfirmView.setVisibility(View.GONE); + progressView.setVisibility(View.GONE); + errorView.setVisibility(View.GONE); + } + + @Override + public void screenSecurityKey(Integer pinLength, SecurityKeyDialogOptions.FormFactor formFactor) { + SecurityKeyManager.getInstance().rediscoverConnectedSecurityKeys(); + keypadPinInput.reset(pinLength); + + TransitionManager.beginDelayedTransition(innerBottomSheet); + keypadPinInput.setVisibility(View.GONE); + keyboardPinInput.setVisibility(View.GONE); + buttonPinInputSwitch.setVisibility(View.GONE); + buttonRight.setVisibility(View.INVISIBLE); + switch (formFactor) { + case SMART_CARD: { + smartcardFormFactor.setVisibility(View.VISIBLE); securityKeyFormFactor.setVisibility(View.GONE); - includeShowPuk.setVisibility(View.GONE); - wipeConfirmView.setVisibility(View.GONE); - progressView.setVisibility(View.GONE); - errorView.setVisibility(View.GONE); - break; - } - case NORMAL_SECURITY_KEY: { - SecurityKeyManager.getInstance().rediscoverConnectedSecurityKeys(); - keypadPinInput.reset(options.getPinLength()); - - textViewTitle.setText(getTitle()); - textViewDescription.setText(R.string.hwsecurity_ui_description_start); - - TransitionManager.beginDelayedTransition(innerBottomSheet); - keypadPinInput.setVisibility(View.GONE); - keyboardPinInput.setVisibility(View.GONE); - buttonKeyboardSwitch.setVisibility(View.GONE); - buttonPositive.setVisibility(View.GONE); - boolean isSmartcardFormFactor = options.getFormFactor() == SecurityKeyDialogOptions.FormFactor.SMART_CARD; - boolean isSecurityKeyFormFactor = options.getFormFactor() == SecurityKeyDialogOptions.FormFactor.SECURITY_KEY; - smartcardFormFactor.setVisibility(isSmartcardFormFactor ? View.VISIBLE : View.GONE); - securityKeyFormFactor.setVisibility(isSecurityKeyFormFactor ? View.VISIBLE : View.GONE); - includeShowPuk.setVisibility(View.GONE); - wipeConfirmView.setVisibility(View.GONE); - progressView.setVisibility(View.GONE); - errorView.setVisibility(View.GONE); break; } - case NORMAL_SECURITY_KEY_HOLD: { - textViewTitle.setText(getTitle()); - textViewDescription.setText(isTransportNfc ? R.string.hwsecurity_ui_description_hold_nfc : R.string.hwsecurity_ui_description_hold_usb); - - // no animation for speed! - keypadPinInput.setVisibility(View.GONE); - keyboardPinInput.setVisibility(View.GONE); - buttonKeyboardSwitch.setVisibility(View.GONE); - buttonPositive.setVisibility(View.GONE); + case SECURITY_KEY: { smartcardFormFactor.setVisibility(View.GONE); - securityKeyFormFactor.setVisibility(View.GONE); - includeShowPuk.setVisibility(View.GONE); - wipeConfirmView.setVisibility(View.GONE); - progressView.setVisibility(View.VISIBLE); - errorView.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.VISIBLE); break; } - case RESET_PIN_ENTER_PUK: { - keypadPinInput.reset(options.getPukLength()); - - textViewTitle.setText(R.string.hwsecurity_ui_title_reset_pin); - textViewDescription.setText(R.string.hwsecurity_ui_description_enter_puk); + } + includeShowPuk.setVisibility(View.GONE); + wipeConfirmView.setVisibility(View.GONE); + progressView.setVisibility(View.GONE); + errorView.setVisibility(View.GONE); + } - TransitionManager.beginDelayedTransition(innerBottomSheet); - keypadPinInput.setVisibility(View.VISIBLE); - keyboardPinInput.setVisibility(View.GONE); - buttonKeyboardSwitch.setVisibility(View.GONE); - buttonPositive.setVisibility(View.GONE); - smartcardFormFactor.setVisibility(View.GONE); - securityKeyFormFactor.setVisibility(View.GONE); - includeShowPuk.setVisibility(View.GONE); - wipeConfirmView.setVisibility(View.GONE); - progressView.setVisibility(View.GONE); - errorView.setVisibility(View.GONE); - break; - } - case RESET_PIN_ENTER_NEW_PIN: { - keypadPinInput.reset(options.getPinLength()); + @Override + public void screenHoldSecurityKey() { + // no animation for speed! + keypadPinInput.setVisibility(View.GONE); + keyboardPinInput.setVisibility(View.GONE); + buttonPinInputSwitch.setVisibility(View.GONE); + buttonRight.setVisibility(View.INVISIBLE); + smartcardFormFactor.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.GONE); + includeShowPuk.setVisibility(View.GONE); + wipeConfirmView.setVisibility(View.GONE); + progressView.setVisibility(View.VISIBLE); + errorView.setVisibility(View.GONE); + } - textViewTitle.setText(R.string.hwsecurity_ui_title_reset_pin); - textViewDescription.setText(R.string.hwsecurity_ui_description_enter_new_pin); + @Override + public void screenResetPinEnterPinOrPuk(Integer pinOrPukLength) { + keypadPinInput.reset(pinOrPukLength); + + TransitionManager.beginDelayedTransition(innerBottomSheet); + keypadPinInput.setVisibility(View.VISIBLE); + keyboardPinInput.setVisibility(View.GONE); + buttonPinInputSwitch.setVisibility(View.GONE); + buttonRight.setVisibility(View.INVISIBLE); + smartcardFormFactor.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.GONE); + includeShowPuk.setVisibility(View.GONE); + wipeConfirmView.setVisibility(View.GONE); + progressView.setVisibility(View.GONE); + errorView.setVisibility(View.GONE); + } - TransitionManager.beginDelayedTransition(innerBottomSheet); - keypadPinInput.setVisibility(View.VISIBLE); - keyboardPinInput.setVisibility(View.GONE); - buttonKeyboardSwitch.setVisibility(View.GONE); - buttonPositive.setVisibility(View.GONE); - smartcardFormFactor.setVisibility(View.GONE); - securityKeyFormFactor.setVisibility(View.GONE); - includeShowPuk.setVisibility(View.GONE); - wipeConfirmView.setVisibility(View.GONE); - progressView.setVisibility(View.GONE); - errorView.setVisibility(View.GONE); - break; - } - case RESET_PIN_SECURITY_KEY: { - SecurityKeyManager.getInstance().rediscoverConnectedSecurityKeys(); - - textViewTitle.setText(R.string.hwsecurity_ui_title_reset_pin); - textViewDescription.setText(R.string.hwsecurity_ui_description_hold_nfc); - - TransitionManager.beginDelayedTransition(innerBottomSheet); - keypadPinInput.setVisibility(View.GONE); - keyboardPinInput.setVisibility(View.GONE); - buttonKeyboardSwitch.setVisibility(View.GONE); - buttonPositive.setVisibility(View.GONE); - boolean isSmartcardFormFactor = options.getFormFactor() == SecurityKeyDialogOptions.FormFactor.SMART_CARD; - boolean isSecurityKeyFormFactor = options.getFormFactor() == SecurityKeyDialogOptions.FormFactor.SECURITY_KEY; - smartcardFormFactor.setVisibility(isSmartcardFormFactor ? View.VISIBLE : View.GONE); - securityKeyFormFactor.setVisibility(isSecurityKeyFormFactor ? View.VISIBLE : View.GONE); - includeShowPuk.setVisibility(View.GONE); - wipeConfirmView.setVisibility(View.GONE); - progressView.setVisibility(View.GONE); - errorView.setVisibility(View.GONE); - break; - } - case RESET_PIN_SUCCESS: { - textViewTitle.setText(R.string.hwsecurity_ui_title_reset_pin); - textViewDescription.setText(""); - - TransitionManager.beginDelayedTransition(innerBottomSheet); - keypadPinInput.setVisibility(View.GONE); - keyboardPinInput.setVisibility(View.GONE); - buttonKeyboardSwitch.setVisibility(View.GONE); - buttonPositive.setVisibility(View.GONE); - smartcardFormFactor.setVisibility(View.GONE); - securityKeyFormFactor.setVisibility(View.GONE); - includeShowPuk.setVisibility(View.GONE); - wipeConfirmView.setVisibility(View.GONE); - progressView.setVisibility(View.GONE); - errorView.setVisibility(View.GONE); - break; - } - case NORMAL_ERROR: { - TransitionManager.beginDelayedTransition(innerBottomSheet); - keypadPinInput.setVisibility(View.GONE); - keyboardPinInput.setVisibility(View.GONE); - buttonKeyboardSwitch.setVisibility(View.GONE); - buttonPositive.setVisibility(View.GONE); - smartcardFormFactor.setVisibility(View.GONE); + @Override + public void screenResetPinSecurityKey(SecurityKeyDialogOptions.FormFactor formFactor) { + SecurityKeyManager.getInstance().rediscoverConnectedSecurityKeys(); + + TransitionManager.beginDelayedTransition(innerBottomSheet); + keypadPinInput.setVisibility(View.GONE); + keyboardPinInput.setVisibility(View.GONE); + buttonPinInputSwitch.setVisibility(View.GONE); + buttonRight.setVisibility(View.INVISIBLE); + switch (formFactor) { + case SMART_CARD: { + smartcardFormFactor.setVisibility(View.VISIBLE); securityKeyFormFactor.setVisibility(View.GONE); - includeShowPuk.setVisibility(View.GONE); - wipeConfirmView.setVisibility(View.GONE); - progressView.setVisibility(View.GONE); - errorView.setVisibility(View.VISIBLE); break; } - case RESET_PIN_ERROR: { - TransitionManager.beginDelayedTransition(innerBottomSheet); - keypadPinInput.setVisibility(View.GONE); - keyboardPinInput.setVisibility(View.GONE); - buttonKeyboardSwitch.setVisibility(View.GONE); - buttonPositive.setVisibility(View.GONE); + case SECURITY_KEY: { smartcardFormFactor.setVisibility(View.GONE); - securityKeyFormFactor.setVisibility(View.GONE); - includeShowPuk.setVisibility(View.GONE); - wipeConfirmView.setVisibility(View.GONE); - progressView.setVisibility(View.GONE); - errorView.setVisibility(View.VISIBLE); + securityKeyFormFactor.setVisibility(View.VISIBLE); break; } - case SETUP_CHOOSE_PIN: { - int pinLength = options.getPinLength() == null ? SETUP_DEFAULT_PIN_LENGTH : options.getPinLength(); - keypadPinInput.reset(pinLength); + } + includeShowPuk.setVisibility(View.GONE); + wipeConfirmView.setVisibility(View.GONE); + progressView.setVisibility(View.GONE); + errorView.setVisibility(View.GONE); + } - textViewTitle.setText(getTitle()); - textViewDescription.setText(R.string.hwsecurity_ui_description_choose_pin); + @Override + public void screenResetPinSuccess() { + TransitionManager.beginDelayedTransition(innerBottomSheet); + keypadPinInput.setVisibility(View.GONE); + keyboardPinInput.setVisibility(View.GONE); + buttonPinInputSwitch.setVisibility(View.GONE); + buttonRight.setVisibility(View.INVISIBLE); + smartcardFormFactor.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.GONE); + includeShowPuk.setVisibility(View.GONE); + wipeConfirmView.setVisibility(View.GONE); + progressView.setVisibility(View.GONE); + errorView.setVisibility(View.GONE); + } + + @Override + public void screenError() { + TransitionManager.beginDelayedTransition(innerBottomSheet); + keypadPinInput.setVisibility(View.GONE); + keyboardPinInput.setVisibility(View.GONE); + buttonPinInputSwitch.setVisibility(View.GONE); + buttonRight.setVisibility(View.INVISIBLE); + smartcardFormFactor.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.GONE); + includeShowPuk.setVisibility(View.GONE); + wipeConfirmView.setVisibility(View.GONE); + progressView.setVisibility(View.GONE); + errorView.setVisibility(View.VISIBLE); + } + + @Override + public void screenSetupChoosePin(int pinLength) { + keypadPinInput.reset(pinLength); + + keyboardPinInput.setVisibility(View.GONE); + keypadPinInput.setVisibility(View.VISIBLE); + buttonPinInputSwitch.setVisibility(View.GONE); + buttonRight.setVisibility(View.INVISIBLE); + smartcardFormFactor.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.GONE); + includeShowPuk.setVisibility(View.GONE); + wipeConfirmView.setVisibility(View.GONE); + progressView.setVisibility(View.GONE); + errorView.setVisibility(View.GONE); + } + + @Override + public void screenSetupChoosePuk(int pukLength, ByteSecret setupPuk) { + setupPuk.displayOnTextView(textPuk); + + TransitionManager.beginDelayedTransition(innerBottomSheet); + keyboardPinInput.setVisibility(View.GONE); + keypadPinInput.setVisibility(View.GONE); + buttonPinInputSwitch.setVisibility(View.GONE); + buttonRight.setVisibility(View.INVISIBLE); + smartcardFormFactor.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.GONE); + includeShowPuk.setVisibility(View.VISIBLE); + wipeConfirmView.setVisibility(View.GONE); + progressView.setVisibility(View.GONE); + errorView.setVisibility(View.GONE); + } + + @Override + public void screenConfirmWipe() { + TransitionManager.beginDelayedTransition(innerBottomSheet); + keyboardPinInput.setVisibility(View.GONE); + keypadPinInput.setVisibility(View.GONE); + buttonPinInputSwitch.setVisibility(View.GONE); + buttonRight.setVisibility(View.INVISIBLE); + smartcardFormFactor.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.GONE); + includeShowPuk.setVisibility(View.GONE); + wipeConfirmView.setVisibility(View.VISIBLE); + progressView.setVisibility(View.GONE); + errorView.setVisibility(View.GONE); + } + + @Override + public void screeFullscreenNfc() { + keyboardPinInput.setVisibility(View.GONE); + keypadPinInput.setVisibility(View.GONE); + buttonPinInputSwitch.setVisibility(View.GONE); + buttonRight.setVisibility(View.INVISIBLE); + smartcardFormFactor.setVisibility(View.GONE); + securityKeyFormFactor.setVisibility(View.GONE); + includeShowPuk.setVisibility(View.GONE); + wipeConfirmView.setVisibility(View.GONE); + progressView.setVisibility(View.GONE); + errorView.setVisibility(View.GONE); + + textTitle.setVisibility(View.GONE); + textDescription.setVisibility(View.GONE); + + nfcFullscreenView.setVisibility(View.VISIBLE); + nfcFullscreenView.animateNfcFullscreen(getDialog()); + } + + @Override + public void onSecurityKeyFormFactorClickUsb() { - keyboardPinInput.setVisibility(View.GONE); - keypadPinInput.setVisibility(View.VISIBLE); - buttonKeyboardSwitch.setVisibility(View.GONE); - buttonPositive.setVisibility(View.GONE); - smartcardFormFactor.setVisibility(View.GONE); - securityKeyFormFactor.setVisibility(View.GONE); - includeShowPuk.setVisibility(View.GONE); - wipeConfirmView.setVisibility(View.GONE); - progressView.setVisibility(View.GONE); - errorView.setVisibility(View.GONE); - break; - } - case SETUP_SHOW_PUK: { - int pukLength = options.getPukLength() == null ? SETUP_DEFAULT_PUK_LENGTH : options.getPukLength(); - ByteSecret setupPuk = ByteSecretGenerator.getInstance().createRandomNumeric(pukLength); - staticPinProvider = StaticPinProvider.getInstance(setupPinSecret, setupPuk); - - setupPuk.displayOnTextView(textPuk); - - TransitionManager.beginDelayedTransition(innerBottomSheet); - textViewTitle.setText(getTitle()); - textViewDescription.setText(R.string.hwsecurity_ui_description_puk); - keyboardPinInput.setVisibility(View.GONE); - keypadPinInput.setVisibility(View.GONE); - buttonKeyboardSwitch.setVisibility(View.GONE); - buttonPositive.setVisibility(View.GONE); - smartcardFormFactor.setVisibility(View.GONE); - securityKeyFormFactor.setVisibility(View.GONE); - includeShowPuk.setVisibility(View.VISIBLE); - wipeConfirmView.setVisibility(View.GONE); - progressView.setVisibility(View.GONE); - errorView.setVisibility(View.GONE); - break; - } - case SETUP_CONFIRM_WIPE: { - - TransitionManager.beginDelayedTransition(innerBottomSheet); - textViewTitle.setText(getTitle()); - textViewDescription.setText(""); - keyboardPinInput.setVisibility(View.GONE); - keypadPinInput.setVisibility(View.GONE); - buttonKeyboardSwitch.setVisibility(View.GONE); - buttonPositive.setVisibility(View.GONE); - smartcardFormFactor.setVisibility(View.GONE); - securityKeyFormFactor.setVisibility(View.GONE); - includeShowPuk.setVisibility(View.GONE); - wipeConfirmView.setVisibility(View.VISIBLE); - progressView.setVisibility(View.GONE); - errorView.setVisibility(View.GONE); - break; - } - } - currentState = newState; } @UiThread @@ -642,76 +543,45 @@ private void gotoState(State newState, boolean isTransportNfc) { public void onSecurityKeyDiscovered(@NonNull SecurityKey securityKey) { HwTimber.d("SecurityKeyDialogFragment -> onSecurityKeyDiscovered"); - switch (currentState) { - case NORMAL_ENTER_PIN: { - // automatically proceed with PIN on NFC connection - if (!securityKey.isTransportNfc()) { - break; - } - staticPinProvider = StaticPinProvider.getInstance(currentPinInput.getPin()); - // fall-through - } - case SETUP_CONFIRM_WIPE: - // fall-through - case NORMAL_SECURITY_KEY: - // fall-through - case NORMAL_SECURITY_KEY_HOLD: { - gotoState(State.NORMAL_SECURITY_KEY_HOLD, securityKey.isTransportNfc()); - - boolean isSetupMode = options.getPinMode() == SecurityKeyDialogOptions.PinMode.SETUP; - boolean isWipedConfirmed = wipeConfirmView.isWipeConfirmed(); - if (isSetupMode && !isWipedConfirmed) { - try { - if (!isSecurityKeyEmpty(securityKey)) { - gotoState(State.SETUP_CONFIRM_WIPE); - return; - } - } catch (IOException e) { - handleError(e); - return; - } - } - - try { - callback.onSecurityKeyDialogDiscovered(this, securityKey, staticPinProvider); - } catch (IOException e) { - handleError(e); - return; - } - break; - } - case RESET_PIN_SECURITY_KEY: { - new Thread(() -> { - try { - updateSecurityKeyPinUsingPuk(securityKey, resetPukSecret, resetNewPinSecret); - - innerBottomSheet.post(() -> Toast.makeText(getContext(), R.string.hwsecurity_ui_changed_pin, Toast.LENGTH_LONG).show()); - innerBottomSheet.post(() -> gotoState(State.RESET_PIN_SUCCESS)); - innerBottomSheet.postDelayed(() -> { - if (!isAdded()) { - return; - } - if (options.getPinMode() == SecurityKeyDialogOptions.PinMode.RESET_PIN) { - dismiss(); - } else { - gotoState(State.NORMAL_ENTER_PIN); - } - }, TIME_DELAYED_STATE_CHANGE); - } catch (IOException e) { - innerBottomSheet.post(() -> handleError(e)); - } - }).start(); - break; + boolean isWipeConfirmed = wipeConfirmView.isWipeConfirmed(); + + presenter.onSecurityKeyDiscovered(securityKey, isWipeConfirmed); + } + + @Override + public void onSecurityKeyDialogDiscoveredCallback(@NonNull SecurityKey securityKey, @Nullable PinProvider pinProvider) throws IOException { + callback.onSecurityKeyDialogDiscovered(this, securityKey, pinProvider); + } + + @Override + public void updateErrorViewText(String text) { + errorView.setText(text); + } + + @Override + public void updateErrorViewText(int text) { + errorView.setText(text); + } + + @Override + public boolean postDelayedRunnable(Runnable action, long delayMillis) { + return bottomSheet.postDelayed(() -> { + if (!isAdded()) { + return; } - default: - // do nothing - } + action.run(); + }, delayMillis); + } + + @Override + public boolean postRunnable(Runnable action) { + return bottomSheet.post(action); } @UiThread @Override public void onSecurityKeyDiscoveryFailed(@NonNull IOException exception) { - handleError(exception); + presenter.handleError(exception); } @Override @@ -722,84 +592,17 @@ public void onSecurityKeyDisconnected(@NonNull SecurityKey securityKey) { @AnyThread @Override public void postProgressMessage(String message) { - bottomSheet.post(() -> handleProgressMessage(message)); + bottomSheet.post(() -> progressView.setText(message)); } @AnyThread @Override public void postError(IOException exception) { - bottomSheet.post(() -> handleError(exception)); - } - - private void handleProgressMessage(String message) { - progressView.setText(message); + bottomSheet.post(() -> presenter.handleError(exception)); } - @UiThread @Override - public void handleError(IOException exception) { - HwTimber.d(exception); - - switch (currentState) { - case NORMAL_SECURITY_KEY: - case NORMAL_SECURITY_KEY_HOLD: { - try { - throw exception; - } catch (OpenPgpLockedException e) { - errorView.setText(R.string.hwsecurity_ui_error_no_pin_tries); - gotoState(State.NORMAL_ERROR); - } catch (OpenPgpWrongPinException e) { - gotoErrorStateAndDelayedState(getString(R.string.hwsecurity_ui_error_wrong_pin, e.getPinRetriesLeft()), State.NORMAL_ERROR, State.NORMAL_ENTER_PIN); - } catch (PivWrongPinException e) { - gotoErrorStateAndDelayedState(getString(R.string.hwsecurity_ui_error_wrong_pin, e.getRetriesLeft()), State.NORMAL_ERROR, State.NORMAL_ENTER_PIN); - } catch (OpenPgpPinTooShortException e) { - gotoErrorStateAndDelayedState(getString(R.string.hwsecurity_ui_error_too_short_pin), State.NORMAL_ERROR, State.NORMAL_ENTER_PIN); - } catch (OpenPgpPublicKeyUnavailableException e) { - gotoErrorStateAndDelayedState(getString(R.string.hwsecurity_ui_error_no_pubkey), State.NORMAL_ERROR, State.NORMAL_SECURITY_KEY); - } catch (SecurityKeyException e) { - gotoErrorStateAndDelayedState(exception.getMessage(), State.NORMAL_ERROR, State.NORMAL_SECURITY_KEY); - } catch (SecurityKeyLostException e) { - gotoState(State.NORMAL_SECURITY_KEY); - } catch (IOException e) { - gotoErrorStateAndDelayedState(exception.getMessage(), State.NORMAL_ERROR, State.NORMAL_SECURITY_KEY); - } - break; - } - case RESET_PIN_SECURITY_KEY: { - try { - throw exception; - } catch (OpenPgpLockedException e) { - errorView.setText(R.string.hwsecurity_ui_error_no_puk_tries); - gotoState(State.RESET_PIN_ERROR); - } catch (OpenPgpWrongPinException e) { - gotoErrorStateAndDelayedState(getString(R.string.hwsecurity_ui_error_wrong_puk, e.getPukRetriesLeft()), State.RESET_PIN_ERROR, State.RESET_PIN_ENTER_PUK); - } catch (PivWrongPinException e) { - gotoErrorStateAndDelayedState(getString(R.string.hwsecurity_ui_error_wrong_puk, e.getRetriesLeft()), State.RESET_PIN_ERROR, State.RESET_PIN_ENTER_PUK); - } catch (OpenPgpPinTooShortException e) { - gotoErrorStateAndDelayedState(getString(R.string.hwsecurity_ui_error_too_short_puk), State.RESET_PIN_ERROR, State.RESET_PIN_ENTER_PUK); - } catch (SecurityKeyException e) { - gotoErrorStateAndDelayedState(exception.getMessage(), State.RESET_PIN_ERROR, State.RESET_PIN_SECURITY_KEY); - } catch (SecurityKeyLostException e) { - // TODO - } catch (IOException e) { - gotoErrorStateAndDelayedState(exception.getMessage(), State.RESET_PIN_ERROR, State.RESET_PIN_SECURITY_KEY); - } - break; - } - default: - HwTimber.d("handleError called in State other than NORMAL_SECURITY_KEY, NORMAL_SECURITY_KEY_HOLD or RESET_PIN_SECURITY_KEY."); - } + public void confirmPinInput() { + currentPinInput.confirmPin(); } - - private void gotoErrorStateAndDelayedState(String text, State errorState, State delayedState) { - errorView.setText(text); - gotoState(errorState); - bottomSheet.postDelayed(() -> { - if (!isAdded()) { - return; - } - gotoState(delayedState); - }, TIME_DELAYED_STATE_CHANGE); - } - } diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogInterface.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogInterface.java index 6635b4e..e3ac920 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogInterface.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogInterface.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -38,9 +38,6 @@ public interface SecurityKeyDialogInterface { void dismiss(); - @UiThread - void handleError(IOException exception); - @AnyThread void postError(IOException exception); diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogOptions.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogOptions.java index dc9277e..babedcd 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogOptions.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/SecurityKeyDialogOptions.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -70,11 +70,14 @@ public enum FormFactor { @StyleRes public abstract int getTheme(); + public abstract boolean getShowSdkLogo(); + public static Builder builder() { return new AutoValue_SecurityKeyDialogOptions.Builder() .setPreventScreenshots(true) .setShowReset(false) .setAllowKeyboard(false) + .setShowSdkLogo(false) .setPinMode(PinMode.PIN_INPUT) .setFormFactor(FormFactor.SECURITY_KEY) .setTheme(R.style.HwSecurity_Dialog); @@ -155,6 +158,13 @@ public abstract static class Builder { */ public abstract Builder setTheme(@StyleRes int theme); + /** + * Shows the Hardware Security SDK Logo with a clickable link + *

+ * Default: false + */ + public abstract Builder setShowSdkLogo(boolean showLogo); + abstract SecurityKeyDialogOptions autoBuild(); public SecurityKeyDialogOptions build() { diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/AnimatedVectorDrawableHelper.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/AnimatedVectorDrawableHelper.java index 7732050..7ceb8fb 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/AnimatedVectorDrawableHelper.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/AnimatedVectorDrawableHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/ErrorView.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/ErrorView.java index 2832362..367c06a 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/ErrorView.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/ErrorView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeyboardPinInput.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeyboardPinInput.java index 2eaec3e..9eb9d6d 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeyboardPinInput.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeyboardPinInput.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -106,14 +106,18 @@ private void closeKeyboard() { keyboardInput.clearFocus(); } - private void confirmPin() { - ByteSecret pinSecret = getPin(); - callback.onPinEntered(pinSecret); + private ByteSecret getPinAndClear() { + closeKeyboard(); + if (keyboardInput.length() == 0) { + return null; + } else { + return ByteSecret.fromEditableAsUtf8AndClear(keyboardInput.getText()); + } } @Override - public ByteSecret getPin() { - closeKeyboard(); - return ByteSecret.fromEditableAsUtf8AndClear(keyboardInput.getText()); + public void confirmPin() { + ByteSecret pinSecret = getPinAndClear(); + callback.onPinEntered(pinSecret); } } diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeyboardPreference.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeyboardPreferenceRepository.java similarity index 92% rename from hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeyboardPreference.java rename to hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeyboardPreferenceRepository.java index 4e3a0db..2102d2d 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeyboardPreference.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeyboardPreferenceRepository.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -29,11 +29,11 @@ import androidx.annotation.RestrictTo; @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class KeyboardPreference { +public class KeyboardPreferenceRepository { private Context context; - public KeyboardPreference(Context context) { + public KeyboardPreferenceRepository(Context context) { this.context = context; } diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeypadPinInput.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeypadPinInput.java index 139594d..e06772b 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeypadPinInput.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/KeypadPinInput.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -209,7 +209,7 @@ private void addPinNumber(byte number) { } if (pinPosition == getPinMaxLength()) { if (fixedLength) { - callback.onPinEntered(getPin()); + callback.onPinEntered(getPinAndClear()); } else { setNumberButtonsEnabled(false); } @@ -231,10 +231,6 @@ private void deleteAllPinNumbers() { } } - private void confirmPin() { - callback.onPinEntered(getPin()); - } - private void addPinCircle() { if (fixedLength) { pinCircles.get(pinPosition).setImageResource(R.drawable.hwsecurity_pin_circle_filled); @@ -277,8 +273,16 @@ private int convertDpToPx(@SuppressWarnings("SameParameterValue") int dp) { return Math.round(pixels); } + private ByteSecret getPinAndClear() { + if (pinPosition == 0) { + return null; + } else { + return ByteSecret.fromByteArrayAndClear(pin, pinPosition); + } + } + @Override - public ByteSecret getPin() { - return ByteSecret.fromByteArrayAndClear(pin, pinPosition); + public void confirmPin() { + callback.onPinEntered(getPinAndClear()); } } diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/NfcFullscreenView.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/NfcFullscreenView.java new file mode 100644 index 0000000..427f284 --- /dev/null +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/NfcFullscreenView.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.ui.internal; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ArgbEvaluator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.app.Dialog; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.util.Pair; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.Guideline; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.ViewCompat; +import androidx.lifecycle.LifecycleObserver; +import androidx.transition.TransitionManager; +import androidx.vectordrawable.graphics.drawable.Animatable2Compat; + +import java.util.ArrayList; +import java.util.List; + +import de.cotech.hw.ui.R; +import de.cotech.hw.util.HwTimber; + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class NfcFullscreenView implements LifecycleObserver { + private Context context; + + private ViewGroup view; + private ConstraintLayout innerBottomSheet; + + private ImageView imageNfcSweetspot; + private TextView textNfcFullscreen; + private ImageView imageNfcFullscreen; + + private Guideline guidelineForceHeight; + + private NfcSweetspotData nfcSweetspotData; + + public NfcFullscreenView(@NonNull ViewGroup view, ConstraintLayout innerBottomSheet) { + this.context = view.getContext(); + + this.view = view; + + this.innerBottomSheet = innerBottomSheet; + + imageNfcSweetspot = view.findViewById(R.id.imageNfcSweetspot); + textNfcFullscreen = view.findViewById(R.id.textNfcFullscreen); + imageNfcFullscreen = view.findViewById(R.id.imageNfcFullscreen); + + guidelineForceHeight = innerBottomSheet.findViewById(R.id.guidelineForceHeight); + + nfcSweetspotData = NfcSweetspotData.getInstance(context); + } + + public void setVisibility(int visibility) { + view.setVisibility(visibility); + } + + public int getVisibility() { + return view.getVisibility(); + } + + public void animateNfcFullscreen(Dialog dialog) { + CoordinatorLayout coordinator = dialog.findViewById(com.google.android.material.R.id.coordinator); + FrameLayout bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet); + + ValueAnimator bottomSheetFullscreenAnimator = ValueAnimator + .ofInt(bottomSheet.getHeight(), coordinator.getHeight()) + .setDuration(250); + + bottomSheetFullscreenAnimator.addUpdateListener(animation -> { + bottomSheet.getLayoutParams().height = (int) animation.getAnimatedValue(); + bottomSheet.requestLayout(); + guidelineForceHeight.setGuidelineEnd((int) animation.getAnimatedValue() - 100); + guidelineForceHeight.requestLayout(); + }); + + int colorFrom = context.getResources().getColor(R.color.hwSecurityWhite); + int colorTo = resolveColorFromAttr(R.attr.hwSecuritySurfaceColor); + ValueAnimator colorChange = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); + colorChange.setDuration(100); + colorChange.addUpdateListener(animator -> { + innerBottomSheet.setBackgroundColor((int) animator.getAnimatedValue()); + }); + + ObjectAnimator fadeInImageNfcFullscreen = ObjectAnimator + .ofFloat(imageNfcFullscreen, View.ALPHA, 0, 1) + .setDuration(150); + fadeInImageNfcFullscreen.setStartDelay(50); + fadeInImageNfcFullscreen.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + imageNfcFullscreen.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animator animation) { + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + + bottomSheetFullscreenAnimator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + animateNfcFinal(dialog); + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + + List items = new ArrayList<>(); + items.add(bottomSheetFullscreenAnimator); + items.add(fadeInImageNfcFullscreen); + items.add(colorChange); + + AnimatorSet set = new AnimatorSet(); + set.playTogether(items); + set.setInterpolator(new AccelerateDecelerateInterpolator()); + set.start(); + } + + private void animateNfcFinal(Dialog dialog) { + textNfcFullscreen.setText(R.string.hwsecurity_ui_title_nfc_fullscreen); + textNfcFullscreen.setVisibility(View.VISIBLE); + + Animatable2Compat.AnimationCallback animationCallback = new Animatable2Compat.AnimationCallback() { + @Override + public void onAnimationEnd(Drawable drawable) { + if (!ViewCompat.isAttachedToWindow(imageNfcFullscreen)) { + return; + } + + fadeToNfcSweetSpot(dialog); + } + }; + + AnimatedVectorDrawableHelper.startAnimation(imageNfcFullscreen, R.drawable.hwsecurity_nfc_handling, animationCallback); + } + + private void fadeToNfcSweetSpot(Dialog dialog) { + Pair nfcPosition = nfcSweetspotData.getSweetspotForBuildModel(); + if (nfcPosition == null) { + HwTimber.d("No NFC sweetspot data available for this model."); + return; + } + + int colorFrom = resolveColorFromAttr(R.attr.hwSecuritySurfaceColor); + int colorTo = context.getResources().getColor(R.color.hwSecurityWhite); + ValueAnimator colorChange = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); + colorChange.setDuration(150); + colorChange.addUpdateListener(animator -> { + innerBottomSheet.setBackgroundColor((int) animator.getAnimatedValue()); + }); + + ObjectAnimator fadeOutImageNfcFullscreen = ObjectAnimator + .ofFloat(imageNfcFullscreen, "alpha", 1, 0) + .setDuration(150); + + fadeOutImageNfcFullscreen.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + showNfcSweetSpot(dialog); + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + + List items = new ArrayList<>(); + items.add(colorChange); + items.add(fadeOutImageNfcFullscreen); + + AnimatorSet set = new AnimatorSet(); + set.playTogether(items); + set.start(); + } + + private void showNfcSweetSpot(Dialog dialog) { + Pair nfcPosition = nfcSweetspotData.getSweetspotForBuildModel(); + + if (dialog == null) { + return; + } + + DisplayMetrics metrics = new DisplayMetrics(); + dialog.getWindow().getWindowManager().getDefaultDisplay().getMetrics(metrics); + + float statusBarHeight = getStatusbarHeight(dialog.getWindow()); + + final float translationX = (float) (metrics.widthPixels * nfcPosition.first); + final float translationY = (float) (metrics.heightPixels * nfcPosition.second) + statusBarHeight; + + imageNfcSweetspot.post(() -> { + imageNfcSweetspot.setTranslationX(translationX - imageNfcSweetspot.getWidth() / 2); + imageNfcSweetspot.setTranslationY(translationY - imageNfcSweetspot.getHeight() / 2); + + TransitionManager.beginDelayedTransition(innerBottomSheet); + imageNfcSweetspot.setVisibility(View.VISIBLE); + textNfcFullscreen.setVisibility(View.VISIBLE); + imageNfcFullscreen.setVisibility(View.GONE); + }); + + AnimatedVectorDrawableHelper.startAndLoopAnimation(imageNfcSweetspot, R.drawable.hwsecurity_nfc_sweet_spot_a); + } + + private static int getStatusbarHeight(Window window) { + Rect rectangle = new Rect(); + window.getDecorView().getWindowVisibleDisplayFrame(rectangle); + int statusBarHeight = rectangle.top; + int contentViewTop = window.findViewById(Window.ID_ANDROID_CONTENT).getTop(); + return contentViewTop - statusBarHeight; + } + + private int resolveColorFromAttr(@AttrRes int resId) { + TypedValue outValue = new TypedValue(); + // must be the themed context to work correctly on Android < 5 + innerBottomSheet.getContext().getTheme().resolveAttribute(resId, outValue, true); + return outValue.data; + } + +} diff --git a/hwsecurity/core/src/main/java/de/cotech/hw/internal/NfcSweetspotData.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/NfcSweetspotData.java similarity index 96% rename from hwsecurity/core/src/main/java/de/cotech/hw/internal/NfcSweetspotData.java rename to hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/NfcSweetspotData.java index 5bfebd8..207bd0e 100644 --- a/hwsecurity/core/src/main/java/de/cotech/hw/internal/NfcSweetspotData.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/NfcSweetspotData.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -22,7 +22,7 @@ * along with this program. If not, see . */ -package de.cotech.hw.internal; +package de.cotech.hw.ui.internal; import android.content.Context; import android.os.Build; diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/OpenPgpSecurityKeyDialogFragment.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/OpenPgpSecurityKeyDialogFragment.java deleted file mode 100644 index a6f04f8..0000000 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/OpenPgpSecurityKeyDialogFragment.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2018-2019 Confidential Technologies GmbH - * - * You can purchase a commercial license at https://hwsecurity.dev. - * Buying such a license is mandatory as soon as you develop commercial - * activities involving this program without disclosing the source code - * of your own applications. - * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package de.cotech.hw.ui.internal; - -import android.os.Bundle; - -import androidx.annotation.RestrictTo; - -import de.cotech.hw.SecurityKey; -import de.cotech.hw.SecurityKeyManager; -import de.cotech.hw.openpgp.OpenPgpSecurityKey; -import de.cotech.hw.openpgp.OpenPgpSecurityKeyConnectionMode; -import de.cotech.hw.openpgp.OpenPgpSecurityKeyConnectionModeConfig; -import de.cotech.hw.secrets.ByteSecret; -import de.cotech.hw.ui.SecurityKeyDialogFragment; - -import java.io.IOException; - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class OpenPgpSecurityKeyDialogFragment extends SecurityKeyDialogFragment { - public static final String ARG_OPENPGP_CONFIG = "de.cotech.hw.ui.ARG_OPENPGP_CONFIG"; - - @Override - public void initSecurityKeyConnectionMode(Bundle arguments) { - OpenPgpSecurityKeyConnectionModeConfig openpgpConfig = arguments.getParcelable(ARG_OPENPGP_CONFIG); - SecurityKeyManager.getInstance().registerCallback(OpenPgpSecurityKeyConnectionMode.getInstance(openpgpConfig), this, this); - } - - @Override - public void updateSecurityKeyPinUsingPuk(SecurityKey securityKey, ByteSecret puk, ByteSecret newPin) throws IOException { - OpenPgpSecurityKey openPgpSecurityKey = (OpenPgpSecurityKey) securityKey; - openPgpSecurityKey.updatePinUsingPuk(puk, newPin); - } - - @Override - public boolean isSecurityKeyEmpty(SecurityKey securityKey) throws IOException { - OpenPgpSecurityKey openPgpSecurityKey = (OpenPgpSecurityKey) securityKey; - return openPgpSecurityKey.isSecurityKeyEmpty(); - } - -} diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/PinInput.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/PinInput.java index 94f9ab5..9d2552f 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/PinInput.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/PinInput.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -35,5 +35,5 @@ public interface PinInputCallback { void onPinEntered(ByteSecret pinSecret); } - public abstract ByteSecret getPin(); + abstract public void confirmPin(); } diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/ProgressView.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/ProgressView.java index b111331..991de8f 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/ProgressView.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/ProgressView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/SecurityKeyDialogPresenter.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/SecurityKeyDialogPresenter.java new file mode 100644 index 0000000..446205a --- /dev/null +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/SecurityKeyDialogPresenter.java @@ -0,0 +1,403 @@ +/* + * Copyright (C) 2018-2020 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.ui.internal; + +import android.content.Context; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; + +import java.io.IOException; + +import de.cotech.hw.SecurityKey; +import de.cotech.hw.secrets.ByteSecret; +import de.cotech.hw.secrets.ByteSecretGenerator; +import de.cotech.hw.secrets.PinProvider; +import de.cotech.hw.secrets.StaticPinProvider; +import de.cotech.hw.ui.R; +import de.cotech.hw.ui.SecurityKeyDialogOptions; + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public abstract class SecurityKeyDialogPresenter { + + protected static final int SETUP_DEFAULT_PIN_LENGTH = 6; + protected static final int SETUP_DEFAULT_PUK_LENGTH = 8; + + protected static final long TIME_DELAYED_SCREEN_CHANGE = 3000; + + protected Context context; + protected View view; + protected SecurityKeyDialogOptions options; + + protected KeyboardPreferenceRepository keyboardPreferenceRepository; + + protected StaticPinProvider staticPinProvider; + + protected ByteSecret resetNewPinSecret; + protected ByteSecret resetPukSecret; + protected ByteSecret setupPinSecret; + + protected enum Screen { + NORMAL_ENTER_PIN, + NORMAL_SECURITY_KEY, + NORMAL_SECURITY_KEY_HOLD, + NORMAL_ERROR, + RESET_PIN_ENTER_PUK, + RESET_PIN_ENTER_NEW_PIN, + RESET_PIN_SECURITY_KEY, + RESET_PIN_SUCCESS, + RESET_PIN_ERROR, + SETUP_CHOOSE_PIN, + SETUP_SHOW_PUK, + SETUP_CONFIRM_WIPE, + } + + protected Screen currentScreen; + + public SecurityKeyDialogPresenter(View view, Context context, SecurityKeyDialogOptions options) { + this.view = view; + this.context = context; + this.options = options; + + keyboardPreferenceRepository = new KeyboardPreferenceRepository(context); + } + + // functions are implemented in the specific presenters for each protocol, such as OpenPGP, PIV + abstract public void handleError(IOException exception); + + abstract public void updateSecurityKeyPinUsingPuk(SecurityKey securityKey, ByteSecret puk, ByteSecret newPin) throws IOException; + + abstract public boolean isSecurityKeyEmpty(SecurityKey securityKey) throws IOException; + + public void cancel() { + view.cancel(); + } + + public void resetPinScreen() { + gotoScreen(Screen.RESET_PIN_ENTER_PUK); + } + + public void finishedWithPuk() { + gotoScreen(Screen.NORMAL_SECURITY_KEY); + } + + public void gotoScreenOnCreate() { + switch (options.getPinMode()) { + case PIN_INPUT: { + gotoScreen(Screen.NORMAL_ENTER_PIN); + break; + } + case NO_PIN_INPUT: { + gotoScreen(Screen.NORMAL_SECURITY_KEY); + break; + } + case RESET_PIN: { + gotoScreen(Screen.RESET_PIN_ENTER_PUK); + break; + } + case SETUP: { + gotoScreen(Screen.SETUP_CHOOSE_PIN); + break; + } + default: { + throw new IllegalArgumentException("unknown PinMode!"); + } + } + } + + public void onPinEntered(ByteSecret pinSecret) { + // stay on this screen if no PIN is entered + if (pinSecret == null) { + return; + } + + switch (currentScreen) { + case NORMAL_ENTER_PIN: { + staticPinProvider = StaticPinProvider.getInstance(pinSecret); + gotoScreen(Screen.NORMAL_SECURITY_KEY); + break; + } + case RESET_PIN_ENTER_PUK: { + resetPukSecret = pinSecret; + gotoScreen(Screen.RESET_PIN_ENTER_NEW_PIN); + break; + } + case RESET_PIN_ENTER_NEW_PIN: { + resetNewPinSecret = pinSecret; + gotoScreen(Screen.RESET_PIN_SECURITY_KEY); + break; + } + case SETUP_CHOOSE_PIN: { + setupPinSecret = pinSecret; + gotoScreen(Screen.SETUP_SHOW_PUK); + break; + } + default: { + // do nothing + break; + } + } + } + + protected void gotoScreen(Screen newScreen) { + gotoScreen(newScreen, true); + } + + protected void gotoScreen(Screen newScreen, boolean isTransportNfc) { + switch (newScreen) { + case NORMAL_ENTER_PIN: { + view.updateTitle(getTitle(), R.string.hwsecurity_ui_description_enter_pin); + view.screenEnterPin(options.getPinLength(), options.getShowReset(), options.getAllowKeyboard()); + break; + } + case NORMAL_SECURITY_KEY: { + view.updateTitle(getTitle(), R.string.hwsecurity_ui_description_start); + view.screenSecurityKey(options.getPinLength(), options.getFormFactor()); + break; + } + case NORMAL_SECURITY_KEY_HOLD: { + int descriptionResId = isTransportNfc ? R.string.hwsecurity_ui_description_hold_nfc : R.string.hwsecurity_ui_description_hold_usb; + view.updateTitle(getTitle(), descriptionResId); + view.screenHoldSecurityKey(); + break; + } + case RESET_PIN_ENTER_PUK: { + view.updateTitle(R.string.hwsecurity_ui_title_reset_pin, R.string.hwsecurity_ui_description_enter_puk); + view.screenResetPinEnterPinOrPuk(options.getPukLength()); + break; + } + case RESET_PIN_ENTER_NEW_PIN: { + view.updateTitle(R.string.hwsecurity_ui_title_reset_pin, R.string.hwsecurity_ui_description_enter_new_pin); + view.screenResetPinEnterPinOrPuk(options.getPinLength()); + break; + } + case RESET_PIN_SECURITY_KEY: { + view.updateTitle(R.string.hwsecurity_ui_title_reset_pin, R.string.hwsecurity_ui_description_hold_nfc); + view.screenResetPinSecurityKey(options.getFormFactor()); + break; + } + case RESET_PIN_SUCCESS: { + view.updateTitle(R.string.hwsecurity_ui_title_reset_pin, ""); + view.screenResetPinSuccess(); + break; + } + case NORMAL_ERROR: + case RESET_PIN_ERROR: { + view.screenError(); + break; + } + case SETUP_CHOOSE_PIN: { + int pinLength = options.getPinLength() == null ? SETUP_DEFAULT_PIN_LENGTH : options.getPinLength(); + + view.updateTitle(getTitle(), R.string.hwsecurity_ui_description_choose_pin); + view.screenSetupChoosePin(pinLength); + break; + } + case SETUP_SHOW_PUK: { + int pukLength = options.getPukLength() == null ? SETUP_DEFAULT_PUK_LENGTH : options.getPukLength(); + ByteSecret setupPuk = ByteSecretGenerator.getInstance().createRandomNumeric(pukLength); + staticPinProvider = StaticPinProvider.getInstance(setupPinSecret, setupPuk); + + view.updateTitle(getTitle(), R.string.hwsecurity_ui_description_puk); + view.screenSetupChoosePuk(pukLength, setupPuk); + break; + } + case SETUP_CONFIRM_WIPE: { + view.updateTitle(getTitle(), ""); + view.screenConfirmWipe(); + break; + } + } + + currentScreen = newScreen; + } + + public void switchBetweenPinInputs() { + boolean isKeyboardPreferred = keyboardPreferenceRepository.isKeyboardPreferred(); + + if (isKeyboardPreferred) { + keyboardPreferenceRepository.setIsKeyboardPreferred(false); + view.showKeypadPinInput(); + } else { + keyboardPreferenceRepository.setIsKeyboardPreferred(true); + view.showKeyboardPinInput(); + } + } + + public void showPinInput() { + boolean isKeyboardPreferred = keyboardPreferenceRepository.isKeyboardPreferred(); + + if (isKeyboardPreferred) { + view.showKeyboardPinInput(); + } else { + view.showKeypadPinInput(); + } + } + + private String getTitle() { + if (options.getTitle() != null) { + return options.getTitle(); + } + switch (options.getPinMode()) { + case PIN_INPUT: { + return context.getString(R.string.hwsecurity_ui_title_login); + } + case NO_PIN_INPUT: { + return context.getString(R.string.hwsecurity_ui_title_add); + } + case RESET_PIN: { + return context.getString(R.string.hwsecurity_ui_title_reset_pin); + } + case SETUP: { + return context.getString(R.string.hwsecurity_ui_title_setup); + } + default: { + throw new IllegalArgumentException("unknown PinMode!"); + } + } + } + + public void onSecurityKeyDiscovered(@NonNull SecurityKey securityKey, + boolean isWipedConfirmed) { + + switch (currentScreen) { + case NORMAL_ENTER_PIN: { + // Only stay in this screen for USB. Automatically proceed with entered PIN on NFC connection + if (!securityKey.isTransportNfc()) { + break; + } + view.confirmPinInput(); + // fall-through + } + case SETUP_CONFIRM_WIPE: + // fall-through + case NORMAL_SECURITY_KEY: + // fall-through + case NORMAL_SECURITY_KEY_HOLD: { + gotoScreen(Screen.NORMAL_SECURITY_KEY_HOLD, securityKey.isTransportNfc()); + + boolean isSetupMode = options.getPinMode() == SecurityKeyDialogOptions.PinMode.SETUP; + if (isSetupMode && !isWipedConfirmed) { + try { + if (!isSecurityKeyEmpty(securityKey)) { + gotoScreen(Screen.SETUP_CONFIRM_WIPE); + return; + } + } catch (IOException e) { + handleError(e); + } + } + + try { + view.onSecurityKeyDialogDiscoveredCallback(securityKey, staticPinProvider); + } catch (IOException e) { + handleError(e); + return; + } + break; + } + case RESET_PIN_SECURITY_KEY: { + new Thread(() -> { + try { + updateSecurityKeyPinUsingPuk(securityKey, resetPukSecret, resetNewPinSecret); + + view.postRunnable(() -> Toast.makeText(context, R.string.hwsecurity_ui_changed_pin, Toast.LENGTH_LONG).show()); + view.postRunnable(() -> gotoScreen(Screen.RESET_PIN_SUCCESS)); + view.postDelayedRunnable(() -> { + if (options.getPinMode() == SecurityKeyDialogOptions.PinMode.RESET_PIN) { + view.cancel(); + } else { + gotoScreen(Screen.NORMAL_ENTER_PIN); + } + }, TIME_DELAYED_SCREEN_CHANGE); + } catch (IOException e) { + view.postRunnable(() -> handleError(e)); + } + }).start(); + break; + } + default: + // do nothing + } + } + + protected void gotoErrorScreenAndDelayedScreen(String text, Screen errorScreen, Screen delayedScreen) { + view.updateErrorViewText(text); + gotoScreen(errorScreen); + view.postDelayedRunnable(() -> gotoScreen(delayedScreen), TIME_DELAYED_SCREEN_CHANGE); + } + + public interface View { + + void showKeypadPinInput(); + + void showKeyboardPinInput(); + + void confirmPinInput(); + + void cancel(); + + void updateErrorViewText(String text); + + void updateErrorViewText(int text); + + boolean postRunnable(Runnable action); + + boolean postDelayedRunnable(Runnable action, long delayMillis); + + void updateTitle(String title, int descriptionResId); + + void updateTitle(int titleResId, String description); + + void updateTitle(int titleResId, int descriptionResId); + + void updateTitle(String title, String description); + + void screenEnterPin(Integer pinLength, boolean showReset, boolean useKeyboard); + + void screenSecurityKey(Integer pinLength, SecurityKeyDialogOptions.FormFactor formFactor); + + void screenHoldSecurityKey(); + + void screenResetPinEnterPinOrPuk(Integer pinLength); + + void screenResetPinSecurityKey(SecurityKeyDialogOptions.FormFactor formFactor); + + void screenResetPinSuccess(); + + void screenError(); + + void screenSetupChoosePin(int pinLength); + + void screenSetupChoosePuk(int pukLength, ByteSecret setupPuk); + + void screenConfirmWipe(); + + void onSecurityKeyDialogDiscoveredCallback(@NonNull SecurityKey securityKey, @Nullable PinProvider pinProvider) throws IOException; + + } +} diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/SecurityKeyFormFactor.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/SecurityKeyFormFactor.java index 95e95e7..8985241 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/SecurityKeyFormFactor.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/SecurityKeyFormFactor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -24,23 +24,27 @@ package de.cotech.hw.ui.internal; -import android.animation.*; import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.Build; import android.provider.Settings; -import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.widget.*; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; -import androidx.annotation.AttrRes; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.constraintlayout.widget.Guideline; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.OnLifecycleEvent; import androidx.transition.AutoTransition; import androidx.transition.Scene; import androidx.transition.Transition; @@ -50,53 +54,69 @@ import de.cotech.hw.ui.R; import de.cotech.hw.util.NfcStatusObserver; -import java.util.ArrayList; -import java.util.List; - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class SecurityKeyFormFactor { +public class SecurityKeyFormFactor implements LifecycleObserver { private Context context; private ViewGroup view; - private ConstraintLayout innerBottomSheet; - private Guideline guidelineForceHeight; private TextView textTitle; private TextView textDescription; + private TextView textNfc; private TextView textUsb; private ImageView imageNfc; - private ImageView imageNfcFullscreen; private ImageView imageUsb; private TextView textViewNfcDisabled; private Button buttonNfcDisabled; - private ImageView sweetspotIndicator; - private TextView textNfcFullscreen; + private NfcStatusObserver nfcStatusObserver; + + private SelectTransportCallback callback; + + public interface SelectTransportCallback { + void screeFullscreenNfc(); - public SecurityKeyFormFactor(@NonNull ViewGroup view, ConstraintLayout innerBottomSheet) { + void onSecurityKeyFormFactorClickUsb(); + } + + public SecurityKeyFormFactor(@NonNull ViewGroup view, LifecycleOwner lifecycleOwner, SelectTransportCallback callback, ConstraintLayout innerBottomSheet, boolean showSdkButton) { this.context = view.getContext(); this.view = view; - this.innerBottomSheet = innerBottomSheet; + this.callback = callback; + + lifecycleOwner.getLifecycle().addObserver(this); + nfcStatusObserver = new NfcStatusObserver(context, lifecycleOwner, this::showOrHideNfcDisabledView); - guidelineForceHeight = innerBottomSheet.findViewById(R.id.guidelineForceHeight); textTitle = innerBottomSheet.findViewById(R.id.textTitle); textDescription = innerBottomSheet.findViewById(R.id.textDescription); textNfc = view.findViewById(R.id.textNfc); - textNfcFullscreen = view.findViewById(R.id.textNfcFullscreen); textUsb = view.findViewById(R.id.textUsb); imageNfc = view.findViewById(R.id.imageNfc); - imageNfcFullscreen = view.findViewById(R.id.imageNfcFullscreen); - sweetspotIndicator = view.findViewById(R.id.imageNfcSweetspot); imageUsb = view.findViewById(R.id.imageUsb); textViewNfcDisabled = view.findViewById(R.id.textNfcDisabled); buttonNfcDisabled = view.findViewById(R.id.buttonNfcDisabled); + ImageButton sdkButton = view.findViewById(R.id.buttonSdk); + + sdkButton.setVisibility(showSdkButton ? View.VISIBLE : View.GONE); + sdkButton.setOnClickListener(v -> { + String packageName = context.getPackageName(); + String url = "https://hwsecurity.dev/?pk_campaign=sdk&pk_source=" + packageName; + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + context.startActivity(i); + }); imageNfc.setOnClickListener(v -> animateSelectNfc()); - imageUsb.setOnClickListener(v -> animateSelectUsb()); + imageUsb.setOnClickListener(v -> { + callback.onSecurityKeyFormFactorClickUsb(); + animateSelectUsb(); + }); + + showOrHideNfcView(); } public void setVisibility(int visibility) { @@ -107,7 +127,15 @@ public int getVisibility() { return view.getVisibility(); } - private void showOrHideNfcView(NfcStatusObserver nfcStatusObserver) { + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + public void onResume() { + // re-check NFC status, maybe user is coming back from settings + if (getVisibility() == View.VISIBLE) { + showOrHideNfcView(); + } + } + + private void showOrHideNfcView() { boolean isNfcHardwareAvailable = SecurityKeyManager.getInstance().isNfcHardwareAvailable(); textNfc.setVisibility(isNfcHardwareAvailable ? View.VISIBLE : View.GONE); imageNfc.setVisibility(isNfcHardwareAvailable ? View.VISIBLE : View.GONE); @@ -130,24 +158,34 @@ private void showOrHideNfcDisabledView(boolean nfcEnabled) { @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) private void startAndroidNfcConfigActivityWithHint() { - Toast.makeText(context.getApplicationContext(), - R.string.hwsecurity_nfc_settings_toast, Toast.LENGTH_SHORT).show(); - context.startActivity(new Intent(Settings.ACTION_NFC_SETTINGS)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + context.startActivity(new Intent(Settings.Panel.ACTION_NFC)); + } else { + Toast.makeText(context.getApplicationContext(), + R.string.hwsecurity_ui_nfc_settings_toast, Toast.LENGTH_LONG).show(); + context.startActivity(new Intent(Settings.ACTION_NFC_SETTINGS)); + } } - private void removeOnClickListener() { imageNfc.setOnClickListener(null); imageUsb.setOnClickListener(null); } - private int resolveColorFromAttr(@AttrRes int resId) { - TypedValue outValue = new TypedValue(); - context.getTheme().resolveAttribute(resId, outValue, true); - return outValue.data; + public void resetAnimation() { + imageNfc.setImageResource(R.drawable.hwsecurity_nfc_start); + imageUsb.setImageResource(R.drawable.hwsecurity_usb_start); + + AutoTransition selectModeTransition = new AutoTransition(); + selectModeTransition.setDuration(150); + + TransitionManager.go(new Scene(view), selectModeTransition); + showOrHideNfcView(); + imageUsb.setVisibility(View.VISIBLE); + textUsb.setVisibility(View.VISIBLE); } - private void animateSelectNfc() { + public void animateSelectNfc() { removeOnClickListener(); AutoTransition selectModeTransition = new AutoTransition(); @@ -159,69 +197,7 @@ public void onTransitionStart(@NonNull Transition transition) { @Override public void onTransitionEnd(@NonNull Transition transition) { - int colorFrom = context.getResources().getColor(R.color.hwSecurityWhite); - int colorTo = resolveColorFromAttr(R.attr.hwSecuritySurfaceColor); - ValueAnimator colorChange = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); - colorChange.setDuration(100); - colorChange.addUpdateListener(animator -> { - innerBottomSheet.setBackgroundColor((int) animator.getAnimatedValue()); - }); - - ObjectAnimator fadeInImageNfcFullscreen = ObjectAnimator - .ofFloat(imageNfcFullscreen, View.ALPHA, 0, 1) - .setDuration(150); - fadeInImageNfcFullscreen.setStartDelay(50); - fadeInImageNfcFullscreen.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - imageNfcFullscreen.setVisibility(View.VISIBLE); - } - - @Override - public void onAnimationEnd(Animator animation) { - AnimatedVectorDrawableHelper.startAndLoopAnimation(imageNfcFullscreen, R.drawable.hwsecurity_nfc_handling); - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - }); - - ObjectAnimator fadeOutNfcFullscreen = ObjectAnimator - .ofFloat(imageNfc, View.ALPHA, 1, 0) - .setDuration(150); - fadeInImageNfcFullscreen.setStartDelay(50); - fadeInImageNfcFullscreen.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - } - - @Override - public void onAnimationEnd(Animator animation) { - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - }); - - List items = new ArrayList<>(); - items.add(fadeInImageNfcFullscreen); - items.add(fadeOutNfcFullscreen); - items.add(colorChange); - - AnimatorSet set = new AnimatorSet(); - set.playTogether(items); - set.setInterpolator(new AccelerateDecelerateInterpolator()); - set.start(); + callback.screeFullscreenNfc(); } @Override @@ -238,7 +214,7 @@ public void onTransitionResume(@NonNull Transition transition) { }); TransitionManager.go(new Scene(view), selectModeTransition); - textTitle.setText(R.string.hwsecurity_title_nfc_fullscreen); + textTitle.setText(R.string.hwsecurity_ui_title_nfc_fullscreen); imageUsb.setVisibility(View.GONE); textViewNfcDisabled.setVisibility(View.GONE); buttonNfcDisabled.setVisibility(View.GONE); @@ -247,7 +223,7 @@ public void onTransitionResume(@NonNull Transition transition) { textUsb.setVisibility(View.GONE); } - private void animateSelectUsb() { + public void animateSelectUsb() { removeOnClickListener(); AutoTransition selectModeTransition = new AutoTransition(); @@ -276,7 +252,7 @@ public void onTransitionResume(@NonNull Transition transition) { }); TransitionManager.go(new Scene(view), selectModeTransition); - textTitle.setText(R.string.hwsecurity_title_usb_selected); + textTitle.setText(R.string.hwsecurity_ui_title_usb_selected); imageNfc.setVisibility(View.GONE); textViewNfcDisabled.setVisibility(View.GONE); buttonNfcDisabled.setVisibility(View.GONE); @@ -285,4 +261,45 @@ public void onTransitionResume(@NonNull Transition transition) { textUsb.setVisibility(View.GONE); } + public void animateSelectUsbAndPressButton() { + AutoTransition selectModeTransition = new AutoTransition(); + selectModeTransition.setDuration(150); + selectModeTransition.addListener(new Transition.TransitionListener() { + @Override + public void onTransitionStart(@NonNull Transition transition) { + } + + @Override + public void onTransitionEnd(@NonNull Transition transition) { + AnimatedVectorDrawableHelper.startAndLoopAnimation(imageUsb, R.drawable.hwsecurity_usb_handling_b); + } + + @Override + public void onTransitionCancel(@NonNull Transition transition) { + } + + @Override + public void onTransitionPause(@NonNull Transition transition) { + } + + @Override + public void onTransitionResume(@NonNull Transition transition) { + } + }); + + TransitionManager.go(new Scene(view), selectModeTransition); + textTitle.setText(R.string.hwsecurity_ui_title_usb_button); + imageNfc.setVisibility(View.GONE); + textViewNfcDisabled.setVisibility(View.GONE); + buttonNfcDisabled.setVisibility(View.GONE); + textDescription.setVisibility(View.GONE); + textNfc.setVisibility(View.GONE); + textUsb.setVisibility(View.GONE); + } + + public void animateUsbPressButton() { + textTitle.setText(R.string.hwsecurity_ui_title_usb_button); + AnimatedVectorDrawableHelper.startAndLoopAnimation(imageUsb, R.drawable.hwsecurity_usb_handling_b); + } + } diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/SmartcardFormFactor.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/SmartcardFormFactor.java index cb318b9..ce0584c 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/SmartcardFormFactor.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/SmartcardFormFactor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial @@ -25,26 +25,49 @@ package de.cotech.hw.ui.internal; import android.content.Context; +import android.content.Intent; import android.graphics.drawable.Animatable; +import android.os.Build; +import android.provider.Settings; import android.view.View; +import android.widget.Button; import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.OnLifecycleEvent; +import de.cotech.hw.SecurityKeyManager; import de.cotech.hw.ui.R; +import de.cotech.hw.util.NfcStatusObserver; @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class SmartcardFormFactor { +public class SmartcardFormFactor implements LifecycleObserver { private Context context; private View view; private ImageView smartcardAnimation; - public SmartcardFormFactor(@NonNull View view) { + private TextView textViewNfcDisabled; + private Button buttonNfcDisabled; + + private NfcStatusObserver nfcStatusObserver; + + public SmartcardFormFactor(@NonNull View view, LifecycleOwner lifecycleOwner) { this.context = view.getContext(); this.view = view; + lifecycleOwner.getLifecycle().addObserver(this); + nfcStatusObserver = new NfcStatusObserver(context, lifecycleOwner, this::showOrHideNfcDisabledView); + + textViewNfcDisabled = view.findViewById(R.id.textNfcDisabled); + buttonNfcDisabled = view.findViewById(R.id.buttonNfcDisabled); smartcardAnimation = view.findViewById(R.id.smartcardAnimation); smartcardAnimation.setOnClickListener(v -> { @@ -53,6 +76,46 @@ public SmartcardFormFactor(@NonNull View view) { AnimatedVectorDrawableHelper.startAnimation(smartcardAnimation, R.drawable.hwsecurity_smartcard_animation); } ); + + showOrHideNfcView(); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + public void onResume() { + // re-check NFC status, maybe user is coming back from settings + if (getVisibility() == View.VISIBLE) { + showOrHideNfcView(); + } + } + + private void showOrHideNfcView() { + boolean isNfcHardwareAvailable = SecurityKeyManager.getInstance().isNfcHardwareAvailable(); + smartcardAnimation.setVisibility(isNfcHardwareAvailable ? View.VISIBLE : View.GONE); + + if (isNfcHardwareAvailable) { + boolean nfcEnabled = nfcStatusObserver.isNfcEnabled(); + showOrHideNfcDisabledView(nfcEnabled); + } + } + + private void showOrHideNfcDisabledView(boolean nfcEnabled) { + textViewNfcDisabled.setVisibility(nfcEnabled ? View.GONE : View.VISIBLE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + buttonNfcDisabled.setOnClickListener(v -> startAndroidNfcConfigActivityWithHint()); + buttonNfcDisabled.setVisibility(nfcEnabled ? View.GONE : View.VISIBLE); + } + smartcardAnimation.setVisibility(nfcEnabled ? View.VISIBLE : View.INVISIBLE); + } + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + private void startAndroidNfcConfigActivityWithHint() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + context.startActivity(new Intent(Settings.Panel.ACTION_NFC)); + } else { + Toast.makeText(context.getApplicationContext(), + R.string.hwsecurity_ui_nfc_settings_toast, Toast.LENGTH_LONG).show(); + context.startActivity(new Intent(Settings.ACTION_NFC_SETTINGS)); + } } public void setVisibility(int visibility) { diff --git a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/WipeConfirmView.java b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/WipeConfirmView.java index e656b11..78682c1 100644 --- a/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/WipeConfirmView.java +++ b/hwsecurity/ui/src/main/java/de/cotech/hw/ui/internal/WipeConfirmView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2019 Confidential Technologies GmbH + * Copyright (C) 2018-2020 Confidential Technologies GmbH * * You can purchase a commercial license at https://hwsecurity.dev. * Buying such a license is mandatory as soon as you develop commercial diff --git a/hwsecurity/ui/src/main/res/drawable/hwsecurity_sdk_url_logo.xml b/hwsecurity/ui/src/main/res/drawable/hwsecurity_sdk_url_logo.xml new file mode 100644 index 0000000..1643629 --- /dev/null +++ b/hwsecurity/ui/src/main/res/drawable/hwsecurity_sdk_url_logo.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_dialog.xml b/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_dialog.xml index cd32828..8151a0e 100644 --- a/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_dialog.xml +++ b/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_dialog.xml @@ -4,102 +4,32 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/hwSecurityDialogBottomSheet" android:layout_width="match_parent" - android:layout_height="wrap_content"> - - - - + android:layout_height="match_parent"> + app:layout_constraintGuide_end="32dp" /> - - - - - + android:orientation="horizontal" + app:layout_constraintGuide_end="402dp" /> + app:layout_constraintTop_toBottomOf="@+id/textDescription" + tools:visibility="visible" /> + + + + + + + + + + + + \ No newline at end of file diff --git a/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_error.xml b/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_error.xml index 67cbbe6..50136a5 100644 --- a/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_error.xml +++ b/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_error.xml @@ -12,7 +12,7 @@ android:layout_height="168dp" android:layout_marginStart="8dp" android:layout_marginLeft="8dp" - android:layout_marginTop="32dp" + android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:layout_marginRight="8dp" android:layout_marginBottom="32dp" diff --git a/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_nfc_fullscreen.xml b/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_nfc_fullscreen.xml new file mode 100644 index 0000000..90d2bb9 --- /dev/null +++ b/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_nfc_fullscreen.xml @@ -0,0 +1,49 @@ + + + + + + + + + + \ No newline at end of file diff --git a/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_security_key_form_factor.xml b/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_security_key_form_factor.xml index 41164ec..6573e3d 100644 --- a/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_security_key_form_factor.xml +++ b/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_security_key_form_factor.xml @@ -2,37 +2,21 @@ - - - + app:srcCompat="@drawable/hwsecurity_sdk_url_logo" /> - - @@ -123,7 +96,7 @@ android:id="@+id/textNfc" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/hwsecurity_start_nfc" + android:text="@string/hwsecurity_ui_start_nfc" app:layout_constraintBottom_toTopOf="@+id/imageNfc" app:layout_constraintEnd_toEndOf="@+id/imageNfc" app:layout_constraintStart_toStartOf="@+id/imageNfc" /> diff --git a/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_smartcard_form_factor.xml b/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_smartcard_form_factor.xml index 1b05772..4a3b8d0 100644 --- a/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_smartcard_form_factor.xml +++ b/hwsecurity/ui/src/main/res/layout/hwsecurity_security_key_smartcard_form_factor.xml @@ -18,4 +18,37 @@ app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/hwsecurity_smartcard_animation" /> + + + + \ No newline at end of file diff --git a/hwsecurity/ui/src/main/res/values/colors.xml b/hwsecurity/ui/src/main/res/values/colors.xml index 740ae32..e71ef59 100644 --- a/hwsecurity/ui/src/main/res/values/colors.xml +++ b/hwsecurity/ui/src/main/res/values/colors.xml @@ -3,7 +3,7 @@ #fff #9dc5d2 - #006a8bs + #006a8b #e00f00 #55d264 diff --git a/hwsecurity/ui/src/main/res/values/strings.xml b/hwsecurity/ui/src/main/res/values/strings.xml index d39adf1..f4fb6c0 100644 --- a/hwsecurity/ui/src/main/res/values/strings.xml +++ b/hwsecurity/ui/src/main/res/values/strings.xml @@ -1,38 +1,49 @@ - Wrong PIN code, you have %d remaining attempts. - PIN code too short! - You have entered an incorrect PIN code 3 times!\nYou must reset the PIN to use the Security Key again. - Wrong PUK code, you have %d remaining attempts. - PUK code too short! - You have entered an incorrect PUK code 3 times!\nThis Security Key is deactivated. Generate new keys to reset it. - No cryptographic key on this Security Key! + Cancel Reset PIN Login with Security Key Add Security Key - Enter your PIN code. + Enter your PIN. "Just do it, or click for help." Hold Security Key against the back. Don\'t move it. Keep Security Key inserted. Don\'t remove it. - Reset PIN Code - Enter the PUK code (not PIN!) - Enter your new favorite PIN code - PIN code has been changed! - NFC help - USB help - Insert your Security Key into the USB port. - Hold the Security Key at the back of your smartphone at the NFC spot. - NFC is disabled. You can enable it in Android\'s NFC settings. - NFC Settings - Please activate NFC and press Back to return to the app. - Choose a PIN code that protects your Security Key. + Reset PIN + Enter the PUK (not PIN!) + Enter your new favorite PIN + PIN has been changed! + NFC help + USB help + Insert your Security Key into the USB port. + Hold the Security Key at the back of your smartphone at the NFC spot. + NFC is disabled. You can enable it in Android\'s NFC settings. + NFC Settings + Please activate NFC and press Back to return to the app. + Choose a PIN that protects your Security Key. Setup your Security Key - Please write down the PUK code and store it in a safe place. - I have written down this PUK code.\nWithout it, I will be unable to reset the PIN code. + Please write down the PUK and store it in a safe place. + I have written down this PUK.\nWithout it, I will be unable to reset the PIN. Your PUK: Replace existing keys Your Security Key is not empty. Again, hold Security key against the back.\nDon\'t move it. + Tap the button or golden disc. + + + Register your Security Key + Login with your Security Key + Register your Security Key for\n%s + Login with your Security Key to\n%s + Wrong Security Key used. + Timeout. Please try again! + No PIN + Wrong PIN, you have %d remaining attempts. + Enter the PIN for this Security Key. + You must reset the Security Key to use it again. + Website requests PIN protection, but this Security Key does not support it. + PIN must be at least 4 digits long. + Website requests PIN protection. Set a PIN by using your Security Key with your desktop computer. + Internal error: %1$s \ No newline at end of file diff --git a/hwsecurity/ui/src/main/res/values/themes.xml b/hwsecurity/ui/src/main/res/values/themes.xml index 15f8176..30c9420 100644 --- a/hwsecurity/ui/src/main/res/values/themes.xml +++ b/hwsecurity/ui/src/main/res/values/themes.xml @@ -33,7 +33,7 @@