diff --git a/libpretixnfc-android/build.gradle b/libpretixnfc-android/build.gradle new file mode 100644 index 0000000..c4af2d3 --- /dev/null +++ b/libpretixnfc-android/build.gradle @@ -0,0 +1,59 @@ +// NOTE: update versions in settings.gradle! +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.kotlin.kapt' +} + +android { + namespace 'eu.pretix.libpretixnfc.android' + + compileSdk = 36 + + defaultConfig { + minSdk = 21 + targetSdk = 36 + multiDexEnabled = true + + vectorDrawables { + useSupportLibrary = true + } + } + kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } + buildFeatures { + buildConfig = true + } +} + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +dependencies { + def kotlin_version = "1.9.23" // update in settings.gradle too + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.10.0' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" + + implementation(files('libs/acssmc-1.1.5.jar')) + + implementation(project(':libpretixnfc')) { + transitive = true + } + implementation(project(':libpretixsync')) { + transitive = false + } + implementation 'io.sentry:sentry-android:8.18.0' + implementation 'com.madgag.spongycastle:prov:1.58.0.0' +} \ No newline at end of file diff --git a/libpretixnfc-android/gradlew b/libpretixnfc-android/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/libpretixnfc-android/gradlew @@ -0,0 +1,249 @@ +#!/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/HEAD/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 + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + 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 + + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +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/libpretixnfc-android/gradlew.bat b/libpretixnfc-android/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/libpretixnfc-android/gradlew.bat @@ -0,0 +1,92 @@ +@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=. +@rem This is normally unused +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. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +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/libpretixnfc-android/libs/acssmc-1.1.5.jar b/libpretixnfc-android/libs/acssmc-1.1.5.jar new file mode 100644 index 0000000..c817966 Binary files /dev/null and b/libpretixnfc-android/libs/acssmc-1.1.5.jar differ diff --git a/libpretixnfc-android/settings.gradle b/libpretixnfc-android/settings.gradle new file mode 100644 index 0000000..8c22ba8 --- /dev/null +++ b/libpretixnfc-android/settings.gradle @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + maven { + url 'https://plugins.gradle.org/m2/' + } + } +} + +// workaround: plugin definition with version here, so a standalone build is possible +// but building it inside the multi module project doesn't exit with "unknown version already on classpath" error +plugins { + id 'com.android.library' version '8.4.2' apply false + id 'org.jetbrains.kotlin.android' version '1.9.23' apply false +} + +rootProject.name = 'eu.pretix.libpretixnfc-android' + diff --git a/libpretixnfc-android/src/main/AndroidManifest.xml b/libpretixnfc-android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b63df76 --- /dev/null +++ b/libpretixnfc-android/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/Utils.kt b/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/Utils.kt new file mode 100644 index 0000000..cf4d524 --- /dev/null +++ b/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/Utils.kt @@ -0,0 +1,30 @@ +package eu.pretix.libpretixnfc.android + +import io.sentry.Sentry +import kotlin.system.exitProcess +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +val crashLogger: (CoroutineContext, Throwable) -> Unit = { _, throwable: Throwable -> + throwable.printStackTrace() + if (BuildConfig.DEBUG) { + exitProcess(1) + } else { + Sentry.captureException(throwable) + } +} + +fun CoroutineScope.launchWithSentry( + dispatcher: CoroutineDispatcher = Dispatchers.IO, + exceptionHandler: ((CoroutineContext, Throwable) -> Unit) = crashLogger, + task: suspend () -> Unit +): Job { + return launch(dispatcher + CoroutineExceptionHandler(exceptionHandler)) { + task() + } +} \ No newline at end of file diff --git a/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/hardware/AcsNfcHandler.kt b/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/hardware/AcsNfcHandler.kt new file mode 100644 index 0000000..cf715c7 --- /dev/null +++ b/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/hardware/AcsNfcHandler.kt @@ -0,0 +1,251 @@ +package eu.pretix.libpretixnfc.android.hardware + +import Mf0aesKeySet +import android.app.Activity +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import android.util.Log +import com.acs.smartcard.Reader +import com.acs.smartcard.ReaderException +import eu.pretix.libpretixnfc.android.BuildConfig +import eu.pretix.libpretixnfc.communication.AbstractNfcA +import eu.pretix.libpretixnfc.communication.ChipReadError +import eu.pretix.libpretixnfc.communication.NfcChipReadError +import eu.pretix.libpretixnfc.communication.NfcIOError +import eu.pretix.libpretixsync.db.ReusableMediaType +import eu.pretix.libpretixnfc.android.hardware.acs.AcsReaderService +import eu.pretix.libpretixnfc.android.launchWithSentry +import io.sentry.Sentry +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.withContext +import java.io.IOException + + +private var lastTagId: String = "" +private var lastTagTime: Long = 0 + +class AcsNfcHandler( + private val ctx: Activity, + private val keySets: List, + private val useRandomIdForNewTags: Boolean, + private val mode: NfcHandlerMode = NfcHandlerMode.DEFAULT +) : NfcHandler, (Reader, Int) -> Unit { + private var mediaTypes: List? = null + private var running = false + private var readerService: AcsReaderService? = null + private var chipReadListener: NfcHandler.OnChipReadListener? = null + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + val supportedTypes = listOf(ReusableMediaType.NFC_UID, ReusableMediaType.NFC_MF0AES) + + companion object { + val TAG = "AcsNfcHandler" + } + + private val readerConnection = object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + val binder = service as AcsReaderService.LocalBinder + readerService = binder.getService() + readerService!!.cardHandler = this@AcsNfcHandler + } + + override fun onServiceDisconnected(arg0: ComponentName) { + readerService = null + } + } + + override fun start(mediaTypes: List) { + if (!supportedTypes.any { mediaTypes.contains(it) }) { + return + } + + this.mediaTypes = mediaTypes + Log.i(TAG, "start (${mediaTypes.joinToString(", ")}) @$ctx") + + running = true + + Intent(ctx, AcsReaderService::class.java).also { intent -> + ctx.bindService(intent, readerConnection, Context.BIND_AUTO_CREATE) + } + } + + override fun getMediaTypes(): List? { + return mediaTypes + } + + override fun setOnChipReadListener(listener: NfcHandler.OnChipReadListener?) { + chipReadListener = listener + } + + override fun stop() { + running = false + readerService?.cardHandler = null + scope.cancel() + Log.i(TAG, "stop @$ctx") + } + + override fun isRunning(): Boolean { + return running + } + + private fun readUid(reader: Reader, slotNum: Int): String { + val command = + byteArrayOf(0xFF.toByte(), 0xCA.toByte(), 0x00, 0x00, 0x00) // Read UID + val responseBuffer = ByteArray(300) + val responseLength = reader!!.transmit( + slotNum, + command, + command.size, + responseBuffer, + responseBuffer.size + ) + val response = responseBuffer.copyOfRange(0, responseLength - 2) + val hexId = response.joinToString("") { "%02x".format(it).uppercase() } + return hexId + } + + fun beep(reader: Reader) { + val duration = 10 // * 10ms + // Set buzzer to only buzz on cart insertion, not removal + val command = + byteArrayOf(0xE0.toByte(), 0x00, 0x00, 0x28, 0x01, duration.toByte()) + val responseBuffer = ByteArray(300) + if (BuildConfig.DEBUG) { + Log.i(TAG, "beep") + } + reader.control( + 0, + Reader.IOCTL_CCID_ESCAPE, + command, + command.size, + responseBuffer, + responseBuffer.size + ) + } + + override fun invoke(reader: Reader, slotNum: Int) { + val nfca = AcsNfcA(reader, slotNum) + nfca.connect() + + val hexId = readUid(reader, slotNum) + if (hexId == lastTagId && lastTagTime > System.currentTimeMillis() - 2000) { + // Debounce duplicate reads + Log.i(TAG, "debounced @$ctx") + return + } + + try { + if (mediaTypes?.contains(ReusableMediaType.NFC_UID) == true) { + Log.i(TAG, "found tag with id $hexId") + + lastTagId = hexId + lastTagTime = System.currentTimeMillis() + + ctx.runOnUiThread { + chipReadListener?.chipReadSuccessfully(hexId, ReusableMediaType.NFC_UID) + } + beep(reader) + } else if (mediaTypes?.contains(ReusableMediaType.NFC_MF0AES) == true) { + scope.launchWithSentry { + val identifier = try { + val nfca = AcsNfcA(reader, slotNum) + processMf0aes(keySets, mode, useRandomIdForNewTags, nfca) + } catch (e: NfcChipReadError) { + withContext(Dispatchers.Main) { + chipReadListener?.chipReadError(e.errorType, hexId) + } + beep(reader) + return@launchWithSentry + } catch (e: NfcIOError) { + e.printStackTrace() + withContext(Dispatchers.Main) { + chipReadListener?.chipReadError(ChipReadError.IO_ERROR, hexId) + } + beep(reader) + return@launchWithSentry + } catch (e: IOException) { + e.printStackTrace() + withContext(Dispatchers.Main) { + chipReadListener?.chipReadError(ChipReadError.IO_ERROR, hexId) + } + beep(reader) + return@launchWithSentry + } catch (e: Exception) { + e.printStackTrace() + Sentry.captureException(e) + withContext(Dispatchers.Main) { + chipReadListener?.chipReadError( + ChipReadError.UNKNOWN_ERROR, + hexId + ) + } + beep(reader) + return@launchWithSentry + } + withContext(Dispatchers.Main) { + if (mode == NfcHandlerMode.DEFAULT) { + lastTagId = hexId + lastTagTime = System.currentTimeMillis() + } + + chipReadListener?.chipReadSuccessfully( + identifier, + ReusableMediaType.NFC_MF0AES + ) + } + beep(reader) + } + } + + + } catch (e: ReaderException) { + e.printStackTrace() + } + } + + class AcsNfcA(val reader: Reader, val slotNum: Int) : AbstractNfcA { + var connected = false + + override fun connect() { + if (!connected) { + if (BuildConfig.DEBUG) { + Log.i("NfcTest", "reader.power()") + } + reader.power(slotNum, Reader.CARD_WARM_RESET) + if (BuildConfig.DEBUG) { + Log.i("NfcTest", "reader.setProtocol()") + } + reader.setProtocol(slotNum, Reader.PROTOCOL_T0 or Reader.PROTOCOL_T1) + connected = true + } + } + + @OptIn(ExperimentalStdlibApi::class) + override fun transceive(data: ByteArray): ByteArray { + check(data.size <= 255) + val command = byteArrayOf(0xFF.toByte(), 0x00, 0x00, 0x00, data.size.toByte()) + data + val responseBuffer = ByteArray(300) + if (BuildConfig.DEBUG) { + Log.i("NfcTest", "reader.transmit(${command.toHexString()})") + } + val responseLength = reader.transmit( + slotNum, + command, + command.size, + responseBuffer, + responseBuffer.size + ) + val response = responseBuffer.copyOfRange(0, responseLength - 2) + return response + } + + override fun close() { + } + } +} diff --git a/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/hardware/AndroidNativeNfcHandler.kt b/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/hardware/AndroidNativeNfcHandler.kt new file mode 100644 index 0000000..ca8269f --- /dev/null +++ b/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/hardware/AndroidNativeNfcHandler.kt @@ -0,0 +1,187 @@ +package eu.pretix.libpretixnfc.android.hardware + +import Mf0aesKeySet +import android.app.Activity +import android.content.Context +import android.nfc.NfcAdapter +import android.nfc.NfcAdapter.FLAG_READER_NFC_A +import android.nfc.NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK +import android.nfc.Tag +import android.nfc.tech.NfcA +import android.os.Bundle +import android.os.Vibrator +import android.util.Log +import eu.pretix.libpretixnfc.android.BuildConfig +import eu.pretix.libpretixnfc.android.launchWithSentry +import eu.pretix.libpretixnfc.communication.AbstractNfcA +import eu.pretix.libpretixnfc.communication.ChipReadError +import eu.pretix.libpretixnfc.communication.NfcChipReadError +import eu.pretix.libpretixnfc.communication.NfcIOError +import eu.pretix.libpretixnfc.toHexString +import eu.pretix.libpretixsync.db.ReusableMediaType +import io.sentry.Sentry +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.withContext +import java.io.IOException + + +private var lastTagId: String = "" +private var lastTagTime: Long = 0 + + +enum class NfcHandlerMode { + DEFAULT, + ENCODE +} + + +class AndroidNativeNfcHandler( + private val ctx: Activity, + private val keySets: List, + private val useRandomIdForNewTags: Boolean, + private val mode: NfcHandlerMode = NfcHandlerMode.DEFAULT +) : NfcHandler, NfcAdapter.ReaderCallback { + private var chipReadListener: NfcHandler.OnChipReadListener? = null + private var nfcAdapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(ctx) + private var mediaTypes: List? = null + private val buzzer = + ctx.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? + private var running = false + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + val supportedTypes = listOf(ReusableMediaType.NFC_UID, ReusableMediaType.NFC_MF0AES) + + companion object { + val TAG = "AndroidNativeNfcHandler" + } + + override fun start(mediaTypes: List) { + if (!supportedTypes.any { mediaTypes.contains(it) }) { + return + } + + this.mediaTypes = mediaTypes + Log.i(TAG, "start (${mediaTypes.joinToString(", ")}) @$ctx") + if (nfcAdapter == null) { + throw NfcUnsupported() + } + if (!nfcAdapter!!.isEnabled) { + throw NfcDisabled() + } + + nfcAdapter!!.enableReaderMode( + ctx, + this, + FLAG_READER_SKIP_NDEF_CHECK or FLAG_READER_NFC_A, + Bundle().apply { + putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 1200) + } + ) + running = true + } + + override fun getMediaTypes(): List? { + return mediaTypes + } + + override fun stop() { + nfcAdapter?.disableReaderMode(ctx) + running = false + scope.cancel() + Log.i(TAG, "stop @$ctx") + } + + override fun isRunning(): Boolean { + return running + } + + override fun setOnChipReadListener(listener: NfcHandler.OnChipReadListener?) { + chipReadListener = listener + } + + override fun onTagDiscovered(tag: Tag) { + Log.i(TAG, "onTagDiscovered @$ctx") + if (tag.hexId() == lastTagId && lastTagTime > System.currentTimeMillis() - 2000) { + // Debounce duplicate reads + Log.i(TAG, "debounced @$ctx") + return + } + + lastTagId = tag.hexId() + lastTagTime = System.currentTimeMillis() + + if (mediaTypes?.contains(ReusableMediaType.NFC_UID) == true) { + ctx.runOnUiThread { + buzzer?.vibrate(125) + chipReadListener?.chipReadSuccessfully(tag.hexId(), ReusableMediaType.NFC_UID) + } + } else if (mediaTypes?.contains(ReusableMediaType.NFC_MF0AES) == true) { + scope.launchWithSentry { + val identifier = try { + val nfca = AndroidNfcA(tag) + processMf0aes(keySets, mode, useRandomIdForNewTags, nfca) + } catch (e: NfcChipReadError) { + withContext(Dispatchers.Main) { + buzzer?.vibrate(125) + chipReadListener?.chipReadError(e.errorType, tag.hexId()) + } + return@launchWithSentry + } catch (e: NfcIOError) { + e.printStackTrace() + withContext(Dispatchers.Main) { + buzzer?.vibrate(125) + chipReadListener?.chipReadError(ChipReadError.IO_ERROR, tag.hexId()) + } + return@launchWithSentry + } catch (e: IOException) { + e.printStackTrace() + withContext(Dispatchers.Main) { + buzzer?.vibrate(125) + chipReadListener?.chipReadError(ChipReadError.IO_ERROR, tag.hexId()) + } + return@launchWithSentry + } catch (e: Exception) { + e.printStackTrace() + Sentry.captureException(e) + withContext(Dispatchers.Main) { + buzzer?.vibrate(125) + chipReadListener?.chipReadError(ChipReadError.UNKNOWN_ERROR, tag.hexId()) + } + return@launchWithSentry + } + withContext(Dispatchers.Main) { + buzzer?.vibrate(125) + chipReadListener?.chipReadSuccessfully(identifier, ReusableMediaType.NFC_MF0AES) + } + } + } + } + + class AndroidNfcA(val tag: Tag) : AbstractNfcA { + val raw = NfcA.get(tag) ?: throw NfcChipReadError(ChipReadError.UNKNOWN_CHIP_TYPE) + + override fun connect() { + raw.connect() + } + + override fun transceive(data: ByteArray): ByteArray { + if (BuildConfig.DEBUG) { + Log.i("NFC TRANSMISSION", "=> " + data.toHexString(true)) + } + val reply = raw.transceive(data) + if (BuildConfig.DEBUG) { + Log.i("NFC TRANSMISSION", "<= " + reply.toHexString(true)) + } + return reply + } + + override fun close() { + raw.close() + } + } +} + +fun Tag.hexId(): String = id.joinToString("") { "%02x".format(it).uppercase() } diff --git a/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/hardware/NfcHandler.kt b/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/hardware/NfcHandler.kt new file mode 100644 index 0000000..a7b1e7c --- /dev/null +++ b/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/hardware/NfcHandler.kt @@ -0,0 +1,60 @@ +package eu.pretix.libpretixnfc.android.hardware + +import Mf0aesKeySet +import PretixMf0aes +import android.app.Activity +import eu.pretix.libpretixnfc.android.BuildConfig +import eu.pretix.libpretixnfc.communication.AbstractNfcA + +import eu.pretix.libpretixnfc.communication.ChipReadError +import eu.pretix.libpretixsync.db.ReusableMediaType +import java.lang.RuntimeException + +class NfcUnsupported : Exception() +class NfcDisabled : Exception() + +interface NfcHandler { + interface OnChipReadListener { + fun chipReadSuccessfully(identifier: String, mediaType: ReusableMediaType) + fun chipReadError(error: ChipReadError, identifier: String?) + } + + /** + * Start listening for chips. Should e.g. be called in Activity.onResume(). + */ + @Throws(NfcDisabled::class, NfcUnsupported::class) + fun start(mediaTypes: List) + + /** + * Stop listening for chips. Should e.g. be called in Activity.onPause(). + */ + fun stop() + + fun isRunning(): Boolean + + fun getMediaTypes(): List? + + /** + * Set the callback to be fired whenever a chip is read. + */ + fun setOnChipReadListener(listener: OnChipReadListener?) +} + + +fun getNfcHandler(activity: Activity, keySets: List, useRandomIdForNewTags: Boolean, mode: NfcHandlerMode = NfcHandlerMode.DEFAULT, nfcReaderType: String): NfcHandler { + return when (nfcReaderType) { + "acs" -> AcsNfcHandler(activity, keySets, useRandomIdForNewTags, mode) + "native" -> AndroidNativeNfcHandler(activity, keySets, useRandomIdForNewTags, mode) + else -> throw RuntimeException("Unknown NFC reader type ${nfcReaderType}") + } +} + +fun processMf0aes(keySets: List, mode: NfcHandlerMode, useRandomIdForNewTags: Boolean = false, nfca: AbstractNfcA): String { + return PretixMf0aes( + keySets, + useRandomIdForNewTags, + BuildConfig.DEBUG, + ).process( + nfca, + encodeWith = if (mode == NfcHandlerMode.ENCODE) keySets.firstOrNull { it.canEncode } else null) +} diff --git a/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/hardware/acs/AcsReaderService.kt b/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/hardware/acs/AcsReaderService.kt new file mode 100644 index 0000000..f29b74c --- /dev/null +++ b/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/hardware/acs/AcsReaderService.kt @@ -0,0 +1,241 @@ +package eu.pretix.libpretixnfc.android.hardware.acs + +import android.annotation.SuppressLint +import android.app.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.hardware.usb.UsbDevice +import android.hardware.usb.UsbManager +import android.os.Binder +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.PendingIntentCompat +import androidx.core.content.ContextCompat +import androidx.core.content.IntentCompat +import com.acs.smartcard.Reader +import eu.pretix.libpretixnfc.android.R +import eu.pretix.libpretixnfc.android.hardware.AcsNfcHandler +import eu.pretix.libpretixnfc.android.launchWithSentry +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel + +class AcsReaderService : Service() { + /* + Tested with ACR1252U + + API docs: https://www.acs.com.hk/download-manual/6402/API-ACR1252U-1.17.pdf + */ + + private val binder = LocalBinder() + val notificationChannel = "pretixscan:AcsReaderService" + val description = "ACS reader Service" + val notificationId = 40182724 + public var cardHandler: ((Reader, Int) -> Unit)? = null + private var reader: Reader? = null + val VENDOR_ID = 0x072f + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + /** + * Class used for the client Binder. Because we know this service always + * runs in the same process as its clients, we don't need to deal with IPC. + */ + inner class LocalBinder : Binder() { + // Return this instance of LocalService so clients can call public methods + fun getService(): AcsReaderService = this@AcsReaderService + } + + private fun readerConfig() { + // Disable auto buzzer + val command = + byteArrayOf(0xE0.toByte(), 0x00, 0x00, 0x21, 0x01, 0b01100111.toByte()) + val responseBuffer = ByteArray(300) + reader!!.control( + 0, + Reader.IOCTL_CCID_ESCAPE, + command, + command.size, + responseBuffer, + responseBuffer.size, + ) + } + + private fun setup() { + val manager = getSystemService(USB_SERVICE) as UsbManager + if (reader == null) { + reader = Reader(manager) + var firstStateChange = true + + reader!!.setOnStateChangeListener { slotNum, prevState, currState -> + Log.i( + AcsNfcHandler.Companion.TAG, + "onStateChange slot=$slotNum prevState=$prevState currState=$currState", + ) + if (firstStateChange) { + firstStateChange = false + scope.launchWithSentry { + readerConfig() + } + } + if (slotNum == 0 && prevState == Reader.CARD_ABSENT && currState == Reader.CARD_PRESENT) { + scope.launchWithSentry { + cardHandler?.invoke(reader!!, slotNum) + } + } + } + + findAndConnectDevice() + + val usbFilter = IntentFilter() + usbFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) + usbFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED) + + ContextCompat.registerReceiver( + this, + object : BroadcastReceiver() { + override fun onReceive( + p0: Context, + intent: Intent, + ) { + val device = IntentCompat.getParcelableExtra(intent, "device", UsbDevice::class.java) ?: return + if (intent.action == UsbManager.ACTION_USB_DEVICE_ATTACHED && device.vendorId == VENDOR_ID) { + Log.i( + AcsNfcHandler.Companion.TAG, + "device attached", + ) + connectDevice(device) + } + } + }, + usbFilter, + ContextCompat.RECEIVER_NOT_EXPORTED, + ) + } + } + + private fun findAndConnectDevice() { + val manager = getSystemService(USB_SERVICE) as UsbManager + for (dev in manager.deviceList.values) { + if (dev.vendorId == VENDOR_ID) { + connectDevice(dev) + return + } + } + } + + @SuppressLint("UnspecifiedImmutableFlag") + private fun connectDevice(dev: UsbDevice) { + val manager = getSystemService(USB_SERVICE) as UsbManager + val intent = Intent("eu.pretix.pretixscan.droid.hardware.nfc.USB_PERMISSION") + intent.setPackage(packageName) + val permissionIntent = PendingIntentCompat.getBroadcast( + this, + 0, + intent, + 0, + true + ) + + val filter = IntentFilter("eu.pretix.pretixscan.droid.hardware.nfc.USB_PERMISSION") + ContextCompat.registerReceiver( + this, + object : BroadcastReceiver() { + override fun onReceive( + ctx: Context, + intent: Intent, + ) { + val device = IntentCompat.getParcelableExtra(intent, "device", UsbDevice::class.java) + if (!intent.getBooleanExtra("permission", false)) { + // show error + } else if (device == null) { + // show error + } else { + reader!!.open(device) + } + } + }, + filter, + ContextCompat.RECEIVER_EXPORTED, + ) + manager.requestPermission(dev, permissionIntent) + } + + override fun onBind(intent: Intent): IBinder { + setup() + return binder + } + + override fun onUnbind(intent: Intent?): Boolean { + return true + } + + private fun _startForeground() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = + NotificationChannel( + notificationChannel, + description, + NotificationManager.IMPORTANCE_LOW, + ) + channel.setSound(null, null) + channel.enableVibration(false) + getSystemService(NotificationManager::class.java).createNotificationChannel(channel) + } + + //val startBaseActivity = Intent(this, MainActivity::class.java) + val startBaseActivity = Intent(this, this::class.java) + startBaseActivity.action = Intent.ACTION_MAIN + startBaseActivity.addCategory(Intent.CATEGORY_LAUNCHER) + startBaseActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + val keepAliveNotification: Notification = + NotificationCompat.Builder(this, notificationChannel) + .setContentTitle(getString(R.string.nfc_connected_notification)) + .setSmallIcon(R.drawable.ic_nfc_white_24dp) + .setContentIntent( + PendingIntent.getActivity( + this, + 0, + startBaseActivity, + if (Build.VERSION.SDK_INT >= 23) { + PendingIntent.FLAG_IMMUTABLE + } else { + 0 + }, + ), + ) + .build() + this.startForeground(notificationId, keepAliveNotification) + } + + override fun onStartCommand( + intent: Intent?, + flags: Int, + startId: Int, + ): Int { + _startForeground() + return START_NOT_STICKY + } + + override fun onRebind(intent: Intent?) { + setup() + super.onRebind(intent) + } + + override fun onCreate() { + super.onCreate() + _startForeground() + } + + override fun onDestroy() { + reader?.close() + reader = null + scope.cancel() + super.onDestroy() + } +} diff --git a/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/platform/AndroidKeyStore.kt b/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/platform/AndroidKeyStore.kt new file mode 100644 index 0000000..8c4fe46 --- /dev/null +++ b/libpretixnfc-android/src/main/java/eu/pretix/libpretixnfc/android/platform/AndroidKeyStore.kt @@ -0,0 +1,130 @@ +package eu.pretix.libpretixnfc.android.platform + +import android.content.Context +import android.os.Build +import android.security.KeyStoreParameter +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.security.keystore.KeyProtection +import eu.pretix.libpretixnfc.platform.HardwareBackedKeyStore +import org.spongycastle.util.io.pem.PemObject +import org.spongycastle.util.io.pem.PemWriter +import java.io.StringWriter +import java.nio.charset.Charset +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.PrivateKey +import java.util.concurrent.locks.ReentrantLock +import javax.crypto.Cipher +import javax.crypto.Mac +import javax.crypto.SecretKey +import kotlin.concurrent.withLock + + +val keyLock = ReentrantLock() + +class AndroidKeyStore(val ctx: Context) : HardwareBackedKeyStore { + override fun importHmacKey(keyName: String, keyValue: ByteArray) { + val key = object : SecretKey { + override fun getAlgorithm(): String { + return "HmacSHA512" + } + + override fun getFormat(): String { + return "RAW" + } + + override fun getEncoded(): ByteArray { + return keyValue + } + } + + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + keyStore.setEntry( + keyName, + KeyStore.SecretKeyEntry(key), + KeyProtection.Builder(KeyProperties.PURPOSE_SIGN).build() + ) + } else { + keyStore.setEntry( + keyName, + KeyStore.SecretKeyEntry(key), + KeyStoreParameter.Builder(ctx).build(), + ) + } + } + + override fun hmacSHA256(keyName: String, message: ByteArray): ByteArray { + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + // Key imported, obtain a reference to it. + val keyStoreKey = keyStore.getKey(keyName, null) + // The original key can now be discarded. + + val mac = Mac.getInstance("HmacSHA512") + mac.init(keyStoreKey) + mac.update(message) + return mac.doFinal() + } + + override fun hasHmacKey(keyName: String): Boolean { + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + return keyStore.isKeyEntry(keyName) + } + + override fun getOrCreateRsaPubKey(keyName: String): ByteArray? { + return keyLock.withLock { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return null + } + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + val pubkey = if (keyStore.isKeyEntry(keyName)) { + keyStore.getCertificate(keyName).publicKey + } else { + val keyPair = + KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore") + .apply { + val parameterSpec = KeyGenParameterSpec.Builder( + keyName, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ).run { + setBlockModes(KeyProperties.BLOCK_MODE_ECB) + setDigests( + KeyProperties.DIGEST_SHA1, + KeyProperties.DIGEST_SHA256, + KeyProperties.DIGEST_SHA512 + ) + setEncryptionPaddings( + KeyProperties.ENCRYPTION_PADDING_RSA_OAEP, + KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1 + ) + setKeySize(2048) + setUserAuthenticationRequired(false) + build() + } + initialize(parameterSpec) + }.genKeyPair() + keyPair.public + } + val writer = StringWriter() + val pemWriter = PemWriter(writer) + pemWriter.writeObject(PemObject("PUBLIC KEY", pubkey.encoded)) + pemWriter.flush() + pemWriter.close() + return@withLock writer.toString().toByteArray(Charset.defaultCharset()) + } + } + + override fun decryptRsa(keyName: String, ciphertext: ByteArray): ByteArray { + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + val privateKey = keyStore.getKey(keyName, null) as PrivateKey? + val cipher: Cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding") + cipher.init(Cipher.DECRYPT_MODE, privateKey) + return cipher.doFinal(ciphertext) + } +} \ No newline at end of file diff --git a/libpretixnfc-android/src/main/res/drawable/ic_nfc_24dp.xml b/libpretixnfc-android/src/main/res/drawable/ic_nfc_24dp.xml new file mode 100644 index 0000000..8f4ecad --- /dev/null +++ b/libpretixnfc-android/src/main/res/drawable/ic_nfc_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/libpretixnfc-android/src/main/res/drawable/ic_nfc_white_24dp.xml b/libpretixnfc-android/src/main/res/drawable/ic_nfc_white_24dp.xml new file mode 100644 index 0000000..ab3d9d1 --- /dev/null +++ b/libpretixnfc-android/src/main/res/drawable/ic_nfc_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/libpretixnfc-android/src/main/res/values-de/strings.xml b/libpretixnfc-android/src/main/res/values-de/strings.xml new file mode 100644 index 0000000..c17d3bf --- /dev/null +++ b/libpretixnfc-android/src/main/res/values-de/strings.xml @@ -0,0 +1,11 @@ + + NFC-Reader-Service läuft + Dieses Medium hat eine zufällige UID und kann nicht benutzt werden. + Der NFC-Chip konnte nicht gelesen werden. Probieren Sie, ihn länger am Gerät zu halten. + Diese Art von NFC-Chip wird nicht unterstützt. + Dieser NFC-Chip ist von einem anderen System kodiert. + Dieser NFC-Chip ist noch nicht kodiert. + NFC ist auf diesem Gerät gerade deaktiviert. + NFC wird auf diesem Gerät nicht unterstützt. + Prüfe Medium… + \ No newline at end of file diff --git a/libpretixnfc-android/src/main/res/values-es/strings.xml b/libpretixnfc-android/src/main/res/values-es/strings.xml new file mode 100644 index 0000000..d2da22d --- /dev/null +++ b/libpretixnfc-android/src/main/res/values-es/strings.xml @@ -0,0 +1,11 @@ + + Este chip NFC está cifrado por un mecanismo aparte. + Este chip NFC todavía no está cifrado. + NFC está deshabilitado en este momento en este dispositivo. + Este dispositivo no admite NFC. + Comprobando el medio… + El soporte tiene un identificador aleatorio y no se puede utilizar. + No se ha podido leer el chip NFC. Intente mantenerlo cerca del dispositivo durante más tiempo. + Este tipo de chip NFC no es compatible. + Servicio de lector NFC en ejecución + diff --git a/libpretixnfc-android/src/main/res/values-fr/strings.xml b/libpretixnfc-android/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..cc4f507 --- /dev/null +++ b/libpretixnfc-android/src/main/res/values-fr/strings.xml @@ -0,0 +1,12 @@ + + + Cette puce NFC est chiffrée avec un système différent. + Cette puce NFC n\\\'est pas encore chiffrée. + La fonction NFC est actuellement désactivée sur cet appareil. + La technologie NFC n\\\'est pas prise en charge par cet appareil. + Vérification du support… + Service de lecture NFC en cours d\\\'exécution + Ce support a un identifiant aléatoire et ne peut pas être utilisé. + La puce NFC n\\\'a pas pu être lue. Essayez de le maintenir plus longtemps à proximité de l\\\'appareil. + Ce type de puce NFC n\\\'est pas pris en charge. + \ No newline at end of file diff --git a/libpretixnfc-android/src/main/res/values-ja/strings.xml b/libpretixnfc-android/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000..35580fb --- /dev/null +++ b/libpretixnfc-android/src/main/res/values-ja/strings.xml @@ -0,0 +1,5 @@ + + + このデバイスでは NFC はサポートされていません。 + メディアを確認中… + \ No newline at end of file diff --git a/libpretixnfc-android/src/main/res/values-nl/strings.xml b/libpretixnfc-android/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000..50acc71 --- /dev/null +++ b/libpretixnfc-android/src/main/res/values-nl/strings.xml @@ -0,0 +1,11 @@ + + NFC-lezerservice actief + Dit medium heeft een willekeurig ID en kan niet worden gebruikt. + De NFC-chip kon niet uitgelezen worden. Probeer de chip langer en dicht tegen het apparaat te houden. + Deze NFC-chip is gecodeerd door een ander systeem. + De NFC-chip is nog niet gecodeerd. + NFC is momenteel uitgeschakeld op dit apparaat. + NFC is niet ondersteund op dit apparaat. + Controleren van het medium… + Dit type van NFC-chip is niet ondersteund. + \ No newline at end of file diff --git a/libpretixnfc-android/src/main/res/values/strings.xml b/libpretixnfc-android/src/main/res/values/strings.xml new file mode 100644 index 0000000..2857e6c --- /dev/null +++ b/libpretixnfc-android/src/main/res/values/strings.xml @@ -0,0 +1,11 @@ + + NFC reader service running + This medium has a random ID and can\'t be used. + The NFC chip could not be read. Try keeping it close to the device for a longer time. + This type of NFC chip is not supported. + This NFC chip is encoded by a different system. + This NFC chip is not yet encoded. + NFC is currently disabled on this device. + NFC is not supported on this device. + Checking medium… + \ No newline at end of file diff --git a/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/commands/nxp/mf0aes/AuthenticationHelper.kt b/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/commands/nxp/mf0aes/AuthenticationHelper.kt index caa3170..cd41034 100644 --- a/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/commands/nxp/mf0aes/AuthenticationHelper.kt +++ b/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/commands/nxp/mf0aes/AuthenticationHelper.kt @@ -4,7 +4,6 @@ import eu.pretix.libpretixnfc.commands.nxp.ReadPages import eu.pretix.libpretixnfc.communication.AbstractNfcA import eu.pretix.libpretixnfc.communication.NfcIOError import eu.pretix.libpretixnfc.rotateLeft -import eu.pretix.libpretixnfc.toHexString import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.SecureRandom import javax.crypto.Cipher @@ -39,11 +38,11 @@ private val defaultRndGenerator = { length: Int -> b } +val staticAesCMac: Mac = Mac.getInstance("AESCMAC", BouncyCastleProvider()) fun aesCmac(secret: ByteArray, plaintext: ByteArray): ByteArray { val secretKey = SecretKeySpec(secret, 0, 16, "AES") - val mac = Mac.getInstance("AESCMAC", BouncyCastleProvider()) - mac.init(secretKey) - return mac.doFinal(plaintext) + staticAesCMac.init(secretKey) + return staticAesCMac.doFinal(plaintext) } fun truncateMac(mac: ByteArray): ByteArray { diff --git a/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/cryptography/An10922KeyDiversification.kt b/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/cryptography/An10922KeyDiversification.kt index 7a1d9aa..04bec98 100644 --- a/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/cryptography/An10922KeyDiversification.kt +++ b/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/cryptography/An10922KeyDiversification.kt @@ -7,6 +7,10 @@ import javax.crypto.spec.SecretKeySpec class An10922KeyDiversification { // https://www.nxp.com/docs/en/application-note/AN10922.pdf + companion object { + val staticAesCMac: Mac = Mac.getInstance("AESCMAC", BouncyCastleProvider()) + } + fun generateDiversifiedKeyAES128(masterKey: ByteArray, uid: ByteArray, applicationId: ByteArray, systemId: ByteArray): ByteArray { check(masterKey.size == 16) @@ -23,9 +27,8 @@ class An10922KeyDiversification { }*/ val secretKey = SecretKeySpec(masterKey, 0, 16, "AES") - val mac = Mac.getInstance("AESCMAC", BouncyCastleProvider()) - mac.init(secretKey) - val cmac = mac.doFinal(diversificationInput) + staticAesCMac.init(secretKey) + val cmac = staticAesCMac.doFinal(diversificationInput) return cmac } } \ No newline at end of file diff --git a/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/highlevel/PretixMf0aes.kt b/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/highlevel/PretixMf0aes.kt index bfc1cb6..fdde57e 100644 --- a/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/highlevel/PretixMf0aes.kt +++ b/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/highlevel/PretixMf0aes.kt @@ -53,7 +53,11 @@ class PretixMf0aes(val keySets: List, val useRandomIdForNewTags: B } throw NfcChipReadError(ChipReadError.FOREIGN_CHIP) } finally { - nfca.close() + try { + nfca.close() + } catch (_: SecurityException) { + // ignore "tag is out of date" + } } } diff --git a/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/platform/HardwareBackedKeyStore.kt b/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/platform/HardwareBackedKeyStore.kt new file mode 100644 index 0000000..8486728 --- /dev/null +++ b/libpretixnfc/src/main/java/eu/pretix/libpretixnfc/platform/HardwareBackedKeyStore.kt @@ -0,0 +1,9 @@ +package eu.pretix.libpretixnfc.platform + +interface HardwareBackedKeyStore { + fun hasHmacKey(keyName: String): Boolean + fun importHmacKey(keyName: String, keyValue: ByteArray) + fun hmacSHA256(keyName: String, message: ByteArray): ByteArray + fun getOrCreateRsaPubKey(keyName: String): ByteArray? + fun decryptRsa(keyName: String, ciphertext: ByteArray): ByteArray +} \ No newline at end of file