diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..1820a98
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,2 @@
+[*.{kt,kts}]
+kotlin_disabled_rules = no-wildcard-imports, import-ordering
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..874c0ce
--- /dev/null
+++ b/README.md
@@ -0,0 +1,37 @@
+# AusweisApp2 SDK Wrapper
+
+The SDK Wrapper is an android library for providing a convenient frontend to the AusweisApp2.
+
+## Installation
+
+For now you need to include the aars in tester/libs in your project.
+You also have to make sure to add all dependencies found in tester/build.gradle to your project.
+
+## Usage
+
+You can use the app as a simple wrapper of the ausweisapp2 sdk via the WorkflowController:
+
+ import de.governikus.ausweiapp2.sdkwrapper.SDKWrapper
+
+ val tcTokenUrl: Uri = ...
+ SDKWrapper.workflowController.registerCallbacks(object : WorkflowCallbacks {
+ override fun onStarted() {
+ SDKWrapper.workflowController.startAuthentication(tcTokenUrl)
+ }
+
+ override fun onAuthenticationCompleted(authResult: AuthResult) {
+ [...]
+ }
+ })
+
+ SDKWrapper.workflowController.start()
+
+## Contact
+
+ Governikus GmbH & Co. KG.
+ Hochschulring 4
+ 28359 Bremen
+
+## License
+
+Copyright (c) 2023 Governikus GmbH & Co. KG, Germany
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..c7643a5
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,44 @@
+buildscript {
+ ext.kotlin_version = '1.8.10'
+ ext.sdkwrapper_version = "1.26.3.0"
+
+ repositories {
+ google()
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:7.4.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath "org.jmailen.gradle:kotlinter-gradle:3.13.0"
+ classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3"
+ }
+}
+
+plugins {
+ id("org.sonarqube") version "3.5.0.2730"
+ id("java")
+}
+
+version = sdkwrapper_version
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ maven { url "$project.rootDir/libs" }
+ }
+}
+
+task tarball(type: Tar) {
+ into ('/') {
+ from rootDir
+ include '**'
+ exclude ('**/build/**', 'tester', '.reviewboardrc', '.gradle', '.idea', 'jenkins')
+ }
+
+ destinationDirectory = file('build/tar')
+ compression = Compression.GZIP
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100755
index 0000000..0ddec36
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,9 @@
+org.gradle.jvmargs=-Xmx1536m
+android.useAndroidX=true
+android.enableJetifier=true
+kotlin.code.style=official
+
+systemProp.sonar.host.url=https://sonar.govkg.de
+systemProp.sonar.projectKey=SDKWrapper-Android
+systemProp.sonar.sourceEncoding=UTF-8
+systemProp.sonar.language=kotlin
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..249e583
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..ae04661
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..a69d9cb
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,240 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original 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
+#
+# https://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 POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${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" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..53a6b23
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,91 @@
+@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 https://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
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@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" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/lint.xml b/lint.xml
new file mode 100644
index 0000000..c76b768
--- /dev/null
+++ b/lint.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sdkwrapper/build.gradle b/sdkwrapper/build.gradle
new file mode 100644
index 0000000..580279e
--- /dev/null
+++ b/sdkwrapper/build.gradle
@@ -0,0 +1,137 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-parcelize'
+apply plugin: 'kotlin-kapt'
+apply plugin: "org.jmailen.kotlinter"
+apply plugin: 'maven-publish'
+
+android {
+ compileSdkVersion 33
+ namespace "de.governikus.ausweisapp2.sdkwrapper"
+
+ defaultConfig {
+ buildToolsVersion "30.0.3"
+ minSdkVersion 24
+ targetSdkVersion 33
+ versionCode 2
+ versionName sdkwrapper_version
+ archivesBaseName = "sdkwrapper"
+
+ consumerProguardFiles 'consumer-rules.pro'
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ testOptions.unitTests.includeAndroidResources = true
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_1_8.toString()
+ allWarningsAsErrors = true
+ freeCompilerArgs = ["-opt-in=kotlin.RequiresOptIn"] // Used for UnitTests, can be removed with kotlin 1.7.0
+ }
+
+ lintOptions {
+ setLintConfig(file("../lint.xml"))
+ checkDependencies false
+ warningsAsErrors true
+ }
+
+ buildFeatures {
+ dataBinding = true
+ }
+
+ buildTypes {
+ debug {
+ minifyEnabled false
+ }
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ libraryVariants.all { variant ->
+ variant.outputs.all {
+ outputFileName = "${archivesBaseName}-${variant.name}-${defaultConfig.versionName}.aar"
+ }
+ }
+
+ publishing {
+ singleVariant("release")
+ }
+
+ packagingOptions {
+ jniLibs {
+ useLegacyPackaging = true
+ }
+ }
+}
+
+task sourceJar(type: Jar) {
+ from android.sourceSets.main.java.srcDirs
+ archiveClassifier = "sources"
+ archiveVersion = android.defaultConfig.versionName
+}
+
+afterEvaluate {
+ publishing {
+ repositories {
+ maven {
+ name = 'Local'
+ url "$project.rootDir/build/dist/libs"
+ }
+ maven {
+ name = 'Nexus'
+ credentials {
+ username System.getenv("NEXUS_USERNAME")
+ password System.getenv("NEXUS_PSW")
+ }
+ url = System.getenv("NEXUS_URL")
+ }
+ }
+ publications {
+ snapshot(MavenPublication) {
+ from components.release
+ groupId = 'de.governikus.ausweisapp2'
+ artifactId = 'sdkwrapper'
+ version = "${android.defaultConfig.versionName}-SNAPSHOT"
+
+ artifact(sourceJar)
+ }
+ release(MavenPublication) {
+ from components.release
+ pom {
+ name = 'AusweisApp2 SDK Wrapper'
+ description = 'Convenient authentication SDK'
+ url = 'https://www.governikus.de'
+ }
+ groupId = 'de.governikus.ausweisapp2'
+ artifactId = 'sdkwrapper'
+ version = android.defaultConfig.versionName
+
+ artifact(sourceJar)
+ }
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ implementation "com.governikus:ausweisapp:1.26.3"
+ implementation 'com.google.code.gson:gson:2.10.1'
+
+ testImplementation "junit:junit:4.13.2"
+ testImplementation 'androidx.test:core:1.5.0'
+ testImplementation 'org.robolectric:robolectric:4.9.2'
+ testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
+
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
+
+}
diff --git a/sdkwrapper/consumer-rules.pro b/sdkwrapper/consumer-rules.pro
new file mode 100644
index 0000000..3beb66c
--- /dev/null
+++ b/sdkwrapper/consumer-rules.pro
@@ -0,0 +1,6 @@
+-keep class androidx.databinding.** { *; }
+
+-keep class de.governikus.ausweisapp2.sdkwrapper.DataBinderMapperImpl { *; }
+-keep class de.governikus.ausweisapp2.sdkwrapper.DataBinderMapperImpl$** { *; }
+
+-keepclassmembers,allowoptimization class de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.** { *; }
diff --git a/sdkwrapper/proguard-rules.pro b/sdkwrapper/proguard-rules.pro
new file mode 100644
index 0000000..3067231
--- /dev/null
+++ b/sdkwrapper/proguard-rules.pro
@@ -0,0 +1,42 @@
+-dontobfuscate
+-keep class kotlin.Metadata { *; }
+-keepattributes RuntimeVisibleAnnotations
+-keep class androidx.databinding.** { *; }
+
+-keep class de.governikus.ausweisapp2.sdkwrapper.DataBinderMapperImpl { *; }
+-keep class de.governikus.ausweisapp2.sdkwrapper.DataBinderMapperImpl$** { *; }
+
+-keep class de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.** { *; }
+-keep class de.governikus.ausweisapp2.sdkwrapper.card.core.CertificateDescription { *; }
+-keep class de.governikus.ausweisapp2.sdkwrapper.card.core.CertificateValidity { *; }
+-keep class de.governikus.ausweisapp2.sdkwrapper.card.core.AccessRights { *; }
+-keep class de.governikus.ausweisapp2.sdkwrapper.card.core.AuxiliaryData { *; }
+-keep class de.governikus.ausweisapp2.sdkwrapper.card.core.Card { *; }
+-keep class de.governikus.ausweisapp2.sdkwrapper.card.core.AuthResult { *; }
+-keep class de.governikus.ausweisapp2.sdkwrapper.card.core.AuthResultData { *; }
+-keep class de.governikus.ausweisapp2.sdkwrapper.card.core.WrapperError { *; }
+-keep class de.governikus.ausweisapp2.sdkwrapper.card.core.ChangePinResult { *; }
+-keep enum de.governikus.ausweisapp2.sdkwrapper.card.core.AccessRight { *; }
+-keep enum de.governikus.ausweisapp2.sdkwrapper.card.core.WorkflowProgressType { *; }
+
+-keep class de.governikus.ausweisapp2.sdkwrapper.card.core.NfcForegroundDispatcher { *; }
+
+-keep public interface de.governikus.ausweisapp2.sdkwrapper.card.core.WorkflowCallbacks { *; }
+-keep public class de.governikus.ausweisapp2.sdkwrapper.card.core.WorkflowController {
+ public ;
+ public ;
+}
+
+-keep class de.governikus.ausweisapp2.sdkwrapper.SDKWrapper$** {
+ public ;
+}
+-keep class de.governikus.ausweisapp2.sdkwrapper.SDKWrapper {
+ public *;
+}
+
+-assumenosideeffects class android.util.Log {
+ public static boolean isLoggable(java.lang.String, int);
+ public static int v(...);
+ public static int d(...);
+ public static int i(...);
+}
diff --git a/sdkwrapper/src/main/AndroidManifest.xml b/sdkwrapper/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3f76fcf
--- /dev/null
+++ b/sdkwrapper/src/main/AndroidManifest.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/SDKWrapper.kt b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/SDKWrapper.kt
new file mode 100644
index 0000000..eff4ec3
--- /dev/null
+++ b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/SDKWrapper.kt
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany
+ */
+
+package de.governikus.ausweisapp2.sdkwrapper
+
+import de.governikus.ausweisapp2.sdkwrapper.card.core.WorkflowController
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.AA2SdkConnection
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+
+object SDKWrapper : CoroutineScope by MainScope() {
+ val workflowController = WorkflowController(AA2SdkConnection())
+}
diff --git a/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/NfcForegroundDispatcher.kt b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/NfcForegroundDispatcher.kt
new file mode 100644
index 0000000..33a00c1
--- /dev/null
+++ b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/NfcForegroundDispatcher.kt
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany
+ */
+
+package de.governikus.ausweisapp2.sdkwrapper.card.core
+
+import android.app.Activity
+import android.nfc.NfcAdapter.FLAG_READER_NFC_A
+import android.nfc.NfcAdapter.FLAG_READER_NFC_B
+import android.nfc.NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
+import android.nfc.NfcAdapter.getDefaultAdapter
+import android.nfc.Tag
+import android.nfc.tech.IsoDep
+
+/**
+ * ForegroundDispatcher used to detect ISO-DEP (ISO 14443-4) NFC tags, like an id card.
+ *
+ * Use it to detect and pass detected id cards to the [WorkflowController.onNfcTagDetected] function.
+ *
+ * @param activity Activity that the NFCAdapter will be attached to
+ * @param workflowController Workflow manager that is th receive the nfc tags
+ */
+class NfcForegroundDispatcher(
+ private val activity: Activity,
+ private val workflowController: WorkflowController
+) {
+ private val nfcAdapter by lazy {
+ getDefaultAdapter(activity)
+ }
+ private var isStarted = false
+ private val nfcReaderFlags =
+ FLAG_READER_NFC_A or FLAG_READER_NFC_B or FLAG_READER_SKIP_NDEF_CHECK
+ private val nfcTechnology = IsoDep::class.java.name
+
+ fun start() {
+ if (isStarted) {
+ return
+ }
+ isStarted = true
+ nfcAdapter.enableReaderMode(
+ activity,
+ { tag: Tag ->
+ if (tag.techList.contains(nfcTechnology)) {
+ workflowController.onNfcTagDetected(tag)
+ }
+ },
+ nfcReaderFlags,
+ null
+ )
+ }
+
+ /**
+ * Stop the [NfcForegroundDispatcher]
+ *
+ * Must be called in [Activity.onPause], when the activity put into the background
+ */
+ fun stop() {
+ if (!isStarted) {
+ return
+ }
+ isStarted = false
+ nfcAdapter.disableReaderMode(activity)
+ }
+}
diff --git a/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/WorkflowCallbacks.kt b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/WorkflowCallbacks.kt
new file mode 100644
index 0000000..f8e0867
--- /dev/null
+++ b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/WorkflowCallbacks.kt
@@ -0,0 +1,199 @@
+/**
+ * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany
+ */
+
+package de.governikus.ausweisapp2.sdkwrapper.card.core
+
+/**
+ * Authentication workflow callbacks.
+ *
+ * You need to register them with the [WorkflowController]
+ *
+ * @see WorkflowController.registerCallbacks
+ */
+interface WorkflowCallbacks {
+
+ /**
+ * [WorkflowController] has successfully been initialized.
+ */
+ fun onStarted()
+
+ /**
+ * An authentication has been started via [WorkflowController.startAuthentication].
+ *
+ * The next callback should be [onAccessRights] or [onAuthenticationCompleted] if the authentication immediately results in an error.
+ */
+ fun onAuthenticationStarted()
+
+ /**
+ * An authentication could not be started.
+ * This is different from an authentication that was started but failed during the process.
+ *
+ * @param error Error message about why the authentication could not be started.
+ */
+ fun onAuthenticationStartFailed(error: String)
+
+ /**
+ * Indicates that the authentication workflow is completed.
+ *
+ * The [AuthResult] will contain a refresh url or in case of an error a communication error address.
+ * You can check the state of the authentication, by looking for the [AuthResult.result] field.
+ *
+ * @param authResult Result of the authentication
+ */
+ fun onAuthenticationCompleted(authResult: AuthResult)
+
+ /**
+ * A pin change has been started via [WorkflowController.startChangePin].
+ */
+ fun onChangePinStarted()
+
+ /**
+ * Access rights requested in response to an authentication.
+ *
+ * This function will be called once the authentication is started by [WorkflowController.startAuthentication]
+ * and the SDK got the certificate from the service.
+ *
+ * Accept ([WorkflowController.accept]) the rights to continue with the workflow or completely
+ * abort the workflow with ([WorkflowController.cancel]).
+ *
+ * It is also possible to change the optional rights via [WorkflowController.setAccessRights].
+ *
+ * @param error Optional error message if the call to [WorkflowController.setAccessRights] failed.
+ * @param accessRights Requested access rights.
+ */
+ fun onAccessRights(error: String?, accessRights: AccessRights?)
+
+ /**
+ * Provides information about the used certificate.
+ *
+ * Response of a call to [WorkflowController.getCertificate].
+ *
+ * @param certificateDescription Requested certificate.
+ */
+ fun onCertificate(certificateDescription: CertificateDescription)
+
+ /**
+ * Indicates that the workflow now requires a card to continue.
+ *
+ * If your application receives this message it should show a hint to the user.
+ * After the user inserted a card the workflow will automatically continue, unless the eID functionality is disabled.
+ * In this case, the workflow will be paused until another card is inserted.
+ * If the user already inserted a card this function will not be called at all.
+ *
+ * @param error Optional detailed error message if the previous call to [WorkflowController.setCard] failed.
+ */
+ fun onInsertCard(error: String?)
+
+ /**
+ * A specific reader was recognized or has vanished. Also called as a response to [WorkflowController.getReader].
+ *
+ * @param reader Recognized or vanished reader, might be null if an unknown reader was requested in [WorkflowController.getReader].
+ */
+ fun onReader(reader: Reader?)
+
+ /**
+ * Called as a reponse to [WorkflowController.getReaderList].
+ *
+ * @param readers Optional list of present readers (if any).
+ */
+ fun onReaderList(readers: List?)
+
+ /**
+ * Indicates that a PIN is required to continue the workflow.
+ *
+ * A PIN is needed to unlock the id card, provide it with [WorkflowController.setPin].
+ *
+ * @param error Optional error message if the call to [WorkflowController.setPin] failed.
+ * @param reader Reader the PIN is requested for
+ */
+ fun onEnterPin(error: String?, reader: Reader)
+
+ /**
+ * Indicates that a new PIN is required to continue the workflow.
+ *
+ * A new PIN is needed fin response to a pin change, provide it with [WorkflowController.setNewPin].
+ *
+ * @param error Optional error message if the call to [WorkflowController.setNewPin] failed.
+ * @param reader Reader the new PIN is requested for
+ */
+ fun onEnterNewPin(error: String?, reader: Reader)
+
+ /**
+ * Indicates that a PUK is required to continue the workflow.
+ *
+ * A PUK is needed to unlock the id card, provide it with [WorkflowController.setPuk].
+ *
+ * @param error Optional error message if the call to [WorkflowController.setPuk] failed.
+ * @param reader Reader the PUK is requested for
+ */
+ fun onEnterPuk(error: String?, reader: Reader)
+
+ /**
+ * Indicates that a CAN is required to continue workflow.
+ *
+ * A CAN is needed to unlock the id card, provide it with [WorkflowController.setCan].
+ *
+ * @param error Optional error message if the call to [WorkflowController.setCan] failed.
+ * @param reader Reader the CAN is requested for
+ */
+ fun onEnterCan(error: String?, reader: Reader)
+
+ /**
+ * Indicates that the pin change workflow is completed.
+ *
+ * @param changePinResult Result of the pin change
+ */
+ fun onChangePinCompleted(changePinResult: ChangePinResult)
+
+ /**
+ * Indicates that an error has occurred.
+ *
+ * This might be called if there was an error in the workflow.
+ *
+ * @param error Error
+ */
+ fun onWrapperError(error: WrapperError)
+
+ /**
+ * Provides information about the current workflow and state. This callback indicates if a
+ * workflow is in progress or the workflow is paused. This can occur if the AusweisApp2 needs
+ * additional data like ACCESS_RIGHTS or INSERT_CARD.
+ *
+ * @param workflowProgress Holds information about the current workflow progress.
+ */
+ fun onStatus(workflowProgress: WorkflowProgress)
+
+ /**
+ * Provides information about the supported API level of the employed AusweisApp2
+ *
+ * Response to a call to WorkflowController.getApiLevel() and WorkflowController.setApiLevel().
+ *
+ * @param error Optional error message if WorkflowController.setApiLevel() failed.
+ * @param apiLevel Contains information about the supported and employed API level.
+ */
+ fun onApiLevel(error: String?, apiLevel: ApiLevel?)
+
+ /**
+ * Provides information about the AusweisApp2 that is used in the SDK Wrapper.
+ *
+ * Response to a call to [WorkflowController.getInfo].
+ *
+ * @param versionInfo Holds information about the currently utilized AusweisApp2.
+ */
+ fun onInfo(versionInfo: VersionInfo)
+
+ /**
+ * Called if an error within the AusweisApp2 SDK occurred. Please report this as it indicates a bug.
+ *
+ * @param error Information about the error.
+ */
+ fun onInternalError(error: String)
+
+ /**
+ * Called if the sent command is not allowed within the current workflow.
+ *
+ * @param error Error message which SDK command failed.
+ */
+ fun onBadState(error: String)
+}
diff --git a/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/WorkflowController.kt b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/WorkflowController.kt
new file mode 100644
index 0000000..959e910
--- /dev/null
+++ b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/WorkflowController.kt
@@ -0,0 +1,611 @@
+/**
+ * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany
+ */
+
+package de.governikus.ausweisapp2.sdkwrapper.card.core
+
+import android.content.Context
+import android.net.Uri
+import android.nfc.Tag
+import android.nfc.tech.IsoDep
+import android.util.Log
+import de.governikus.ausweisapp2.sdkwrapper.SDKWrapper
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.getAccessRights
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.getApiLevel
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.getAuthResult
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.getCertificateDescription
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.getReaderFromReaderMember
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.getReaderFromRoot
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.getReaderList
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.getVersionInfo
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.getWorkflowProgress
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Accept
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Cancel
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Command
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.GetAccessRights
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.GetApiLevel
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.GetCertificate
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.GetInfo
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.GetReader
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.GetReaderList
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.GetStatus
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Message
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_ACCESS_RIGHTS
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_API_LEVEL
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_AUTH
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_BAD_STATE
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_CERTIFICATE
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_CHANGE_PIN
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_ENTER_CAN
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_ENTER_NEW_PIN
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_ENTER_PIN
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_ENTER_PUK
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_INFO
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_INSERT_CARD
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_INTERNAL_ERROR
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_INVALID
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_READER
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_READER_LIST
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_STATUS
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Messages.MSG_UNKNOWN_COMMAND
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.RunAuth
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.RunChangePin
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.SetAccessRights
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.SetApiLevel
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.SetCan
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.SetCard
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.SetNewPin
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.SetPin
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.SetPuk
+import de.governikus.ausweisapp2.sdkwrapper.card.core.util.workflowSimulatorToCommandSimulator
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+/**
+ * [WorkflowController] is used to control the authentication and pin change workflow
+ */
+class WorkflowController internal constructor(private val sdkConnection: SdkConnection) {
+
+ internal interface SdkConnection {
+ val isConnected: Boolean
+ fun bind(
+ context: Context,
+ onConnected: (() -> Unit)? = null,
+ onConnectionFailed: (() -> Unit)? = null,
+ onMessageReceived: ((message: Message) -> Unit)? = null
+ )
+ fun unbind()
+ fun updateNfcTag(tag: Tag): Boolean
+ fun send(command: T): Boolean
+ }
+
+ private val workflowCallbacks = ArrayList()
+
+ /**
+ * Indicates that the [WorkflowController] is ready to be used.
+ * When the [WorkflowController] is not in started state, other api calls will fail.
+ */
+ val isStarted: Boolean
+ get() = sdkConnection.isConnected
+
+ /**
+ * Initialize the [WorkflowController].
+ *
+ * Before it is possible to use the [WorkflowController] it needs to be initialized.
+ * Make sure to call this function and wait for the [WorkflowCallbacks.onStarted] callback before using it.
+ *
+ * @param context Context
+ */
+ fun start(context: Context) {
+ if (isStarted) {
+ Log.d(TAG, "WorkflowController already started")
+ return
+ }
+
+ sdkConnection.bind(
+ context,
+ onConnected = {
+ callback { onStarted() }
+ },
+ onConnectionFailed = {
+ val error = WrapperError("WorkflowController::start", "Connection failed")
+ callback { onWrapperError(error) }
+ },
+ onMessageReceived = { messageJson ->
+ handleMessage(messageJson)
+ }
+ )
+ }
+
+ /**
+ * Stop the [WorkflowController].
+ *
+ * When you no longer need the [WorkflowController] make sure to stop it to free up some
+ * resources.
+ */
+ fun stop() {
+ if (!isStarted) {
+ Log.d(TAG, "WorkflowController not started")
+ return
+ }
+
+ sdkConnection.unbind()
+ }
+
+ /**
+ * Register callbacks with controller.
+ *
+ * @param callbacks Callbacks to register.
+ */
+ fun registerCallbacks(callbacks: WorkflowCallbacks) {
+ workflowCallbacks.add(callbacks)
+ }
+
+ /**
+ * Unregister callback from controller.
+ *
+ * @param callbacks Callbacks to unregister.
+ */
+ fun unregisterCallbacks(callbacks: WorkflowCallbacks) {
+ workflowCallbacks.remove(callbacks)
+ }
+
+ /**
+ * Starts an authentication workflow.
+ *
+ * The [WorkflowController] will call [WorkflowCallbacks.onAuthenticationStarted],
+ * when the authentication is started. If there was an error starting the authentication
+ * [WorkflowCallbacks.onAuthenticationStartFailed].
+ *
+ * After calling this method, the expected minimal workflow is:
+ * [WorkflowCallbacks.onAuthenticationStarted] is called.
+ * When [WorkflowCallbacks.onAccessRights] is called, accept it via [accept].
+ * [WorkflowCallbacks.onInsertCard] is called, when the user has not yet placed the phone on the card.
+ * When [WorkflowCallbacks.onEnterPin] is called, provide the pin via [setPin].
+ * When the authentication workflow is finished [WorkflowCallbacks.onAuthenticationCompleted] is called.
+ *
+ * This command is allowed only if the SDK has no running workflow.
+ * Otherwise you will get a callback to [WorkflowCallbacks.onBadState].
+ *
+ * @param tcTokenUrl URL to the TcToken.
+ *
+ * @param developerMode Enable "Developer Mode" for test cards and disable some security
+ * checks according to BSI TR-03124-1.
+ *
+ * @param status True to enable automatic STATUS messages, which are delivered by
+ * callbacks to [WorkflowCallbacks.onStatus].
+ */
+ fun startAuthentication(tcTokenUrl: Uri, developerMode: Boolean = false, status: Boolean = true) {
+ send(RunAuth(tcTokenUrl.toString(), developerMode, status))
+ }
+
+ /**
+ * Start a pin change workflow.
+ *
+ * The [WorkflowController] will call [WorkflowCallbacks.onChangePinStarted],
+ * when the pin change is started.
+ *
+ * After calling this method, the expected minimal workflow is:
+ * [WorkflowCallbacks.onChangePinStarted] is called.
+ * [WorkflowCallbacks.onInsertCard] is called, when the user has not yet placed the card on the reader.
+ * When [WorkflowCallbacks.onEnterPin] is called, provide the pin via [setPin].
+ * When [WorkflowCallbacks.onEnterNewPin] is called, provide the new pin via [setNewPin].
+ * When the pin workflow is finished, [WorkflowCallbacks.onChangePinCompleted] is called.
+ *
+ * This command is allowed only if the SDK has no running workflow.
+ * Otherwise you will get a callback to [WorkflowCallbacks.onBadState].
+ *
+ * @param status True to enable automatic STATUS messages, which are delivered by
+ * callbacks to [WorkflowCallbacks.onStatus].
+ */
+ fun startChangePin(status: Boolean = true) {
+ send(RunChangePin(status))
+ }
+
+ /**
+ * Set optional access rights
+ *
+ * If the SDK asks for specific access rights in [WorkflowCallbacks.onAccessRights],
+ * you can modify the requested optional rights by setting a list of accepted optional rights here.
+ * When the command is successful you get a callback to [WorkflowCallbacks.onAccessRights]
+ * with the updated access rights.
+ *
+ * List of possible access rights are listed in [AccessRight]
+ *
+ * This command is allowed only if the SDK asked for a pin via [WorkflowCallbacks.onAccessRights].
+ * Otherwise you will get a callback to [WorkflowCallbacks.onBadState].
+ *
+ * @param accessRights List of enabled optional access rights. If the list is empty all optional access rights are disabled.
+ */
+ fun setAccessRights(accessRights: List) {
+ send(SetAccessRights(accessRights.map { it.rawName }))
+ }
+
+ /**
+ * Returns information about the requested access rights.
+ * This command is allowed only if the SDK Wrapper called [WorkflowCallbacks.onAccessRights] beforehand.
+ */
+ fun getAccessRights() {
+ send(GetAccessRights())
+ }
+
+ /**
+ * Set supported API level of your application.
+ *
+ * If you initially develop your application against the SDK Wrapper you should check
+ * the highest supported level with getApiLevel() and set this value with this command
+ * when you connect to the SDK Wrapper.
+ * This will set the SDK Wrapper to act with the defined level even if a newer level is available.
+ * The SDK Wrapper will call [WorkflowCallbacks.onApiLevel] as an answer.
+ *
+ * @param level Supported API level of your app.
+ */
+ fun setApiLevel(level: Int) {
+ send(SetApiLevel(level))
+ }
+
+ /**
+ * Returns information about the available and current API level.
+ *
+ * The SDK will call [WorkflowCallbacks.onApiLevel] as an answer.
+ */
+ fun getApiLevel() {
+ send(GetApiLevel())
+ }
+
+ /**
+ * Provides information about the utilized AusweisApp2.
+ *
+ * The SDK Wrapper will call [WorkflowCallbacks.onInfo] as an answer.
+ */
+ fun getInfo() {
+ send(GetInfo())
+ }
+
+ /**
+ * Dummy match for SDK Wrapper iOS method, does not actually do anything at the moment.
+ */
+ fun interrupt() {
+ }
+
+ /**
+ * Set PIN of inserted card.
+ *
+ * If the SDK calls [WorkflowCallbacks.onEnterPin] you need to call this function to unblock the card with the PIN.
+ *
+ * If your application provides an invalid PIN the SDK will call [WorkflowCallbacks.onEnterPin]
+ * again with a decreased retryCounter.
+ *
+ * If the value of retryCounter is 1 the SDK will initially call [WorkflowCallbacks.onEnterCan].
+ * Once your application provides a correct CAN the SDK will call [WorkflowCallbacks.onEnterPin]
+ * again with a retryCounter of 1.
+ * If the value of retryCounter is 0 the SDK will initially call [WorkflowCallbacks.onEnterPuk].
+ * Once your application provides a correct PUK the SDK will call [WorkflowCallbacks.onEnterPin]
+ * again with a retryCounter of 3.
+ *
+ * This command is allowed only if the SDK asked for a pin via [WorkflowCallbacks.onEnterPin].
+ * Otherwise you will get a callback to [WorkflowCallbacks.onBadState].
+ *
+ * @param pin The personal identification number (PIN) of the card. Must contain 5 (Transport PIN) or 6 digits.
+ */
+ fun setPin(pin: String?) {
+ send(SetPin(pin))
+ }
+
+ /**
+ * Set new PIN for inserted card.
+ *
+ * If the SDK calls [WorkflowCallbacks.onEnterNewPin] you need to call this function to provide a new pin.
+ *
+ * This command is allowed only if the SDK asked for a new pin via [WorkflowCallbacks.onEnterNewPin].
+ * Otherwise you will get a callback to [WorkflowCallbacks.onBadState].
+ *
+ * @param newPin The new personal identification number (PIN) of the card. Must only contain 6 digits.
+ * Must be null if the current reader has a keypad. See [Reader].
+ */
+ fun setNewPin(newPin: String?) {
+ send(SetNewPin(newPin))
+ }
+
+ /**
+ * Set PUK of inserted card.
+ *
+ * If the SDK calls [WorkflowCallbacks.onEnterPuk] you need to call this function to unblock [setPin].
+ *
+ * The workflow will automatically continue if the PUK was correct and the SDK will call [WorkflowCallbacks.onEnterPin].
+ * If the correct PUK is entered the retryCounter will be set to 3.
+ *
+ * If your application provides an invalid PUK the SDK will call [WorkflowCallbacks.onEnterPuk] again.
+ *
+ * If the SDK calls [WorkflowCallbacks.onEnterPuk] with [Card.inoperative] set true it is not possible to unblock the PIN.
+ * You will have to show a message to the user that the card is inoperative and the user should
+ * contact the authority responsible for issuing the identification card to unblock the PIN.
+ *
+ * This command is allowed only if the SDK asked for a puk via [WorkflowCallbacks.onEnterPuk].
+ * Otherwise you will get a callback to [WorkflowCallbacks.onBadState].
+ *
+ * @param puk The personal unblocking key (PUK) of the card. Must only contain 10 digits.
+ * Must be null if the current reader has a keypad. See [Reader].
+ */
+ fun setPuk(puk: String?) {
+ send(SetPuk(puk))
+ }
+
+ /**
+ * Set CAN of inserted card.
+ *
+ * If the SDK calls [WorkflowCallbacks.onEnterCan] you need to call this function to unblock the last retry of [setPin].
+ *
+ * The CAN is required to enable the last attempt of PIN input if the retryCounter is 1.
+ * The workflow continues automatically with the correct CAN and the SDK will call [WorkflowCallbacks.onEnterPin].
+ * Despite the correct CAN being entered, the retryCounter remains at 1.
+ * The CAN is also required, if the authentication terminal has an approved “CAN allowed right”.
+ * This allows the workflow to continue without an additional PIN.
+ *
+ * If your application provides an invalid CAN the SDK will call [WorkflowCallbacks.onEnterCan] again.
+ *
+ * This command is allowed only if the SDK asked for a puk via [WorkflowCallbacks.onEnterCan].
+ * Otherwise you will get a callback to [WorkflowCallbacks.onBadState].
+ *
+ * @param can The card access number (CAN) of the card. Must only contain 6 digits.
+ * Must be null if the current reader has a keypad. See [Reader].
+ */
+ fun setCan(can: String?) {
+ send(SetCan(can))
+ }
+
+ /**
+ * Insert “virtual” card.
+ *
+ * @param name Name of [Reader] with a [Card] that shall be used.
+ * @param simulator Specific data for [Simulator]. (optional) files: Content of card Filesystem.
+ */
+ fun setCard(name: String, simulator: Simulator?) {
+ send(SetCard(name, workflowSimulatorToCommandSimulator(simulator)))
+ }
+
+ /**
+ * Accept the current state.
+ *
+ * If the SDK calls [WorkflowCallbacks.onAccessRights] the user needs to accept or deny them.
+ * The workflow is paused until your application sends this command to accept the requested information.
+ * If the user does not accept the requested information your application needs to call [cancel] to abort the whole workflow.
+ *
+ * This command is allowed only if the SDK asked for access rights via [WorkflowCallbacks.onAccessRights].
+ * Otherwise you will get a callback to [WorkflowCallbacks.onBadState].
+ *
+ * Note: This accepts the requested access rights as well as the provider's certificate since it is not possible to accept
+ * one without the other.
+ */
+ fun accept() {
+ send(Accept())
+ }
+
+ /**
+ * Cancel the running workflow.
+ *
+ * If your application sends this command the SDK will cancel the workflow.
+ * You can send this command in any state of a running workflow to abort it.
+ */
+ fun cancel() {
+ send(Cancel())
+ }
+
+ /**
+ * Request the certificate of current authentication.
+ *
+ * The SDK will call [WorkflowCallbacks.onCertificate] as an answer.
+ */
+ fun getCertificate() {
+ send(GetCertificate())
+ }
+
+ /**
+ * Request information about the current workflow and state of SDK.
+ * The SDK will call [WorkflowCallbacks.onStatus] as an answer.
+ */
+ fun getStatus() {
+ send(GetStatus())
+ }
+
+ /**
+ * Returns information about the requested reader.
+ *
+ * If you explicitly want to ask for information of a known reader name you can request it with this command.
+ * The SDK Wrapper will call [WorkflowCallbacks.onReader] as an answer.
+ *
+ * @param name Name of the reader.
+ */
+ fun getReader(name: String) {
+ send(GetReader(name))
+ }
+
+ /**
+ * Returns information about all connected readers.
+ *
+ * If you explicitly want to ask for information of all connected readers you can request it with this command.
+ * The SDK Wrapper will call [WorkflowCallbacks.onReaderList] as an answer.
+ */
+ fun getReaderList() {
+ send(GetReaderList())
+ }
+
+ /**
+ * Pass a detected nfc tag to the [WorkflowController]
+ *
+ * Since only a foreground application can detect nfc tags,
+ * you need to pass them to the SDK for it to handle detected id cards.
+ *
+ * @see NfcForegroundDispatcher
+ *
+ * @param tag Detected id card. ISO-DEP (ISO 14443-4) NFC tag
+ */
+ fun onNfcTagDetected(tag: Tag) {
+ require(tag.techList.contains(IsoDep::class.java.name)) { "NFC tag isn't a ISO-DEP (ISO 14443-4) NFC tag" }
+
+ SDKWrapper.launch(Dispatchers.IO) {
+ if (isStarted) {
+ sdkConnection.updateNfcTag(tag)
+ } else {
+ callback { onWrapperError(WrapperError("WorkflowController::onNfcTagDetected: isStarted", "Not started")) }
+ }
+ }
+ }
+
+ private inline fun send(command: T) = SDKWrapper.launch(Dispatchers.IO) {
+ if (isStarted) {
+ sdkConnection.send(command)
+ } else {
+ callback { onWrapperError(WrapperError("WorkflowController::send: isStarted", "Not started")) }
+ }
+ }
+
+ private fun callback(callback: WorkflowCallbacks.() -> Unit) =
+ SDKWrapper.launch(Dispatchers.Main) {
+ workflowCallbacks.forEach {
+ callback(it)
+ }
+ }
+
+ private fun handleMessage(message: Message) {
+ when (message.msg) {
+ MSG_API_LEVEL -> {
+ callback { onApiLevel(message.error, message.getApiLevel()) }
+ }
+ MSG_INFO -> {
+ when (val info = message.getVersionInfo()) {
+ null -> {
+ callback { onWrapperError(WrapperError(message.msg, "Parsing error")) }
+ }
+ else -> {
+ callback { onInfo(info) }
+ }
+ }
+ }
+ MSG_AUTH -> {
+ if (message.error != null) {
+ callback { onAuthenticationStartFailed(message.error) }
+ }
+
+ when (val authResult = message.getAuthResult()) {
+ null -> {
+ // Everything is fine, the SDK started the authentication workflow and sent an empty AUTH message.
+ callback { onAuthenticationStarted() }
+ }
+ else -> {
+ callback { onAuthenticationCompleted(authResult) }
+ }
+ }
+ }
+ MSG_ACCESS_RIGHTS -> {
+ when (val accessRights = message.getAccessRights()) {
+ null -> {
+ callback { onWrapperError(WrapperError(message.msg, "Missing access rights")) }
+ }
+ else -> {
+ callback { onAccessRights(message.error, accessRights) }
+ }
+ }
+ }
+ MSG_BAD_STATE -> {
+ callback { onWrapperError(WrapperError(message.msg, message.error ?: "Unknown bad state")) }
+ }
+ MSG_CHANGE_PIN -> {
+ when (message.success) {
+ null -> {
+ callback { onChangePinStarted() }
+ }
+ else -> {
+ callback { onChangePinCompleted(ChangePinResult(message.success)) }
+ }
+ }
+ }
+ MSG_ENTER_PIN -> {
+ when (val reader = message.getReaderFromReaderMember()) {
+ null -> {
+ callback { onWrapperError(WrapperError(message.msg, "Missing reader")) }
+ }
+ else -> {
+ callback { onEnterPin(message.error, reader) }
+ }
+ }
+ }
+ MSG_ENTER_NEW_PIN -> {
+ when (val reader = message.getReaderFromReaderMember()) {
+ null -> {
+ callback { onWrapperError(WrapperError(message.msg, "Missing reader")) }
+ }
+ else -> {
+ callback { onEnterNewPin(message.error, reader) }
+ }
+ }
+ }
+ MSG_ENTER_PUK -> {
+ when (val reader = message.getReaderFromReaderMember()) {
+ null -> {
+ callback { onWrapperError(WrapperError(message.msg, "Missing reader")) }
+ }
+ else -> {
+ callback { onEnterPuk(message.error, reader) }
+ }
+ }
+ }
+ MSG_ENTER_CAN -> {
+ when (val reader = message.getReaderFromReaderMember()) {
+ null -> {
+ callback { onWrapperError(WrapperError(message.msg, "Missing reader")) }
+ }
+ else -> {
+ callback { onEnterCan(message.error, reader) }
+ }
+ }
+ }
+ MSG_INSERT_CARD -> {
+ callback { onInsertCard(null) }
+ }
+ MSG_CERTIFICATE -> {
+ when (val certificateDescription = message.getCertificateDescription()) {
+ null -> {
+ callback { onWrapperError(WrapperError(message.msg, "Missing certificateDescription")) }
+ }
+ else -> {
+ callback { onCertificate(certificateDescription) }
+ }
+ }
+ }
+ MSG_READER -> {
+ callback { onReader(message.getReaderFromRoot()) }
+ }
+ MSG_READER_LIST -> {
+ callback { onReaderList(message.getReaderList()) }
+ }
+ MSG_INVALID -> {
+ val error = message.error ?: "Unknown SDK Wrapper error"
+ callback { onWrapperError(WrapperError(message.msg, error)) }
+ }
+ MSG_UNKNOWN_COMMAND -> {
+ val error = message.error ?: "Unknown SDK Wrapper error"
+ callback { onWrapperError(WrapperError(message.msg, error)) }
+ }
+ MSG_INTERNAL_ERROR -> {
+ val errorMessage = message.error ?: "Unknown internal error"
+ callback { onInternalError(errorMessage) }
+ }
+ MSG_STATUS -> {
+ callback { onStatus(message.getWorkflowProgress()) }
+ }
+ else -> {
+ Log.d(TAG, "Received unknown message ${message.msg}")
+ }
+ }
+ }
+
+ companion object {
+ private val TAG = WorkflowController::class.java.simpleName
+
+ const val PIN_LENGTH = 6
+ const val TRANSPORT_PIN_LENGTH = 5
+ const val PUK_LENGTH = 10
+ const val CAN_LENGTH = 6
+ }
+}
diff --git a/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/WorkflowData.kt b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/WorkflowData.kt
new file mode 100644
index 0000000..a34d7fa
--- /dev/null
+++ b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/WorkflowData.kt
@@ -0,0 +1,293 @@
+/**
+ * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany
+ */
+
+package de.governikus.ausweisapp2.sdkwrapper.card.core
+
+import android.net.Uri
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import java.util.*
+
+/**
+ * Detailed description of the certificate.
+ *
+ * @property issuerName Name of the certificate issuer.
+ * @property issuerUrl URL of the certificate issuer.
+ * @property subjectName Name of the certificate subject.
+ * @property subjectUrl URL of the certificate subject.
+ * @property termsOfUsage Raw certificate information about the terms of usage.
+ * @property purpose Parsed purpose of the terms of usage.
+ * @property validity Certificate validity
+ */
+@Parcelize
+data class CertificateDescription(
+ val issuerName: String,
+ val issuerUrl: Uri?,
+ val purpose: String,
+ val subjectName: String,
+ val subjectUrl: Uri?,
+ val termsOfUsage: String,
+ val validity: CertificateValidity
+) : Parcelable
+
+/**
+ * Validity dates of the certificate in UTC.
+ *
+ * @property effectiveDate Certificate is valid since this date.
+ * @property expirationDate Certificate is invalid after this date.
+ */
+@Parcelize
+data class CertificateValidity(
+ val effectiveDate: Date,
+ val expirationDate: Date
+) : Parcelable
+
+/**
+ * Access rights requested by the provider.
+ *
+ * @property requiredRights These rights are mandatory and cannot be disabled.
+ * @property optionalRights These rights are optional and can be enabled or disabled
+ * @property effectiveRights Indicates the enabled access rights of optional and required.
+ * @property transactionInfo Optional transaction information.
+ * @property auxiliaryData Optional auxiliary data of the provider.
+ */
+@Parcelize
+data class AccessRights(
+ val requiredRights: List,
+ val optionalRights: List,
+ val effectiveRights: List,
+ val transactionInfo: String?,
+ val auxiliaryData: AuxiliaryData?
+) : Parcelable
+
+/**
+ * Auxiliary data of the provider.
+ *
+ * @property ageVerificationDate Optional required date of birth for AgeVerification.
+ * @property requiredAge Optional required age for AgeVerification. It is calculated by the SDK on the basis of ageVerificationDate and current date.
+ * @property validityDate Optional validity date.
+ * @property communityId Optional id of community.
+ */
+@Parcelize
+data class AuxiliaryData(
+ val ageVerificationDate: Date?,
+ val requiredAge: Int?,
+ val validityDate: Date?,
+ val communityId: String?
+) : Parcelable
+
+/**
+ * Provides information about inserted card.
+ *
+ * @property inoperative True if PUK is inoperative and cannot unblock PIN, otherwise false. This can be recognized if user enters a correct PUK only. It is not possible to read this data before a user tries to unblock the PIN.
+ * @property deactivated True if eID functionality is deactivated, otherwise false.
+ * @property pinRetryCounter Count of possible retries for the PIN. If you enter a PIN it will be decreased if PIN was incorrect.
+ */
+@Parcelize
+data class Card(
+ val deactivated: Boolean,
+ val inoperative: Boolean,
+ val pinRetryCounter: Int
+) : Parcelable
+
+/**
+ * Optional definition of files for the Simulator reader.
+ *
+ * @property files List of Filesystem definitions. See [SimulatorFile].
+ */
+@Parcelize
+data class Simulator(
+ val files: List
+) : Parcelable
+
+/**
+ * Filesystem for Simulator reader
+ *
+ * The content of the filesystem can be provided as a JSON array of objects. The fileId and shortFileId
+ * are specified on the corresponding technical guideline of the BSI and ISO. The content is an
+ * ASN.1 structure in DER encoding.
+ *
+ * All fields are hex encoded.
+ *
+ * @property fileId The fileId and shortFileId are specified on the corresponding technical guideline of the BSI and ISO.
+ * @property shortFileId The fileId just shorter.
+ * @property content The content is an ASN.1 structure in DER encoding.
+ */
+@Parcelize
+data class SimulatorFile(
+ val fileId: String,
+ val shortFileId: String,
+ val content: String
+) : Parcelable
+
+/**
+ * Provides information about the API Level of the underlying AusweisApp2.
+ *
+ * @property available List of all available API levels of the employed AusweisApp2.
+ * @property current The currently set API level.
+ */
+@Parcelize
+data class ApiLevel(
+ val available: List?,
+ val current: Int
+) : Parcelable
+
+/**
+ * Final result of an authentication.
+ *
+ * @property url Refresh url or an optional communication error address.
+ * @property result Contains information about the result of the authentication. See [AuthResultData].
+ */
+@Parcelize
+data class AuthResult(
+ val url: Uri?,
+ val result: AuthResultData?
+) : Parcelable
+
+/**
+ * Final result of a PIN change.
+ *
+ * @property success True if a the PIN has been successfully set, else false
+ */
+
+@Parcelize
+data class ChangePinResult(
+ val success: Boolean
+) : Parcelable
+
+/**
+ * Information about an authentication
+ *
+ * @property major Major error code.
+ * @property minor Minor error code.
+ * @property language Language of description and message. Language “en” is supported only at the moment.
+ * @property description Description of the error message.
+ * @property message The error message.
+ * @property reason Unique error code.
+ */
+@Parcelize
+data class AuthResultData(
+ val major: String,
+ val minor: String?,
+ val language: String?,
+ val description: String?,
+ val message: String?,
+ val reason: String?
+) : Parcelable
+
+/**
+ * Provides information about a reader.
+ *
+ * @property name Identifier of card reader.
+ * @property insertable Indicates whether a card can be inserted via setCard().
+ * @property attached Indicates whether a card reader is connected or disconnected.
+ * @property keypad Indicates whether a card reader has a keypad.
+ * @property card Provides information about inserted card, otherwise null.
+ */
+@Parcelize
+data class Reader(
+ val name: String,
+ val insertable: Boolean,
+ val attached: Boolean,
+ val keypad: Boolean,
+ val card: Card?
+) : Parcelable
+
+/**
+ * Provides information about the underlying AusweisApp2.
+ *
+ * @property name Application name.
+ * @property implementationTitle Title of implementation.
+ * @property implementationVendor Vendor of implementation.
+ * @property implementationVersion Version of implementation.
+ * @property specificationTitle Títle of specification.
+ * @property specificationVendor Vendor of specification.
+ * @property specificationVersion Version of specification.
+ *
+ */
+@Parcelize
+data class VersionInfo(
+ val name: String,
+ val implementationTitle: String,
+ val implementationVendor: String,
+ val implementationVersion: String,
+ val specificationTitle: String,
+ val specificationVendor: String,
+ val specificationVersion: String
+) : Parcelable
+
+/**
+ * Provides information about an error.
+ *
+ * @property msg Message type in which the error occurred.
+ * @property error Error message.
+ */
+@Parcelize
+data class WrapperError(
+ val msg: String,
+ val error: String
+) : Parcelable
+
+/**
+ * Provides information about the workflow status
+ *
+ * @param workflow Type of the current workflow. If there is no workflow in progress this will
+ * be null.
+ * @param progress Percentage of workflow progress. If there is no workflow in progress this
+ * will be null.
+ * @param state Name of the current state if paused. If there is no workflow in progress or the
+ * workflow is not paused this will be null.
+ */
+@Parcelize
+data class WorkflowProgress(
+ val workflow: WorkflowProgressType?,
+ val progress: Int?,
+ val state: String?
+) : Parcelable
+
+/**
+ * List of all available access rights a provider might request.
+ */
+enum class AccessRight(val rawName: String) {
+ ADDRESS("Address"),
+ BIRTH_NAME("BirthName"),
+ FAMILY_NAME("FamilyName"),
+ GIVEN_NAMES("GivenNames"),
+ PLACE_OF_BIRTH("PlaceOfBirth"),
+ DATE_OF_BIRTH("DateOfBirth"),
+ DOCTORAL_DEGREE("DoctoralDegree"),
+ ARTISTIC_NAME("ArtisticName"),
+ PSEUDONYM("Pseudonym"),
+ VALID_UNTIL("ValidUntil"),
+ NATIONALITY("Nationality"),
+ ISSUING_COUNTRY("IssuingCountry"),
+ DOCUMENT_TYPE("DocumentType"),
+ RESIDENCE_PERMIT_I("ResidencePermitI"),
+ RESIDENCE_PERMIT_II("ResidencePermitII"),
+ COMMUNITY_ID("CommunityID"),
+ ADDRESS_VERIFICATION("AddressVerification"),
+ AGE_VERIFICATION("AgeVerification"),
+ WRITE_ADDRESS("WriteAddress"),
+ WRITE_COMMUNITY_ID("WriteCommunityID"),
+ WRITE_RESIDENCE_PERMIT_I("WriteResidencePermitI"),
+ WRITE_RESIDENCE_PERMIT_II("WriteResidencePermitII"),
+ CAN_ALLOWED("CanAllowed"),
+ PIN_MANAGEMENT("PinManagement");
+
+ companion object {
+ fun fromRawName(rawName: String) = values().firstOrNull { it.rawName == rawName }
+ }
+}
+
+/**
+ * List of all types of WorkflowProgress
+ */
+enum class WorkflowProgressType(val rawName: String) {
+ AUTHENTICATION("AUTH"),
+ CHANGE_PIN("CHANGE_PIN");
+
+ companion object {
+ fun fromRawName(rawName: String?) = values().firstOrNull { it.rawName == rawName }
+ }
+}
diff --git a/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/AA2SdkConnection.kt b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/AA2SdkConnection.kt
new file mode 100644
index 0000000..11a11fa
--- /dev/null
+++ b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/AA2SdkConnection.kt
@@ -0,0 +1,133 @@
+/**
+ * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany
+ */
+
+package de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.nfc.Tag
+import android.os.IBinder
+import android.os.RemoteException
+import android.util.Log
+import com.google.gson.Gson
+import com.google.gson.JsonSyntaxException
+import com.governikus.ausweisapp2.IAusweisApp2Sdk
+import com.governikus.ausweisapp2.IAusweisApp2SdkCallback
+import de.governikus.ausweisapp2.sdkwrapper.card.core.WorkflowController
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Command
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Message
+
+internal class AA2SdkConnection : WorkflowController.SdkConnection {
+
+ private var context: Context? = null
+ private var sdkConnection: ServiceConnection? = null
+ private var sdk: IAusweisApp2Sdk? = null
+ private var sdkSessionId: String? = null
+
+ private val gson = Gson()
+
+ override val isConnected: Boolean
+ get() = sdk != null
+
+ override fun bind(
+ context: Context,
+ onConnected: (() -> Unit)?,
+ onConnectionFailed: (() -> Unit)?,
+ onMessageReceived: ((message: Message) -> Unit)?
+ ) {
+ this.context = context.applicationContext
+ val serviceIntent = Intent("com.governikus.ausweisapp2.START_SERVICE")
+ serviceIntent.setPackage(context.applicationContext.packageName)
+
+ val sdkCallback = object : IAusweisApp2SdkCallback.Stub() {
+ override fun sessionIdGenerated(sessionId: String, isSecureSessionId: Boolean) {
+ this@AA2SdkConnection.sdkSessionId = sessionId
+ }
+
+ override fun receive(messageJson: String) {
+ Log.d(TAG, "Received message: $messageJson")
+
+ try {
+ val message = gson.fromJson(messageJson, Message::class.java)
+ onMessageReceived?.invoke(message)
+ } catch (e: JsonSyntaxException) {
+ Log.e(TAG, "Could not parse json message", e)
+ }
+ }
+
+ override fun sdkDisconnected() {
+ sdkSessionId = null
+ }
+ }
+
+ sdkConnection = object : ServiceConnection {
+ override fun onServiceConnected(className: ComponentName, service: IBinder) {
+ try {
+ sdk = IAusweisApp2Sdk.Stub.asInterface(service)
+ sdk?.connectSdk(sdkCallback)
+ onConnected?.invoke()
+ } catch (e: RemoteException) {
+ Log.d(TAG, "Could not connect to ausweisapp2 sdk", e)
+ onConnectionFailed?.invoke()
+ }
+ }
+
+ override fun onServiceDisconnected(className: ComponentName) {
+ Log.d(TAG, "Service disconnected")
+ sdk = null
+ sdkSessionId = null
+ }
+ }.apply {
+ try {
+ context.bindService(serviceIntent, this, Context.BIND_AUTO_CREATE)
+ } catch (e: SecurityException) {
+ Log.d(TAG, "Could not bind service", e)
+ onConnectionFailed?.invoke()
+ }
+ }
+ }
+
+ override fun unbind() {
+ sdkConnection?.apply {
+ context?.unbindService(this)
+ }
+ context = null
+ sdkConnection = null
+ sdk = null
+ sdkSessionId = null
+ }
+
+ override fun send(command: T): Boolean {
+ val sdk = sdk ?: return false
+ val sessionId = sdkSessionId ?: return false
+
+ return try {
+ val messageJson = gson.toJson(command)
+ Log.d(TAG, "Send message: $messageJson")
+ sdk.send(sessionId, messageJson)
+ } catch (e: RemoteException) {
+ Log.d(TAG, "Could not send command", e)
+ false
+ }
+ }
+
+ override fun updateNfcTag(tag: Tag): Boolean {
+ val sdk = sdk ?: return false
+ val sessionId = sdkSessionId ?: return false
+
+ return try {
+ sdk.updateNfcTag(sessionId, tag)
+ true
+ } catch (e: RemoteException) {
+ Log.d(TAG, "Could not update nfc tag", e)
+ false
+ }
+ }
+
+ companion object {
+ private val TAG = ServiceConnection::class.java.simpleName
+ }
+}
diff --git a/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/MessageExtension.kt b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/MessageExtension.kt
new file mode 100644
index 0000000..b4c1898
--- /dev/null
+++ b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/MessageExtension.kt
@@ -0,0 +1,191 @@
+/**
+ * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany
+ */
+
+package de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2
+
+import android.net.Uri
+import de.governikus.ausweisapp2.sdkwrapper.card.core.AccessRight
+import de.governikus.ausweisapp2.sdkwrapper.card.core.AccessRights
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ApiLevel
+import de.governikus.ausweisapp2.sdkwrapper.card.core.AuthResult
+import de.governikus.ausweisapp2.sdkwrapper.card.core.AuthResultData
+import de.governikus.ausweisapp2.sdkwrapper.card.core.AuxiliaryData
+import de.governikus.ausweisapp2.sdkwrapper.card.core.Card
+import de.governikus.ausweisapp2.sdkwrapper.card.core.CertificateDescription
+import de.governikus.ausweisapp2.sdkwrapper.card.core.CertificateValidity
+import de.governikus.ausweisapp2.sdkwrapper.card.core.Reader
+import de.governikus.ausweisapp2.sdkwrapper.card.core.VersionInfo
+import de.governikus.ausweisapp2.sdkwrapper.card.core.WorkflowProgress
+import de.governikus.ausweisapp2.sdkwrapper.card.core.WorkflowProgressType
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Message
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+private val dateFormat: SimpleDateFormat
+ get() = SimpleDateFormat("yyyy-MM-dd", Locale.GERMANY)
+
+internal fun Message.getCertificateDescription(): CertificateDescription? {
+ val description = description ?: return null
+ val validity = validity ?: return null
+ val issueDate = dateFormat.parse(validity.effectiveDate) ?: return null
+ val expirationDate = dateFormat.parse(validity.expirationDate) ?: return null
+
+ val issuerUrl = if (description.issuerUrl.isNotBlank()) Uri.parse(description.issuerUrl) else null
+ val subjectUrl = if (description.subjectUrl.isNotBlank()) Uri.parse(description.subjectUrl) else null
+
+ return CertificateDescription(
+ description.issuerName,
+ issuerUrl,
+ description.purpose,
+ description.subjectName,
+ subjectUrl,
+ description.termsOfUsage,
+ CertificateValidity(
+ issueDate,
+ expirationDate
+ )
+ )
+}
+
+internal fun Message.getCard(): Card? {
+ val card = card ?: reader?.card ?: return null
+
+ return Card(
+ card.deactivated,
+ card.inoperative,
+ card.retryCounter
+ )
+}
+
+internal fun Message.getApiLevel(): ApiLevel? {
+ val current = current ?: return null
+
+ return ApiLevel(
+ available,
+ current
+ )
+}
+
+internal fun Message.getReaderFromRoot(): Reader? {
+ val name = name ?: return null
+ val insertable = insertable ?: return null
+ val attached = attached ?: return null
+ val keypad = keypad ?: return null
+
+ return Reader(
+ name,
+ insertable,
+ attached,
+ keypad,
+ getCard()
+ )
+}
+
+internal fun Message.getReaderFromReaderMember(): Reader? {
+ val reader = reader ?: return null
+ val name = reader.name
+ val insertable = reader.insertable
+ val keypad = reader.keypad
+ val attached = reader.attached
+
+ return Reader(
+ name,
+ insertable,
+ attached,
+ keypad,
+ getCard()
+ )
+}
+
+internal fun Message.getReaderList(): List? {
+ val readers = readers ?: return null
+
+ return readers.map {
+ val card = if (it.card == null) {
+ null
+ } else {
+ Card(
+ it.card.deactivated,
+ it.card.inoperative,
+ it.card.retryCounter
+ )
+ }
+ Reader(
+ it.name,
+ it.insertable,
+ it.attached,
+ it.keypad,
+ card
+ )
+ }
+}
+
+internal fun Message.getVersionInfo(): VersionInfo? {
+ val info = versionInfo ?: return null
+
+ return VersionInfo(
+ info.name,
+ info.implementationTitle,
+ info.implementationVendor,
+ info.implementationVersion,
+ info.specificationTitle,
+ info.specificationVendor,
+ info.specificationVersion
+ )
+}
+
+internal fun Message.getAccessRights(): AccessRights? {
+ val chat = chat ?: return null
+
+ val auxiliaryData = aux?.run {
+ AuxiliaryData(
+ if (ageVerificationDate != null) dateFormat.parse(ageVerificationDate) else null,
+ requiredAge?.toInt(),
+ if (validityDate != null) dateFormat.parse(validityDate) else null,
+ communityId
+ )
+ }
+
+ val requiredRights = chat.required.mapNotNull { AccessRight.fromRawName(it) }
+ val optionalRights = chat.optional.mapNotNull { AccessRight.fromRawName(it) }
+ val effectiveRights = chat.effective.mapNotNull { AccessRight.fromRawName(it) }
+
+ return AccessRights(
+ requiredRights,
+ optionalRights,
+ effectiveRights,
+ transactionInfo,
+ auxiliaryData
+ )
+}
+
+internal fun Message.getAuthResult(): AuthResult? {
+ val uri = if (url != null) Uri.parse(url) else null
+ val resultData = getAuthResultData()
+
+ if (resultData != null || uri != null) {
+ return AuthResult(uri, resultData)
+ }
+
+ return null
+}
+
+internal fun Message.getAuthResultData(): AuthResultData? {
+ val result = result ?: return null
+ if (result.major == null) return null
+
+ return AuthResultData(
+ result.major,
+ result.minor,
+ result.language,
+ result.description,
+ result.message,
+ result.reason
+ )
+}
+
+internal fun Message.getWorkflowProgress(): WorkflowProgress {
+ val workflowType = WorkflowProgressType.fromRawName(workflow)
+ return WorkflowProgress(workflowType, progress, state)
+}
diff --git a/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/protocol/CommandData.kt b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/protocol/CommandData.kt
new file mode 100644
index 0000000..6dc17aa
--- /dev/null
+++ b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/protocol/CommandData.kt
@@ -0,0 +1,11 @@
+package de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol
+
+internal data class Simulator(
+ val files: List
+)
+
+internal data class SimulatorFile(
+ val fileId: String,
+ val shortFileId: String,
+ val content: String
+)
diff --git a/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/protocol/Commands.kt b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/protocol/Commands.kt
new file mode 100644
index 0000000..344863a
--- /dev/null
+++ b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/protocol/Commands.kt
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany
+ */
+
+package de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol
+
+internal interface Command {
+ val cmd: String
+}
+
+internal class Accept : Command {
+ override val cmd = "ACCEPT"
+}
+
+internal class Cancel : Command {
+ override val cmd = "CANCEL"
+}
+
+internal class SetApiLevel(val level: Int) : Command {
+ override val cmd = "SET_API_LEVEL"
+}
+
+internal class GetApiLevel : Command {
+ override val cmd = "GET_API_LEVEL"
+}
+
+internal class GetCertificate : Command {
+ override val cmd = "GET_CERTIFICATE"
+}
+
+internal class RunAuth(val tcTokenURL: String, val developerMode: Boolean, val status: Boolean) : Command {
+ override val cmd = "RUN_AUTH"
+}
+
+internal class RunChangePin(val status: Boolean) : Command {
+ override val cmd = "RUN_CHANGE_PIN"
+}
+
+internal class GetAccessRights : Command {
+ override val cmd = "GET_ACCESS_RIGHTS"
+}
+
+internal class SetAccessRights(val chat: List) : Command {
+ override val cmd = "SET_ACCESS_RIGHTS"
+}
+
+internal class SetCan(val value: String?) : Command {
+ override val cmd = "SET_CAN"
+}
+
+internal class SetCard(val name: String, val simulator: Simulator?) : Command {
+ override val cmd = "SET_CARD"
+}
+
+internal class SetPin(val value: String?) : Command {
+ override val cmd = "SET_PIN"
+}
+
+internal class SetNewPin(val value: String?) : Command {
+ override val cmd = "SET_NEW_PIN"
+}
+
+internal class SetPuk(val value: String?) : Command {
+ override val cmd = "SET_PUK"
+}
+
+internal class GetStatus : Command {
+ override val cmd = "GET_STATUS"
+}
+
+internal class GetInfo : Command {
+ override val cmd = "GET_INFO"
+}
+
+internal class GetReader(val name: String) : Command {
+ override val cmd = "GET_READER"
+}
+
+internal class GetReaderList : Command {
+ override val cmd = "GET_READER_LIST"
+}
diff --git a/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/protocol/Messages.kt b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/protocol/Messages.kt
new file mode 100644
index 0000000..9bdf024
--- /dev/null
+++ b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/ausweisapp2/protocol/Messages.kt
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany
+ */
+
+package de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol
+
+import com.google.gson.annotations.SerializedName
+
+internal object Messages {
+ const val MSG_ACCESS_RIGHTS = "ACCESS_RIGHTS"
+ const val MSG_API_LEVEL = "API_LEVEL"
+ const val MSG_AUTH = "AUTH"
+ const val MSG_BAD_STATE = "BAD_STATE"
+ const val MSG_CERTIFICATE = "CERTIFICATE"
+ const val MSG_CHANGE_PIN = "CHANGE_PIN"
+ const val MSG_ENTER_CAN = "ENTER_CAN"
+ const val MSG_ENTER_NEW_PIN = "ENTER_NEW_PIN"
+ const val MSG_INFO = "INFO"
+ const val MSG_ENTER_PIN = "ENTER_PIN"
+ const val MSG_ENTER_PUK = "ENTER_PUK"
+ const val MSG_INSERT_CARD = "INSERT_CARD"
+ const val MSG_INTERNAL_ERROR = "INTERNAL_ERROR"
+ const val MSG_INVALID = "INVALID"
+ const val MSG_READER = "READER"
+ const val MSG_READER_LIST = "READER_LIST"
+ const val MSG_STATUS = "STATUS"
+ const val MSG_UNKNOWN_COMMAND = "UNKNOWN_COMMAND"
+}
+
+internal data class Message(
+ val attached: Boolean?,
+ val aux: Aux?,
+ val available: List?,
+ val card: Card?,
+ val chat: Chat?,
+ val current: Int?,
+ val description: Description?,
+ val error: String?,
+ val insertable: Boolean?,
+ val keypad: Boolean?,
+ val msg: String?,
+ val name: String?,
+ val progress: Int?,
+ val reader: Reader?,
+ val readers: List?,
+ val reason: String?,
+ val result: Result?,
+ val state: String?,
+ val success: Boolean?,
+ val transactionInfo: String?,
+ val url: String?,
+ val validity: Validity?,
+ @SerializedName("VersionInfo")
+ val versionInfo: VersionInfo?,
+ val workflow: String?
+)
+
+internal data class Description(
+ val issuerName: String,
+ val issuerUrl: String,
+ val purpose: String,
+ val subjectName: String,
+ val subjectUrl: String,
+ val termsOfUsage: String
+)
+
+internal data class Validity(
+ val effectiveDate: String,
+ val expirationDate: String
+)
+
+internal data class Chat(
+ val effective: List,
+ val optional: List,
+ val required: List
+)
+
+internal data class Aux(
+ val ageVerificationDate: String?,
+ val requiredAge: String?,
+ val validityDate: String?,
+ val communityId: String?
+)
+
+internal data class Card(
+ val inoperative: Boolean,
+ val deactivated: Boolean,
+ val retryCounter: Int
+)
+
+internal data class Reader(
+ val name: String,
+ val insertable: Boolean,
+ val attached: Boolean,
+ val keypad: Boolean,
+ val card: Card?
+)
+
+internal data class Result(
+ val major: String?,
+ val minor: String?,
+ val language: String?,
+ val description: String?,
+ val message: String?,
+ val reason: String?
+)
+
+internal data class VersionInfo(
+ @SerializedName("Name")
+ val name: String,
+ @SerializedName("Implementation-Title")
+ val implementationTitle: String,
+ @SerializedName("Implementation-Vendor")
+ val implementationVendor: String,
+ @SerializedName("Implementation-Version")
+ val implementationVersion: String,
+ @SerializedName("Specification-Title")
+ val specificationTitle: String,
+ @SerializedName("Specification-Vendor")
+ val specificationVendor: String,
+ @SerializedName("Specification-Version")
+ val specificationVersion: String
+)
diff --git a/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/util/StringUtil.kt b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/util/StringUtil.kt
new file mode 100644
index 0000000..63dff41
--- /dev/null
+++ b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/util/StringUtil.kt
@@ -0,0 +1,9 @@
+/**
+ * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany
+ */
+
+package de.governikus.ausweisapp2.sdkwrapper.card.core.util
+
+internal val NUMBER_REGEX = Regex("[0-9]+")
+
+fun String.isNumber() = matches(NUMBER_REGEX)
diff --git a/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/util/WorkflowDataToCommandDataConversion.kt b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/util/WorkflowDataToCommandDataConversion.kt
new file mode 100644
index 0000000..a33ee93
--- /dev/null
+++ b/sdkwrapper/src/main/java/de/governikus/ausweisapp2/sdkwrapper/card/core/util/WorkflowDataToCommandDataConversion.kt
@@ -0,0 +1,22 @@
+package de.governikus.ausweisapp2.sdkwrapper.card.core.util
+
+internal typealias WorkflowSimulator = de.governikus.ausweisapp2.sdkwrapper.card.core.Simulator
+internal typealias WorkflowSimulatorFile = de.governikus.ausweisapp2.sdkwrapper.card.core.SimulatorFile
+
+internal typealias CommandSimulator = de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Simulator
+internal typealias CommandSimulatorFile = de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.SimulatorFile
+
+internal fun workflowSimulatorToCommandSimulator(simulator: WorkflowSimulator?): CommandSimulator? {
+ simulator ?: return null
+ return CommandSimulator(
+ simulator.files.map { workflowSimulatorFileToCommandSimulatorFile(it) }
+ )
+}
+
+internal fun workflowSimulatorFileToCommandSimulatorFile(file: WorkflowSimulatorFile): CommandSimulatorFile {
+ return CommandSimulatorFile(
+ file.fileId,
+ file.shortFileId,
+ file.content
+ )
+}
diff --git a/sdkwrapper/src/test/java/de/governikus/idloneos/card/core/WorkflowControllerTest.kt b/sdkwrapper/src/test/java/de/governikus/idloneos/card/core/WorkflowControllerTest.kt
new file mode 100644
index 0000000..83186f3
--- /dev/null
+++ b/sdkwrapper/src/test/java/de/governikus/idloneos/card/core/WorkflowControllerTest.kt
@@ -0,0 +1,350 @@
+/**
+ * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany
+ */
+
+package de.governikus.ausweisapp2.sdkwrapper.card.core
+
+import android.content.Context
+import android.net.Uri
+import android.nfc.Tag
+import com.google.gson.Gson
+import de.governikus.ausweisapp2.sdkwrapper.SDKWrapper
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Accept
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Command
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.Message
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.RunAuth
+import de.governikus.ausweisapp2.sdkwrapper.card.core.ausweisapp2.protocol.SetPin
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.newSingleThreadContext
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.annotation.Config
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+@RunWith(RobolectricTestRunner::class)
+@Config(manifest = Config.NONE, sdk = [24])
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+class WorkflowControllerTest {
+
+ private var workflowController: WorkflowController? = null
+ private var connection: MockSdkConnection? = null
+
+ @OptIn(DelicateCoroutinesApi::class)
+ private val mainThreadSurrogate = newSingleThreadContext("UI thread")
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(mainThreadSurrogate)
+ connection = MockSdkConnection()
+ workflowController = WorkflowController(connection!!)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ mainThreadSurrogate.close()
+ workflowController = null
+ connection = null
+ }
+
+ @Test
+ fun testOnStarted() = runTest(dispatchTimeoutMs = 1000) {
+ assertNotNull(workflowController)
+ val workflowController = workflowController!!
+
+ val completed = suspendCoroutine {
+ workflowController.registerCallbacks(
+ object : TestWorkflowCallbacks() {
+ override fun onStarted() {
+ it.resume(true)
+ }
+ }
+ )
+
+ workflowController.start(RuntimeEnvironment.getApplication())
+ }
+ assert(completed)
+
+ assert(workflowController.isStarted)
+ }
+
+ @Test
+ fun testErrorNotStarted() = runTest(dispatchTimeoutMs = 1000) {
+ assertNotNull(workflowController)
+ val workflowController = workflowController!!
+
+ assertFalse(workflowController.isStarted)
+
+ val completed = suspendCoroutine {
+ workflowController.registerCallbacks(
+ object : TestWorkflowCallbacks() {
+ override fun onWrapperError(error: WrapperError) {
+ it.resume(true)
+ }
+ }
+ )
+
+ workflowController.startAuthentication(Uri.parse("https://test.test"))
+ }
+ assert(completed)
+ }
+
+ @Test
+ fun testAuthenticationStarted() = runTest(dispatchTimeoutMs = 1000) {
+ assertNotNull(workflowController)
+ assertNotNull(connection)
+ val workflowController = workflowController!!
+ val connection = connection!!
+
+ val testUrl = Uri.parse("https://test.test")
+
+ connection.onCommandSend = {
+ val command = it as? RunAuth
+
+ assertNotNull(command)
+ assertEquals(testUrl.toString(), command?.tcTokenURL)
+
+ connection.receive("{\"msg\":\"AUTH\"}")
+ }
+
+ val completed = suspendCoroutine {
+ workflowController.registerCallbacks(
+ object : TestWorkflowCallbacks() {
+ override fun onAuthenticationStarted() {
+ it.resume(true)
+ }
+ }
+ )
+
+ workflowController.start(RuntimeEnvironment.getApplication())
+ workflowController.startAuthentication(testUrl)
+ }
+
+ assert(completed)
+ }
+
+ @Test
+ fun testFullAuthentication() = runTest(dispatchTimeoutMs = 1000) {
+ assertNotNull(workflowController)
+ assertNotNull(connection)
+ val workflowController = workflowController!!
+ val connection = connection!!
+
+ val tcTokenUrl = Uri.parse("https://test.test")
+ val testPin = "123456"
+
+ connection.onCommandSend = { command ->
+ when (command) {
+ is RunAuth -> {
+ assertNotNull(command)
+ assertEquals(tcTokenUrl.toString(), command.tcTokenURL)
+
+ connection.receive("{\"msg\":\"AUTH\"}")
+ }
+ is Accept -> {
+ connection.receive("{\"msg\":\"INSERT_CARD\"}")
+ }
+ is SetPin -> {
+ assertEquals(testPin, command.value)
+
+ connection.receive(
+ "{" +
+ " \"msg\": \"AUTH\"," +
+ " \"result\":" +
+ " {" +
+ " \"major\": \"http://www.bsi.bund.de/ecard/api/1.1/resultmajor#ok\"" +
+ " }," +
+ " \"url\": \"https://test.governikus-eid.de/gov_autent/async?refID=_123456789\"" +
+ "}"
+ )
+ }
+ else -> {
+ }
+ }
+ }
+
+ val completed = suspendCoroutine {
+ workflowController.registerCallbacks(
+ object : TestWorkflowCallbacks() {
+ override fun onAuthenticationStarted() {
+ connection.receive(
+ "{" +
+ " \"msg\": \"ACCESS_RIGHTS\"," +
+ " \"aux\":" +
+ " {" +
+ " \"ageVerificationDate\": \"1999-07-20\"," +
+ " \"requiredAge\": \"18\"," +
+ " \"validityDate\": \"2017-07-20\"," +
+ " \"communityId\": \"02760400110000\"" +
+ " }," +
+ " \"chat\":" +
+ " {" +
+ " \"effective\": [\"Address\", \"FamilyName\", \"GivenNames\", \"AgeVerification\"]," +
+ " \"optional\": [\"GivenNames\", \"AgeVerification\"]," +
+ " \"required\": [\"Address\", \"FamilyName\"]" +
+ " }," +
+ " \"transactionInfo\": \"this is an example\"" +
+ "}"
+ )
+ }
+
+ override fun onAccessRights(error: String?, accessRights: AccessRights?) {
+ workflowController.accept()
+ }
+
+ override fun onInsertCard(error: String?) {
+ connection.receive(
+ "{" +
+ " \"msg\": \"READER\"," +
+ " \"name\": \"NFC\"," +
+ " \"attached\": true," +
+ " \"insertable\": true," +
+ " \"keypad\": false," +
+ " \"card\":" +
+ " {" +
+ " \"inoperative\": false," +
+ " \"deactivated\": false," +
+ " \"retryCounter\": 3" +
+ " }" +
+ "}"
+ )
+ }
+
+ override fun onReader(reader: Reader?) {
+ val card = reader?.card
+ assertNotNull(card)
+ assertEquals(3, card?.pinRetryCounter)
+ assertEquals(false, card?.inoperative)
+ assertEquals(false, card?.deactivated)
+
+ connection.receive(
+ "{" +
+ " \"msg\": \"ENTER_PIN\"," +
+ " \"reader\":" +
+ " {" +
+ " \"name\": \"NFC\"," +
+ " \"attached\": true," +
+ " \"insertable\": true," +
+ " \"keypad\": false," +
+ " \"card\":" +
+ " {" +
+ " \"inoperative\": false," +
+ " \"deactivated\": false," +
+ " \"retryCounter\": 3" +
+ " }" +
+ " }" +
+ "}"
+ )
+ }
+
+ override fun onEnterPin(error: String?, reader: Reader) {
+ val card = reader.card
+ if (card != null) {
+ assertEquals(3, card.pinRetryCounter)
+ assertEquals(false, card.inoperative)
+ assertEquals(false, card.deactivated)
+
+ workflowController.setPin(testPin)
+ } else {
+ assert(false)
+ }
+ }
+
+ override fun onAuthenticationCompleted(authResult: AuthResult) {
+ assertNotNull(authResult.result)
+ assertNotNull(authResult.url)
+ assertEquals("http://www.bsi.bund.de/ecard/api/1.1/resultmajor#ok", authResult.result?.major)
+
+ it.resume(true)
+ }
+ }
+ )
+
+ workflowController.start(RuntimeEnvironment.getApplication())
+ workflowController.startAuthentication(tcTokenUrl)
+ }
+
+ assert(completed)
+ }
+}
+
+internal class MockSdkConnection : WorkflowController.SdkConnection {
+
+ private var onMessageReceived: ((message: Message) -> Unit)? = null
+
+ var onCommandSend: ((command: Command) -> Unit)? = null
+
+ override var isConnected: Boolean = false
+ private set
+
+ override fun bind(
+ context: Context,
+ onConnected: (() -> Unit)?,
+ onConnectionFailed: (() -> Unit)?,
+ onMessageReceived: ((message: Message) -> Unit)?
+ ) {
+ this.onMessageReceived = onMessageReceived
+ isConnected = true
+ onConnected?.invoke()
+ }
+
+ override fun unbind() {
+ onMessageReceived = null
+ isConnected = false
+ }
+
+ override fun updateNfcTag(tag: Tag): Boolean {
+ return false
+ }
+
+ override fun send(command: T): Boolean {
+ SDKWrapper.launch {
+ onCommandSend?.invoke(command)
+ }
+ return isConnected
+ }
+
+ fun receive(messageJson: String) {
+ SDKWrapper.launch {
+ val message = Gson().fromJson(messageJson, Message::class.java)
+ onMessageReceived?.invoke(message)
+ }
+ }
+}
+
+internal open class TestWorkflowCallbacks : WorkflowCallbacks {
+ override fun onStarted() {}
+ override fun onAuthenticationStarted() {}
+ override fun onAuthenticationStartFailed(error: String) {}
+ override fun onChangePinStarted() {}
+ override fun onAccessRights(error: String?, accessRights: AccessRights?) {}
+ override fun onCertificate(certificateDescription: CertificateDescription) {}
+ override fun onInsertCard(error: String?) {}
+ override fun onReader(reader: Reader?) {}
+ override fun onReaderList(readers: List?) {}
+ override fun onEnterPin(error: String?, reader: Reader) {}
+ override fun onEnterNewPin(error: String?, reader: Reader) {}
+ override fun onEnterPuk(error: String?, reader: Reader) {}
+ override fun onEnterCan(error: String?, reader: Reader) {}
+ override fun onAuthenticationCompleted(authResult: AuthResult) {}
+ override fun onChangePinCompleted(changePinResult: ChangePinResult) {}
+ override fun onWrapperError(error: WrapperError) {}
+ override fun onStatus(workflowProgress: WorkflowProgress) {}
+ override fun onApiLevel(error: String?, apiLevel: ApiLevel?) {}
+ override fun onInfo(versionInfo: VersionInfo) {}
+ override fun onBadState(error: String) {}
+ override fun onInternalError(error: String) {}
+}
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..c2d2624
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':tester', ':sdkwrapper'
diff --git a/third_party_licenses/ApacheLicense.md b/third_party_licenses/ApacheLicense.md
new file mode 100644
index 0000000..7e310bd
--- /dev/null
+++ b/third_party_licenses/ApacheLicense.md
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2000-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
+
+ 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.
\ No newline at end of file
diff --git a/third_party_licenses/EUPL.md b/third_party_licenses/EUPL.md
new file mode 100644
index 0000000..b76bd59
--- /dev/null
+++ b/third_party_licenses/EUPL.md
@@ -0,0 +1,282 @@
+ OPEN-SOURCE-LIZENZ FÜR DIE EUROPÄISCHE UNION v. 1.2
+ EUPL © Europäische Union 2007, 2016
+
+
+Diese Open-Source-Lizenz für die Europäische Union (»EUPL«) gilt für Werke (im Sinne der nachfolgenden Begriffsbestimmung),
+die unter EUPL-Bedingungen zur Verfügung gestellt werden. Das Werk darf nur in der durch diese Lizenz
+gestatteten Form genutzt werden (insoweit eine solche Nutzung dem Urheber vorbehalten ist).
+
+Das Werk wird unter den Bedingungen dieser Lizenz zur Verfügung gestellt, wenn der Lizenzgeber (im Sinne der
+nachfolgenden Begriffsbestimmung) den folgenden Hinweis unmittelbar hinter dem Urheberrechtshinweis dieses Werks
+anbringt:
+
+Lizenziert unter der EUPL
+
+oder in einer anderen Form zum Ausdruck bringt, dass er es unter der EUPL lizenzieren möchte.
+
+
+ 1. Begriffsbestimmungen
+
+Für diese Lizenz gelten folgende Begriffsbestimmungen:
+
+- »Lizenz«: diese Lizenz.
+
+- »Originalwerk«: das Werk oder die Software, die vom Lizenzgeber unter dieser Lizenz verbreitet oder zugänglich
+ gemacht wird, und zwar als Quellcode und gegebenenfalls auch als ausführbarer Code.
+
+- »Bearbeitungen«: die Werke oder Software, die der Lizenznehmer auf der Grundlage des Originalwerks oder seiner
+ Bearbeitungen schaffen kann. In dieser Lizenz wird nicht festgelegt, wie umfangreich die Änderung oder wie stark die
+ Abhängigkeit vom Originalwerk für eine Einstufung als Bearbeitung sein muss; dies bestimmt sich nach dem
+ Urheberrecht, das in dem unter Artikel 15 aufgeführten Land anwendbar ist.
+
+- »Werk«: das Originalwerk oder seine Bearbeitungen.
+
+- »Quellcode«: diejenige Form des Werkes, die zur Auffassung durch den Menschen bestimmt ist und die am besten
+ geeignet ist, um vom Menschen verstanden und verändert zu werden.
+
+- »Ausführbarer Code«: die - üblicherweise - kompilierte Form des Werks, die von einem Computer als Programm
+ ausgeführt werden soll.
+
+- »Lizenzgeber«: die natürliche oder juristische Person, die das Werk unter der Lizenz verbreitet oder zugänglich
+ macht.
+
+- »Bearbeiter«: jede natürliche oder juristische Person, die das Werk unter der Lizenz verändert oder auf andere Weise
+ zur Schaffung einer Bearbeitung beiträgt.
+
+- »Lizenznehmer« (»Sie«): jede natürliche oder juristische Person, die das Werk unter den Lizenzbedingungen nutzt.
+
+- »Verbreitung« oder »Zugänglichmachung«: alle Formen von Verkauf, Überlassung, Verleih, Vermietung, Verbreitung,
+ Weitergabe, Übermittlung oder anderweitiger Online- oder Offline-Bereitstellung von Vervielfältigungen des Werks
+ oder Zugänglichmachung seiner wesentlichen Funktionen für dritte natürliche oder juristische Personen.
+
+
+ 2. Umfang der Lizenzrechte
+
+Der Lizenzgeber erteilt Ihnen hiermit für die Gültigkeitsdauer der am Originalwerk bestehenden Urheberrechte eine
+weltweite, unentgeltliche, nicht ausschließliche, unterlizenzierbare Lizenz, die Sie berechtigt:
+
+- das Werk uneingeschränkt zu nutzen,
+
+- das Werk zu vervielfältigen,
+
+- das Werk zu verändern und Bearbeitungen auf der Grundlage des Werks zu schaffen,
+
+- das Werk öffentlich zugänglich zu machen, was das Recht einschließt, das Werk oder Vervielfältigungsstücke davon
+ öffentlich bereitzustellen oder wahrnehmbar zu machen oder das Werk, soweit möglich, öffentlich aufzuführen,
+
+- das Werk oder Vervielfältigungen davon zu verbreiten,
+
+- das Werk oder Vervielfältigungen davon zu vermieten oder zu verleihen,
+
+- das Werk oder Vervielfältigungen davon weiter zu lizenzieren.
+
+Für die Wahrnehmung dieser Rechte können beliebige, derzeit bekannte oder künftige Medien, Träger und Formate
+verwendet werden, soweit das geltende Recht dem nicht entgegensteht.
+
+Für die Länder, in denen Urheberpersönlichkeitsrechte an dem Werk bestehen, verzichtet der Lizenzgeber im gesetzlich
+zulässigen Umfang auf seine Urheberpersönlichkeitsrechte, um die Lizenzierung der oben aufgeführten
+Verwertungsrechte wirksam durchführen zu können.
+
+Der Lizenzgeber erteilt dem Lizenznehmer ein nicht ausschließliches, unentgeltliches Nutzungsrecht an seinen Patenten,
+sofern dies zur Ausübung der durch die Lizenz erteilten Nutzungsrechte am Werk notwendig ist.
+
+
+ 3. Zugänglichmachung des Quellcodes
+
+
+Der Lizenzgeber kann das Werk entweder als Quellcode oder als ausführbaren Code zur Verfügung stellen. Stellt er es als
+ausführbaren Code zur Verfügung, so stellt er darüber hinaus eine maschinenlesbare Kopie des Quellcodes für jedes von
+ihm verbreitete Vervielfältigungsstück des Werks zur Verfügung, oder er verweist in einem Vermerk im Anschluss an den
+dem Werk beigefügten Urheberrechtshinweis auf einen Speicherort, an dem problemlos und unentgeltlich auf den
+Quellcode zugegriffen werden kann, solange der Lizenzgeber das Werk verbreitet oder zugänglich macht.
+
+
+ 4. Einschränkungen des Urheberrechts
+
+
+Es ist nicht Zweck dieser Lizenz, Ausnahmen oder Schranken der ausschließlichen Rechte des Urhebers am Werk, die
+dem Lizenznehmer zugutekommen, einzuschränken. Auch die Erschöpfung dieser Rechte bleibt von dieser Lizenz
+unberührt.
+
+
+ 5. Pflichten des Lizenznehmers
+
+
+Die Einräumung der oben genannten Rechte ist an mehrere Beschränkungen und Pflichten für den Lizenznehmer
+gebunden:
+
+Urheberrechtshinweis, Lizenztext, Nennung des Bearbeiters: Der Lizenznehmer muss alle Urheberrechts-, Patent-
+oder Markenrechtshinweise und alle Hinweise auf die Lizenz und den Haftungsausschluss unverändert lassen. Jedem von
+ihm verbreiteten oder zugänglich gemachten Vervielfältigungsstück des Werks muss der Lizenznehmer diese Hinweise
+sowie diese Lizenz beifügen. Der Lizenznehmer muss auf jedem abgeleiteten Werk deutlich darauf hinweisen, dass das
+Werk geändert wurde, und das Datum der Bearbeitung angeben.
+
+»Copyleft«-Klausel: Der Lizenznehmer darf Vervielfältigungen des Originalwerks oder Bearbeitungen nur unter den
+Bedingungen dieser EUPL oder einer neueren Version dieser Lizenz verbreiten oder zugänglich machen, außer wenn das
+Originalwerk ausdrücklich nur unter dieser Lizenzversion - z. B. mit der Angabe »Nur EUPL V. 1.2« - verbreitet
+werden darf. Der Lizenznehmer (der zum Lizenzgeber wird) darf für das Werk oder die Bearbeitung keine zusätzlichen
+Bedingungen anbieten oder vorschreiben, die die Bedingungen dieser Lizenz verändern oder einschränken.
+
+Kompatibilitäts-Klausel: Wenn der Lizenznehmer Bearbeitungen, die auf dem Werk und einem anderen Werk, das
+unter einer kompatiblen Lizenz lizenziert wurde, basieren, oder die Kopien dieser Bearbeitungen verbreitet oder
+zugänglich macht, kann dies unter den Bedingungen dieser kompatiblen Lizenz erfolgen. Unter »kompatibler Lizenz« ist
+eine im Anhang dieser Lizenz angeführte Lizenz zu verstehen. Sollten die Verpflichtungen des Lizenznehmers aus der
+kompatiblen Lizenz mit denjenigen aus der vorliegenden Lizenz (EUPL) in Konflikt stehen, werden die Verpflichtungen
+aus der kompatiblen Lizenz Vorrang haben.
+
+Bereitstellung des Quellcodes: Wenn der Lizenznehmer Vervielfältigungsstücke des Werks verbreitet oder zugänglich
+macht, muss er eine maschinenlesbare Fassung des Quellcodes mitliefern oder einen Speicherort angeben, über den
+problemlos und unentgeltlich so lange auf diesen Quellcode zugegriffen werden kann, wie der Lizenznehmer das Werk
+verbreitet oder zugänglich macht.
+
+Rechtsschutz: Diese Lizenz erlaubt nicht die Benutzung von Kennzeichen, Marken oder geschützten Namensrechten des
+Lizenzgebers, soweit dies nicht für die angemessene und übliche Beschreibung der Herkunft des Werks und der
+inhaltlichen Wiedergabe des Urheberrechtshinweises erforderlich ist.
+
+
+ 6. Urheber und Bearbeiter
+
+Der ursprüngliche Lizenzgeber gewährleistet, dass er das Urheberrecht am Originalwerk innehat oder dieses an ihn
+lizenziert wurde und dass er befugt ist, diese Lizenz zu erteilen.
+
+Jeder Bearbeiter gewährleistet, dass er das Urheberrecht an den von ihm vorgenommenen Änderungen des Werks besitzt
+und befugt ist, diese Lizenz zu erteilen.
+
+Jedes Mal, wenn Sie die Lizenz annehmen, erteilen Ihnen der ursprüngliche Lizenzgeber und alle folgenden Bearbeiter
+eine Befugnis zur Nutzung ihrer Beiträge zum Werk unter den Bedingungen dieser Lizenz.
+
+
+ 7. Gewährleistungsausschluss
+
+Die Arbeit an diesem Werk wird laufend fortgeführt; es wird durch unzählige Bearbeiter ständig verbessert. Das Werk ist
+nicht vollendet und kann daher Fehler (»bugs«) enthalten, die dieser Art der Entwicklung inhärent sind.
+
+Aus den genannten Gründen wird das Werk unter dieser Lizenz »so, wie es ist« ohne jegliche Gewährleistung zur
+Verfügung gestellt. Dies gilt unter anderem - aber nicht ausschließlich - für Marktreife, Verwendbarkeit für einen
+bestimmten Zweck, Mängelfreiheit, Richtigkeit sowie Nichtverletzung von anderen Immaterialgüterrechten als dem
+Urheberrecht (vgl. dazu Artikel 6 dieser Lizenz).
+
+Dieser Gewährleistungsausschluss ist wesentlicher Bestandteil der Lizenz und Bedingung für die Einräumung von
+Rechten an dem Werk.
+
+
+ 8. Haftungsausschluss/Haftungsbeschränkung
+
+Außer in Fällen von Vorsatz oder der Verursachung von Personenschäden haftet der Lizenzgeber nicht für direkte oder
+indirekte, materielle oder immaterielle Schäden irgendwelcher Art, die aus der Lizenz oder der Benutzung des Werks
+folgen; dies gilt unter anderem, aber nicht ausschließlich, für Firmenwertverluste, Produktionsausfall, Computerausfall
+oder Computerfehler, Datenverlust oder wirtschaftliche Schäden, und zwar auch dann, wenn der Lizenzgeber auf die
+Möglichkeit solcher Schäden hingewiesen wurde. Unabhängig davon haftet der Lizenzgeber im Rahmen der gesetzlichen
+Produkthaftung, soweit die entsprechenden Regelungen auf das Werk anwendbar sind.
+
+
+ 9. Zusatzvereinbarungen
+
+Wenn Sie das Werk verbreiten, können Sie Zusatzvereinbarungen schließen, in denen Verpflichtungen oder
+Dienstleistungen festgelegt werden, die mit dieser Lizenz vereinbar sind. Sie dürfen Verpflichtungen indessen nur in
+Ihrem eigenen Namen und auf Ihre eigene Verantwortung eingehen, nicht jedoch im Namen des ursprünglichen
+Lizenzgebers oder eines anderen Bearbeiters, und nur, wenn Sie sich gegenüber allen Bearbeitern verpflichten, sie zu
+entschädigen, zu verteidigen und von der Haftung freizustellen, falls aufgrund der von Ihnen eingegangenen
+Gewährleistungsverpflichtung oder Haftungsübernahme Forderungen gegen sie geltend gemacht werden oder eine
+Haftungsverpflichtung entsteht.
+
+
+ 10. Annahme der Lizenz
+
+Sie können den Bestimmungen dieser Lizenz zustimmen, indem Sie das Symbol »Lizenz annehmen« unter dem Fenster
+mit dem Lizenztext anklicken oder indem Sie Ihre Zustimmung auf vergleichbare Weise in einer nach anwendbarem
+Recht zulässigen Form geben. Das Anklicken des Symbols gilt als Anzeichen Ihrer eindeutigen und unwiderruflichen
+Annahme der Lizenz und der darin enthaltenen Klauseln und Bedingungen.
+
+In gleicher Weise gilt als Zeichen der eindeutigen und unwiderruflichen Zustimmung die Ausübung eines Rechtes, das in
+Artikel 2 dieser Lizenz angeführt ist, wie das Erstellen einer Bearbeitung oder die Verbreitung oder Zugänglichmachung
+des Werks oder dessen Vervielfältigungen.
+
+
+ 11. Informationspflichten
+
+Wenn Sie das Werk verbreiten oder zugänglich machen (beispielsweise, indem Sie es zum Herunterladen von einer
+Website anbieten), müssen Sie über den Vertriebskanal oder das benutzte Verbreitungsmedium der Öffentlichkeit
+zumindest jene Informationen bereitstellen, die nach dem anwendbaren Recht bezüglich der Lizenzgeber, der Lizenz und
+ihrer Zugänglichkeit, des Abschlusses des Lizenzvertrags sowie darüber, wie die Lizenz durch den Lizenznehmer
+gespeichert und vervielfältigt werden kann, erforderlich sind.
+
+
+ 12. Beendigung der Lizenz
+
+Die Lizenz und die damit eingeräumten Rechte erlöschen automatisch, wenn der Lizenznehmer gegen die Lizenzbedingungen
+verstößt.
+
+Ein solches Erlöschen der Lizenz führt nicht zum Erlöschen der Lizenzen von Personen, denen das Werk vom
+Lizenznehmer unter dieser Lizenz zur Verfügung gestellt worden ist, solange diese Personen die Lizenzbedingungen
+erfüllen.
+
+ 13. Sonstiges
+
+Unbeschadet des Artikels 9 stellt die Lizenz die vollständige Vereinbarung der Parteien über das Werk dar.
+
+Sind einzelne Bestimmungen der Lizenz nach geltendem Recht nichtig oder unwirksam, so berührt dies nicht die
+Wirksamkeit oder Durchsetzbarkeit der Lizenz an sich. Solche Bestimmungen werden vielmehr so ausgelegt oder
+modifiziert, dass sie wirksam und durchsetzbar sind.
+
+Die Europäische Kommission kann weitere Sprachfassungen oder neue Versionen dieser Lizenz oder aktualisierte
+Fassungen des Anhangs veröffentlichen, soweit dies notwendig und angemessen ist, ohne den Umfang der Lizenzrechte
+zu verringern. Neue Versionen werden mit einer eindeutigen Versionsnummer veröffentlicht.
+
+Alle von der Europäischen Kommission anerkannten Sprachfassungen dieser Lizenz sind gleichwertig. Die Parteien
+können sich auf die Sprachfassung ihrer Wahl berufen.
+
+
+ 14. Gerichtsstand
+
+Unbeschadet besonderer Vereinbarungen zwischen den Parteien gilt Folgendes:
+
+- Für alle Streitigkeiten über die Auslegung dieser Lizenz zwischen den Organen, Einrichtungen und sonstigen Stellen
+ der Europäischen Union als Lizenzgeber und einem Lizenznehmer ist der Gerichtshof der Europäischen Union
+ gemäß Artikel 272 des Vertrags über die Arbeitsweise der Europäischen Union zuständig;
+
+- Gerichtsstand für Streitigkeiten zwischen anderen Parteien über die Auslegung dieser Lizenz ist allein der Ort, an
+ dem der Lizenzgeber seinen Wohnsitz oder den wirtschaftlichen Mittelpunkt seiner Tätigkeit hat.
+
+
+ 15. Anwendbares Recht
+
+Unbeschadet besonderer Vereinbarungen zwischen den Parteien gilt Folgendes:
+
+- Diese Lizenz unterliegt dem Recht des Mitgliedstaats der Europäischen Union, in dem der Lizenzgeber seinen Sitz,
+ Wohnsitz oder eingetragenen Sitz hat;
+
+- diese Lizenz unterliegt dem belgischen Recht, wenn der Lizenzgeber keinen Sitz, Wohnsitz oder eingetragenen Sitz in
+ einem Mitgliedstaat der Europäischen Union hat.
+
+
+ Anlage
+
+»Kompatible Lizenzen« nach Artikel 5 der EUPL sind:
+
+ - GNU General Public License (GPL) v. 2, v. 3
+
+ - GNU Affero General Public License (AGPL) v. 3
+
+ - Open Software License (OSL) v. 2.1, v. 3.0
+
+ - Eclipse Public License (EPL) v. 1.0
+
+ - CeCILL v. 2.0, v. 2.1
+
+ - Mozilla Public Licence (MPL) v. 2
+
+ - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
+
+ - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) für andere Werke als Software
+
+ - European Union Public Licence (EUPL) v. 1.1, v. 1.2
+
+ - Québec Free and Open-Source Licence - Reciprocity (LiLiQ-R) oder Strong Reciprocity (LiLiQ-R+)
+
+- Die Europäische Kommission kann diesen Anhang aktualisieren, um neuere Fassungen der obigen Lizenzen
+ aufzunehmen, ohne hierfür eine neue Fassung der EUPL auszuarbeiten, solange diese Lizenzen die in Artikel 2
+ gewährten Rechte gewährleisten und den erfassten Quellcode vor ausschließlicher Aneignung schützen.
+
+- Alle sonstigen Änderungen oder Ergänzungen dieses Anhangs bedürfen der Ausarbeitung einer neuen Version der
+ EUPL.
diff --git a/third_party_licenses/LICENSES.md b/third_party_licenses/LICENSES.md
new file mode 100644
index 0000000..d64c691
--- /dev/null
+++ b/third_party_licenses/LICENSES.md
@@ -0,0 +1,10 @@
+Apache License - ApacheLicense.md
+=================================
+
+com.google.code.gson:gson:2.10.1
+
+
+OPEN-SOURCE-LIZENZ FÜR DIE EUROPÄISCHE UNION - EUPL.md
+======================================================
+
+com.governikus:ausweisapp:1.26.3