diff --git a/README.md b/README.md index 344a523..e01bc3e 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,11 @@ The demo application (`demoapp/app`) provides a complete reference implementatio #### Building the AAR +Requirements: + +* JDK 21 or newer — the libraries are compiled to Java 21 bytecode, both for building the AARs and for compiling an application against them. +* Application `minSdk` 34 or higher + ```shell ./gradlew -p libs assemble ``` @@ -76,17 +81,29 @@ The demo application (`demoapp/app`) provides a complete reference implementatio * The `.aar` files are located in: * `libs/id-card-lib/build/outputs/aar` * `libs/smart-card-reader-lib/build/outputs/aar` + * `libs/card-utils-lib/build/outputs/aar` * Move the resulting `.aar` files to your project's `/libs` directory. -* Add the dependencies to your application's `build.gradle` file: - * `implementation files('app/libs/id-card-lib.aar')` - * `implementation files('app/libs/smart-card-reader-lib.aar')` +* Add the dependencies to your application's `build.gradle.kts` file: + * `implementation(files("app/libs/id-card-lib.aar"))` + * `implementation(files("app/libs/smart-card-reader-lib.aar"))` + * `implementation(files("app/libs/card-utils-lib.aar"))` + +All three `.aar` files are required. + +Your application must declare these dependencies: + +```kotlin +implementation("org.bouncycastle:bcprov-jdk18on:1.86") +implementation("org.jetbrains.kotlin:kotlin-stdlib") +``` ## Overview -ID card support for Android applications is based on two libraries: `id-card-lib` and `smart-card-reader-lib`. +ID card support for Android applications is based on three libraries: `id-card-lib`, `smart-card-reader-lib` and `card-utils-lib`. * `smart-card-reader-lib` enables the use of the ID card over NFC. It provides the NFC smart card reader interface and communication layer. * `id-card-lib` implements the APDU-based communication protocols required to use the core functionality across different types of ID cards (IDEMIA and Thales). +* `card-utils-lib` provides the logging utility used by the other two libraries. Integrating ID card support into an Android application proceeds as follows: diff --git a/build.gradle.kts b/build.gradle.kts index 4923bbd..3d8a023 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,14 @@ // SPDX-FileCopyrightText: Estonian Information System Authority // SPDX-License-Identifier: LGPL-2.1-or-later +buildscript { + dependencies { + classpath(libs.kotlin.gradle.plugin) + } +} + // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false - alias(libs.plugins.jetbrains.kotlin.android) apply false - alias(libs.plugins.jetbrains.kotlin.jvm) apply false - alias(libs.plugins.ksp) apply false - alias(libs.plugins.google.dagger) apply false } \ No newline at end of file diff --git a/demoapp/app/build.gradle.kts b/demoapp/app/build.gradle.kts index e6ae5f3..1520311 100644 --- a/demoapp/app/build.gradle.kts +++ b/demoapp/app/build.gradle.kts @@ -5,17 +5,16 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.application) - alias(libs.plugins.jetbrains.kotlin.android) } android { namespace = "ee.ria.DigiDoc.smartcardreader.nfc.example" - compileSdk = 36 + compileSdk = 37 defaultConfig { applicationId = "ee.ria.DigiDoc.smartcardreader.nfc.example" - minSdk = 26 - targetSdk = 36 + minSdk = 34 + targetSdk = 37 versionCode = 3 versionName = "1.0.1" @@ -32,22 +31,22 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - kotlin { - compilerOptions { - jvmTarget = JvmTarget.JVM_17 - } + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } buildFeatures { viewBinding = true } } +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_21 + } +} + dependencies { implementation(libs.androidx.activity) - implementation(libs.androidx.annotation) implementation(libs.androidx.appcompat) implementation(libs.androidx.constraintlayout) implementation(libs.androidx.core) @@ -56,7 +55,6 @@ dependencies { implementation(libs.androidx.navigation.runtime) implementation(libs.bcprov.jdk18on) implementation(libs.fragment.ktx) - implementation(libs.guava) implementation(libs.material) implementation(libs.navigation.fragment.ktx) implementation(libs.recyclerview) diff --git a/demoapp/app/src/main/AndroidManifest.xml b/demoapp/app/src/main/AndroidManifest.xml index c118076..2ed8b6e 100644 --- a/demoapp/app/src/main/AndroidManifest.xml +++ b/demoapp/app/src/main/AndroidManifest.xml @@ -1,8 +1,7 @@ - + @@ -16,8 +15,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.NFCApp" - android:enableOnBackInvokedCallback="true" - tools:targetApi="31"> + android:enableOnBackInvokedCallback="true"> diff --git a/demoapp/app/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/example/MainActivity.kt b/demoapp/app/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/example/MainActivity.kt index 3b99ab8..c6d7caa 100644 --- a/demoapp/app/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/example/MainActivity.kt +++ b/demoapp/app/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/example/MainActivity.kt @@ -84,24 +84,13 @@ class MainActivity : AppCompatActivity() { private fun getAppVersion(context: Context): StringBuilder { val versionName = StringBuilder() try { - versionName.append( - context.packageManager.getPackageInfo( - context.packageName, - 0 - ).versionName + val packageInfo = context.packageManager.getPackageInfo( + context.packageName, + PackageManager.PackageInfoFlags.of(0) ) + versionName.append(packageInfo.versionName) .append(".") - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - versionName.append( - context.packageManager.getPackageInfo(context.packageName, 0).longVersionCode - ) - } else { - @Suppress("DEPRECATION") - versionName.append( - context.packageManager.getPackageInfo(context.packageName, 0).versionCode - ) - } + .append(packageInfo.longVersionCode) } catch (e: PackageManager.NameNotFoundException) { e.printStackTrace() } diff --git a/demoapp/app/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/example/sign/ContainerFragment.kt b/demoapp/app/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/example/sign/ContainerFragment.kt index 1a2cbd5..57ecb27 100644 --- a/demoapp/app/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/example/sign/ContainerFragment.kt +++ b/demoapp/app/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/example/sign/ContainerFragment.kt @@ -33,12 +33,16 @@ import ee.ria.DigiDoc.smartcardreader.nfc.example.util.Utils.filesToCache import ee.ria.DigiDoc.smartcardreader.nfc.example.util.Utils.getContainerFilesList import ee.ria.DigiDoc.smartcardreader.nfc.example.util.Utils.signatureIsAdded import ee.ria.DigiDoc.smartcardreader.nfc.example.viewmodel.DataViewModel +import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.libdigidocpp.Container import java.io.File +import java.io.IOException import java.util.Locale class ContainerFragment : Fragment() { + private val logTag = javaClass.simpleName + private val dataViewModel: DataViewModel by activityViewModels() private lateinit var binding: FragmentContainerBinding private lateinit var backButton: Button @@ -105,7 +109,17 @@ class ContainerFragment : Fragment() { } addFileButton.setOnClickListener { addFile() } addSignatureButton.setOnClickListener { - filesToCache(requireContext().cacheDir.absolutePath) + try { + filesToCache(requireContext().cacheDir.absolutePath) + } catch (ex: IOException) { + errorLog(logTag, "Unable to prepare data files", ex) + Toast.makeText( + requireContext(), + "Unable to prepare data files.", + Toast.LENGTH_SHORT + ).show() + return@setOnClickListener + } createContainer() dataViewModel.setContainerName(containerNameEditText.text.toString()) val bundle = Bundle() @@ -197,7 +211,7 @@ class ContainerFragment : Fragment() { throw Exception("Not enough free space.") } } catch (ex: Exception) { - ex.printStackTrace() + errorLog(logTag, "Unable to create container", ex) } } diff --git a/demoapp/app/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/example/util/Utils.kt b/demoapp/app/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/example/util/Utils.kt index 3dc7f31..c971eb1 100644 --- a/demoapp/app/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/example/util/Utils.kt +++ b/demoapp/app/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/example/util/Utils.kt @@ -7,24 +7,21 @@ import android.app.Activity import android.content.ContentResolver import android.content.Context import android.content.Intent -import android.database.Cursor import android.net.Uri import android.provider.OpenableColumns -import com.google.common.io.Files -import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog +import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.libdigidocpp.Container import ee.ria.libdigidocpp.Signature import java.io.File import java.io.FileOutputStream import java.io.IOException -import java.io.InputStream -import java.io.OutputStream object Utils { val origin = "ivxv.valimised.ee:443" private val logTag = javaClass.simpleName const val SIGNATURE_CONTAINER_EXTENSION = "asice" + private const val DEFAULT_MIME_TYPE = "application/octet-stream" private val containerFiles: MutableList = mutableListOf() lateinit var container: Container lateinit var signature: Signature @@ -54,94 +51,91 @@ object Utils { } fun getFileNameAndSize(uri: Uri, context: Context): Pair { - var fileName: String = uri.lastPathSegment!! + var fileName: String = uri.lastPathSegment ?: "" var fileSize: Long = 0 if (uri.scheme.equals("content")) { try { - val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)!! - if (cursor != null && cursor.moveToFirst()) { - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - if (nameIndex >= 0) { - fileName = cursor.getString(nameIndex) + context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (nameIndex >= 0) { + fileName = cursor.getString(nameIndex) + } + val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) + if (sizeIndex >= 0) { + fileSize = cursor.getLong(sizeIndex) + } } - val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) - if (sizeIndex >= 0) { - fileSize = cursor.getLong(sizeIndex) - } - cursor.close() } } catch (exception: Exception) { - exception.printStackTrace() + errorLog(logTag, "Unable to read file name and size", exception) } } return Pair(fileName, fileSize) } - fun removeFileExtension(fileName: String): String { - val index = fileName.lastIndexOf(".") - return fileName.substring(0, index) - } + fun removeFileExtension(fileName: String): String = fileName.substringBeforeLast('.') fun signatureContainerFile(fileName: String, filesDir: File): File { val containerDir = createContainersDir(filesDir) val containerFile = File(containerDir, fileName) - if (containerFile.canonicalPath.startsWith(containerDir.canonicalPath)) { - Files.createParentDirs(containerFile) - } else { + if (!containerFile.canonicalFile.toPath().startsWith(containerDir.canonicalFile.toPath())) { throw IOException("Invalid file path") } + val parent = containerFile.parentFile + if (parent != null && !parent.isDirectory && !parent.mkdirs()) { + throw IOException("Unable to create container parent directories") + } return containerFile } private fun createContainersDir(filesDir: File): File { val dir = File(filesDir, "containers") - if (dir.mkdirs()) { - debugLog(logTag, String.format("Directories created for %s", dir.path)) + if (!dir.isDirectory && !dir.mkdirs()) { + throw IOException("Unable to create containers directory") } return dir } - fun getFilesFromCache(cacheDir: String): MutableList { - val fileList: MutableList = mutableListOf() - for (file in File("$cacheDir/datafiles").listFiles()!!) { - fileList.add(file) - } - return fileList - } + fun getFilesFromCache(cacheDir: String): MutableList = + File("$cacheDir/datafiles").listFiles()?.toMutableList() ?: mutableListOf() fun filesToCache(cacheDir: String) { val datafilesDir = createDataFilesDir(File(cacheDir)) - Files.createParentDirs(datafilesDir) for (fileData in containerFiles) { val cacheFile = File(datafilesDir, fileData.getFileName()) - if (cacheFile.canonicalPath.startsWith(File(cacheDir).canonicalPath)) { - try { - val inp: InputStream = - fileData.getContentResolver() - .openInputStream(fileData.getFileIntent().data!!)!! - val outputStream: OutputStream = FileOutputStream(cacheFile) - inp.copyTo(outputStream, DEFAULT_BUFFER_SIZE) - inp.close() - outputStream.close() - } catch (ex: Exception) { - ex.printStackTrace() - } + if (!cacheFile.canonicalFile.toPath().startsWith(File(cacheDir).canonicalFile.toPath())) { + errorLog(logTag, "Data file resolves outside the cache directory, skipping") + continue + } + val uri = fileData.getFileIntent().data + if (uri == null) { + errorLog(logTag, "Data file has no URI, skipping") + continue + } + try { + fileData.getContentResolver().openInputStream(uri)?.use { input -> + FileOutputStream(cacheFile).use { output -> + input.copyTo(output, DEFAULT_BUFFER_SIZE) + } + } ?: errorLog(logTag, "Could not open data file, skipping") + } catch (ex: Exception) { + errorLog(logTag, "Unable to cache data file", ex) } } } private fun createDataFilesDir(cacheDir: File): File { val dir = File(cacheDir, "datafiles") - if (dir.mkdirs()) { - debugLog(logTag, String.format("Directories created for %s", dir.path)) + if (!dir.isDirectory && !dir.mkdirs()) { + throw IOException("Unable to create $dir") } return dir } fun getMimeType(index: Int): String { - val uri = containerFiles[index].getFileIntent().data - val contentResolver = containerFiles[index].getContentResolver() - return contentResolver.getType(uri!!)!! + val uri = containerFiles[index].getFileIntent().data ?: return DEFAULT_MIME_TYPE + return containerFiles[index].getContentResolver().getType(uri) ?: DEFAULT_MIME_TYPE } fun checkFreeSpace(container: Container, activity: Activity): Boolean { diff --git a/demoapp/libdigidocpp/build.gradle.kts b/demoapp/libdigidocpp/build.gradle.kts index 1deffe1..7740713 100644 --- a/demoapp/libdigidocpp/build.gradle.kts +++ b/demoapp/libdigidocpp/build.gradle.kts @@ -5,17 +5,15 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.library) - alias(libs.plugins.jetbrains.kotlin.android) - alias(libs.plugins.ksp) } android { namespace = "ee.ria.libdigidocpp" - compileSdk = 36 - testOptions.targetSdk = 36 - lint.targetSdk = 36 + compileSdk = 37 + testOptions.targetSdk = 37 + lint.targetSdk = 37 defaultConfig { - minSdk = 26 + minSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -26,7 +24,7 @@ android { sourceSets { named("main") { - java.srcDir("include") + java.directories.add("include") } } @@ -41,19 +39,13 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } +} - kotlin { - compilerOptions { - jvmTarget = JvmTarget.JVM_17 - } +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_21 } } - -dependencies { - implementation(libs.dagger) - implementation(libs.javax.inject) - ksp(libs.dagger.compiler) -} \ No newline at end of file diff --git a/demoapp/libdigidocpp/src/main/java/ee/ria/libdigidocpp/DigiDocWrapperImpl.kt b/demoapp/libdigidocpp/src/main/java/ee/ria/libdigidocpp/DigiDocWrapperImpl.kt index 3702893..6e1082d 100644 --- a/demoapp/libdigidocpp/src/main/java/ee/ria/libdigidocpp/DigiDocWrapperImpl.kt +++ b/demoapp/libdigidocpp/src/main/java/ee/ria/libdigidocpp/DigiDocWrapperImpl.kt @@ -12,9 +12,8 @@ import java.io.IOException import java.util.zip.ZipEntry import java.util.zip.ZipException import java.util.zip.ZipInputStream -import javax.inject.Inject -class DigiDocWrapperImpl @Inject constructor(private val context: Context) : DigiDocWrapper { +class DigiDocWrapperImpl(private val context: Context) : DigiDocWrapper { override fun schemaDirectory() : String { return File(context.cacheDir.absolutePath + File.separator + SCHEMA_DIR).canonicalPath diff --git a/demoapp/libdigidocpp/src/main/java/ee/ria/libdigidocpp/di/DigiDocModule.kt b/demoapp/libdigidocpp/src/main/java/ee/ria/libdigidocpp/di/DigiDocModule.kt deleted file mode 100644 index 5cd25ac..0000000 --- a/demoapp/libdigidocpp/src/main/java/ee/ria/libdigidocpp/di/DigiDocModule.kt +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: Estonian Information System Authority -// SPDX-License-Identifier: LGPL-2.1-or-later - -package ee.ria.libdigidocpp.di - -import dagger.Binds -import dagger.Module -import ee.ria.libdigidocpp.DigiDocWrapper -import ee.ria.libdigidocpp.DigiDocWrapperImpl -import javax.inject.Singleton - -@Module(includes = [DigiDocModule.Definitions::class]) -object DigiDocModule { - - @Module - internal interface Definitions { - - @Binds - @Singleton - fun bindDigiDoc(digiDoc: DigiDocWrapperImpl): DigiDocWrapper - } -} diff --git a/gradle.properties b/gradle.properties index 2e568ce..cc16b69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,9 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# Caches the configuration phase result so subsequent builds skip it entirely, +# reducing incremental build times +org.gradle.configuration-cache=true # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects @@ -24,4 +27,3 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -android.nonFinalResIds=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a99a466..ffffede 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,27 +2,20 @@ # SPDX-License-Identifier: LGPL-2.1-or-later [versions] -activity = "1.11.0" -agp = "8.13.0" -annotation = "1.9.1" -appcompat = "1.7.1" -autoValue = "1.11.0" -bouncyCastle = "1.82" -constraintlayout = "2.2.1" -core = "1.17.0" -dagger = "2.57.2" -dependencyCheck = "3.0.4" -fragmentKtx = "1.8.9" -guava = "33.5.0-android" -hamcrest = "3.0" -javaxInject = "1" -junit = "4.13.2" -kotlin = "2.2.20" -ksp = "2.2.20-2.0.3" -lifecycleViewmodel = "2.9.4" -material = "1.13.0" -mockitoCore = "5.20.0" -navigationFragmentKtx = "2.9.5" +activity = "1.13.0" +agp = "9.4.1" +annotation = "1.10.0" +appcompat = "1.8.0" +bouncyCastle = "1.86" +constraintlayout = "2.2.2" +core = "1.19.0" +fragmentKtx = "1.9.0" +junitBom = "6.1.3" +kotlin = "2.4.20" +lifecycleViewmodel = "2.11.0" +material = "1.14.0" +mockitoCore = "5.23.0" +navigationFragmentKtx = "2.10.1" recyclerview = "1.4.0" truth = "1.4.5" @@ -35,19 +28,12 @@ androidx-core = { module = "androidx.core:core", version.ref = "core" } androidx-lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "lifecycleViewmodel" } androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycleViewmodel" } androidx-navigation-runtime = { module = "androidx.navigation:navigation-runtime", version.ref = "navigationFragmentKtx" } -auto-value = { group = "com.google.auto.value", name = "auto-value", version.ref = "autoValue" } -auto-value-annotations = { group = "com.google.auto.value", name = "auto-value-annotations", version.ref = "autoValue" } bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncyCastle" } -dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } -dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" } -google-dagger-hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "dagger" } -google-dagger-hilt-android-compile = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "dagger" } -google-dagger-hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "dagger" } -guava = {group = "com.google.guava", name = "guava", version.ref = "guava" } -hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } -javax-inject = { module = "javax.inject:javax.inject", version.ref = "javaxInject" } -junit = { group = "junit", name = "junit", version.ref = "junit" } +junit-bom = { module = "org.junit:junit-bom", version.ref = "junitBom" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockitoCore" } navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } @@ -57,8 +43,3 @@ truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } -dependency-analysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyCheck" } -google-dagger = {id = "com.google.dagger.hilt.android", version.ref = "dagger"} -jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8bdaf60..eddabd2 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6d023f1..ad7845b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index faf9300..249efbb 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 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. @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # 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" ) @@ -212,8 +210,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21..a51ec4f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,12 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ 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 +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,30 +65,18 @@ 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 +"%COMSPEC%" /c exit 1 :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 %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -: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 +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/libs/card-utils-lib/build.gradle.kts b/libs/card-utils-lib/build.gradle.kts index 7c07b86..398a3a1 100644 --- a/libs/card-utils-lib/build.gradle.kts +++ b/libs/card-utils-lib/build.gradle.kts @@ -5,30 +5,21 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.library) - alias(libs.plugins.google.dagger) - alias(libs.plugins.jetbrains.kotlin.android) - alias(libs.plugins.ksp) } android { namespace = "ee.ria.DigiDoc.utilsLib" - compileSdk = 36 + compileSdk = 37 defaultConfig { - minSdk = 26 + minSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlin { - compilerOptions { - jvmTarget = JvmTarget.JVM_17 - } + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } buildFeatures { @@ -43,10 +34,8 @@ android { } } -dependencies { - implementation(libs.dagger) - implementation(libs.google.dagger.hilt.android) - implementation(libs.google.dagger.hilt.core) - implementation(libs.javax.inject) - ksp(libs.google.dagger.hilt.android.compile) +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_21 + } } diff --git a/libs/card-utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/di/UtilModules.kt b/libs/card-utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/di/UtilModules.kt deleted file mode 100644 index e936f0d..0000000 --- a/libs/card-utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/di/UtilModules.kt +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: Estonian Information System Authority -// SPDX-License-Identifier: LGPL-2.1-or-later - -@file:Suppress("PackageName") - -package ee.ria.DigiDoc.utilsLib.di - -import android.content.Context -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil -import java.util.logging.Logger -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -class UtilModules { - @Provides - @Singleton - fun provideLoggingUtil(context: Context): LoggingUtil = - LoggingUtil().apply { - LoggingUtil.initialize(context, Logger.getLogger(UtilModules::class.java.name), false) - } -} diff --git a/libs/card-utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/logging/LoggingUtil.kt b/libs/card-utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/logging/LoggingUtil.kt index 8517466..33e2be0 100644 --- a/libs/card-utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/logging/LoggingUtil.kt +++ b/libs/card-utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/logging/LoggingUtil.kt @@ -13,8 +13,6 @@ import java.util.logging.ConsoleHandler import java.util.logging.Formatter import java.util.logging.LogRecord import java.util.logging.Logger -import javax.inject.Inject -import javax.inject.Singleton interface Logging { fun errorLog( @@ -35,65 +33,62 @@ interface Logging { ) } -@Singleton -class LoggingUtil - @Inject - constructor() { - companion object : Logging { - private lateinit var logger: Logger - private var isLoggingEnabled: Boolean = false +class LoggingUtil { + companion object : Logging { + private lateinit var logger: Logger + private var isLoggingEnabled: Boolean = false - fun initialize( - context: Context, - appLogger: Logger, - loggingEnabled: Boolean, - ) { - isLoggingEnabled = loggingEnabled + fun initialize( + context: Context, + appLogger: Logger, + loggingEnabled: Boolean, + ) { + isLoggingEnabled = loggingEnabled - if (isLoggingEnabled) { - logger = appLogger - val consoleHandler = ConsoleHandler() - consoleHandler.formatter = LogFormatter() - logger.addHandler(consoleHandler) - logger.level = java.util.logging.Level.ALL - consoleHandler.level = java.util.logging.Level.ALL - } + if (isLoggingEnabled) { + logger = appLogger + val consoleHandler = ConsoleHandler() + consoleHandler.formatter = LogFormatter() + logger.addHandler(consoleHandler) + logger.level = java.util.logging.Level.ALL + consoleHandler.level = java.util.logging.Level.ALL } + } - override fun errorLog( - tag: String, - message: String, - throwable: Throwable?, - ) { - if (isLoggingEnabled) { - throwable?.let { - logger.severe("$tag: $message ${it.localizedMessage}") - } ?: logger.severe("$tag: $message") - } + override fun errorLog( + tag: String, + message: String, + throwable: Throwable?, + ) { + if (isLoggingEnabled) { + throwable?.let { + logger.severe("$tag: $message ${it.localizedMessage}") + } ?: logger.severe("$tag: $message") } + } - override fun debugLog( - tag: String, - message: String, - throwable: Throwable?, - ) { - if (isLoggingEnabled) { - throwable?.let { - logger.fine("$tag: $message ${it.localizedMessage}") - } ?: logger.fine("$tag: $message") - } + override fun debugLog( + tag: String, + message: String, + throwable: Throwable?, + ) { + if (isLoggingEnabled) { + throwable?.let { + logger.fine("$tag: $message ${it.localizedMessage}") + } ?: logger.fine("$tag: $message") } + } - override fun infoLog( - tag: String, - message: String, - ) { - if (isLoggingEnabled) { - logger.info("$tag: $message") - } + override fun infoLog( + tag: String, + message: String, + ) { + if (isLoggingEnabled) { + logger.info("$tag: $message") } } } +} class LogFormatter : Formatter() { override fun format(record: LogRecord): String { diff --git a/libs/id-card-lib/build.gradle.kts b/libs/id-card-lib/build.gradle.kts index a527fb8..b57ee95 100644 --- a/libs/id-card-lib/build.gradle.kts +++ b/libs/id-card-lib/build.gradle.kts @@ -7,19 +7,15 @@ plugins { android { namespace = "ee.ria.DigiDoc.idcard" - compileSdk = 36 + compileSdk = 37 defaultConfig { - minSdk = 26 + minSdk = 34 } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - lint { - lintConfig = file("../lint.xml") + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } buildFeatures { @@ -30,9 +26,6 @@ android { dependencies { implementation(libs.androidx.annotation) implementation(libs.bcprov.jdk18on) - implementation(libs.guava) - annotationProcessor(libs.auto.value) - compileOnly(libs.auto.value.annotations) implementation(project(":libs:smart-card-reader-lib")) implementation(project(":libs:card-utils-lib")) diff --git a/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/Idemia.java b/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/Idemia.java index 60ee9d1..08625c3 100644 --- a/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/Idemia.java +++ b/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/Idemia.java @@ -3,12 +3,10 @@ package ee.ria.DigiDoc.idcard; -import static com.google.common.primitives.Bytes.concat; +import static org.bouncycastle.util.Arrays.concatenate; import android.util.SparseArray; -import com.google.common.primitives.Bytes; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -102,7 +100,7 @@ public void changeCode(CodeType type, byte[] currentCode, byte[] newCode) throws selectMainAid(); } try { - reader.transmit(0x00, 0x24, 0x00, Objects.requireNonNull(VERIFY_PIN_MAP.get(type)), Bytes.concat(code(currentCode), code(newCode)), null); + reader.transmit(0x00, 0x24, 0x00, Objects.requireNonNull(VERIFY_PIN_MAP.get(type)), concatenate(code(currentCode), code(newCode)), null); } catch (ApduResponseException e) { handleApduResponseException(type, e); } @@ -148,7 +146,7 @@ public byte[] decrypt(byte[] pin1, byte[] data, boolean ecc) throws SmartCardRea byte[] prefix = new byte[] {0x00}; verifyCode(CodeType.PIN1, pin1); reader.transmit(0x00, 0x22, 0x41, 0xB8, new byte[] {(byte) 0x80, 0x04, (byte) 0xFF, 0x30, 0x04, 0x00, (byte) 0x84, 0x01, (byte) 0x81}, null); - return reader.transmit(0x00, 0x2A, 0x80, 0x86, concat(prefix, data), 0x00); + return reader.transmit(0x00, 0x2A, 0x80, 0x86, concatenate(prefix, data), 0x00); } private void verifyCode(CodeType type, byte[] code) throws SmartCardReaderException { diff --git a/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/IdemiaWithPace.java b/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/IdemiaWithPace.java index 92dfdf7..8d610d1 100644 --- a/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/IdemiaWithPace.java +++ b/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/IdemiaWithPace.java @@ -3,7 +3,7 @@ package ee.ria.DigiDoc.idcard; -import static com.google.common.primitives.Bytes.concat; +import static org.bouncycastle.util.Arrays.concatenate; import android.annotation.SuppressLint; @@ -203,7 +203,7 @@ private byte[] getGAGetNonce() throws SmartCardReaderException { */ private byte[] getGAMapNonce(byte[] publicKey) throws SmartCardReaderException { byte[] prefix = new byte[] {0x7c, 0x43, (byte)0x81, 0x41}; - byte[] data = concat(prefix, publicKey); + byte[] data = concatenate(prefix, publicKey); return reader.transmit(CLA_CHAIN, INS_GA, 0x00, 0x00, data, 0x00); } @@ -216,7 +216,7 @@ private byte[] getGAMapNonce(byte[] publicKey) throws SmartCardReaderException { */ private byte[] getGAKeyAgreement(byte[] publicKey) throws SmartCardReaderException { byte[] prefix = new byte[] {0x7c, 0x43, (byte)0x83, 0x41}; - byte[] data = concat(prefix, publicKey); + byte[] data = concatenate(prefix, publicKey); return reader.transmit(CLA_CHAIN, INS_GA, 0x00, 0x00, data, 0x00); } @@ -228,7 +228,7 @@ private byte[] getGAKeyAgreement(byte[] publicKey) throws SmartCardReaderExcepti */ private byte[] getGAMutualAuthentication(byte[] mac) throws SmartCardReaderException { byte[] prefix = new byte[] {0x7C, 0x0A, (byte)0x85, 0x08}; - byte[] data = concat(prefix, mac); + byte[] data = concatenate(prefix, mac); return reader.transmit(CLA_ISO, INS_GA, 0x00, 0x00, data, 0x00); } @@ -241,7 +241,7 @@ private byte[] getDataForMac(byte[] publicKey) { byte[] prefix = new byte[] { 0x7f, 0x49, 0x4f, 0x06, 0x0a, 0x04, 0x00, 0x7f, 0x00, 0x07, 0x02, 0x02, 0x04, 0x02, 0x04, (byte)0x86, 0x41}; - return concat(prefix, publicKey); + return concatenate(prefix, publicKey); } /** @@ -253,7 +253,7 @@ private byte[] getDataForMac(byte[] publicKey) { */ private byte[] createKey(byte[] basis, byte lastByte) throws NoSuchAlgorithmException { byte[] padding = new byte[] {0x00, 0x00, 0x00, lastByte}; - byte[] padded = concat(basis, padding); + byte[] padded = concatenate(basis, padding); MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); return messageDigest.digest(padded); } @@ -284,7 +284,7 @@ private byte[] decryptNonce(byte[] encryptedNonce, byte[] CAN) throws * @return MAC */ private byte[] getMAC(byte[] data, byte[] keyMAC) { - BlockCipher blockCipher = new AESEngine(); + BlockCipher blockCipher = AESEngine.newInstance(); CMac cmac = new CMac(blockCipher); cmac.init(new KeyParameter(keyMAC)); cmac.update(data, 0, data.length); @@ -536,14 +536,14 @@ public byte[] encryptAndMac(int cla, int ins, int p1, int p2, byte[] data, Integ newLength += (byte) do97.length; newLength += (byte) do8e.length; - byte[] result = concat( + byte[] result = concatenate(new byte[][] { maskedHeader, new byte[] {newLength}, do8587, do97, do8e, new byte[] {0x00} - ); + }); incrementSSC(ssc); LoggingUtil.Companion.debugLog(TAG, String.format("Encrypted C-APDU: %s", toHexString(result)), null); return result; @@ -560,12 +560,12 @@ public byte[] encryptAndMac(int cla, int ins, int p1, int p2, byte[] data, Integ @NonNull private byte[] getDo8e(byte[] maskedHeader, byte[] do8587, byte[] do97) { byte[] paddedMaskedHeader = pad(maskedHeader, BLOCK_SIZE); - byte[] macData = concat(ssc, paddedMaskedHeader, do8587, do97); + byte[] macData = concatenate(ssc, paddedMaskedHeader, do8587, do97); byte[] paddedMacData = macData; if (macData.length % BLOCK_SIZE != 0) { paddedMacData = pad(macData, BLOCK_SIZE); } - return concat(new byte[] {DO8E, MAC_LENGTH}, getMAC(paddedMacData, keyMAC)); + return concatenate(new byte[] {DO8E, MAC_LENGTH}, getMAC(paddedMacData, keyMAC)); } /** @@ -607,10 +607,10 @@ private byte[] getDo8587(int ins, byte[] data) throws paddedData = pad(data, BLOCK_SIZE); byte[] dataEncrypted = encryptData(paddedData); if (ins % 2 == 0) { - do8587 = concat(new byte[]{DO87, (byte) (dataEncrypted.length + 1), 0x01}, + do8587 = concatenate(new byte[]{DO87, (byte) (dataEncrypted.length + 1), 0x01}, dataEncrypted); } else { - do8587 = concat(new byte[]{DO85, (byte) dataEncrypted.length}, dataEncrypted); + do8587 = concatenate(new byte[]{DO85, (byte) dataEncrypted.length}, dataEncrypted); } } return do8587; @@ -690,7 +690,7 @@ public byte[] decryptAndVerify(byte[] response) throws currentByte += MAC_LENGTH; byte[] rdata = Arrays.copyOfRange(response, 0, macStart); - byte[] macData = pad(concat(ssc, rdata), BLOCK_SIZE); + byte[] macData = pad(concatenate(ssc, rdata), BLOCK_SIZE); byte[] ourMac = getMAC(macData, keyMAC); LoggingUtil.Companion.debugLog(TAG, String.format("Card MAC: %s, our MAC: %s", Hex.toHexString(cardMac), Hex.toHexString(ourMac)), null); diff --git a/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/PersonalData.java b/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/PersonalData.java index 76e5875..ba317a4 100644 --- a/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/PersonalData.java +++ b/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/PersonalData.java @@ -3,38 +3,29 @@ package ee.ria.DigiDoc.idcard; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.auto.value.AutoValue; - import java.time.LocalDate; /** * Personal data file contents. */ -@AutoValue -public abstract class PersonalData { - - public abstract String surname(); - - public abstract String givenNames(); - - public abstract String citizenship(); - - @Nullable public abstract LocalDate dateOfBirth(); - - public abstract String personalCode(); - - public abstract String documentNumber(); - - @Nullable public abstract LocalDate expiryDate(); - - public abstract CardType cardType(); - - static PersonalData create(String surname, String givenNames, String citizenship, - @Nullable LocalDate dateOfBirth, String personalCode, - String documentNumber, @Nullable LocalDate expiryDate, CardType cardType) { - return new AutoValue_PersonalData(surname, givenNames, citizenship, dateOfBirth, +public record PersonalData( + @NonNull String surname, + @NonNull String givenNames, + @NonNull String citizenship, + @Nullable LocalDate dateOfBirth, + @NonNull String personalCode, + @NonNull String documentNumber, + @Nullable LocalDate expiryDate, + @NonNull CardType cardType) { + + static PersonalData create(@NonNull String surname, @NonNull String givenNames, + @NonNull String citizenship, @Nullable LocalDate dateOfBirth, + @NonNull String personalCode, @NonNull String documentNumber, + @Nullable LocalDate expiryDate, @NonNull CardType cardType) { + return new PersonalData(surname, givenNames, citizenship, dateOfBirth, personalCode, documentNumber, expiryDate, cardType); } } diff --git a/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/Thales.java b/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/Thales.java index e7e6d20..bb2735b 100644 --- a/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/Thales.java +++ b/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/Thales.java @@ -3,7 +3,7 @@ package ee.ria.DigiDoc.idcard; -import static com.google.common.primitives.Bytes.concat; +import static org.bouncycastle.util.Arrays.concatenate; import static ee.ria.DigiDoc.idcard.TLV.parseTLVRecursive; import android.util.SparseArray; @@ -131,7 +131,7 @@ public void changeCode(CodeType type, byte[] currentCode, byte[] newCode) throws throw new SmartCardReaderException("Cannot change PUK code"); } try { - reader.transmit(0x00, 0x24, 0x00, Objects.requireNonNull(VERIFY_PIN_MAP.get(type)), concat(code(currentCode), code(newCode)), null); + reader.transmit(0x00, 0x24, 0x00, Objects.requireNonNull(VERIFY_PIN_MAP.get(type)), concatenate(code(currentCode), code(newCode)), null); } catch (ApduResponseException e) { handleApduResponseException(type, e); } @@ -143,7 +143,7 @@ public void unblockAndChangeCode(byte[] pukCode, CodeType type, byte[] newCode) throw new SmartCardReaderException("Cannot unblock and change PUK code"); } try { - reader.transmit(0x00, 0x2C, pukCode == null ? 0x02 : 0x00, Objects.requireNonNull(VERIFY_PIN_MAP.get(type)), concat(code(pukCode), code(newCode)), null); + reader.transmit(0x00, 0x2C, pukCode == null ? 0x02 : 0x00, Objects.requireNonNull(VERIFY_PIN_MAP.get(type)), concatenate(code(pukCode), code(newCode)), null); } catch (ApduResponseException e) { handleApduResponseException(CodeType.PUK, e); } @@ -151,7 +151,7 @@ public void unblockAndChangeCode(byte[] pukCode, CodeType type, byte[] newCode) private void setSecEnv(byte mode, byte[] algo, byte keyRef) throws SmartCardReaderException { byte[] data = algo != null ? TLV.encodeTLV(0x80, algo) : new byte[] {}; - reader.transmit(0x00, 0x22, 0x41, mode, concat(data, TLV.encodeTLV(0x84, new byte[] {keyRef})), null); + reader.transmit(0x00, 0x22, 0x41, mode, concatenate(data, TLV.encodeTLV(0x84, new byte[] {keyRef})), null); } private byte[] sign(CodeType type, byte[] pin, byte keyRef, byte[] hash) throws SmartCardReaderException { @@ -180,7 +180,7 @@ public byte[] decrypt(byte[] pin1, byte[] data, boolean ecc) throws SmartCardRea verifyCode(CodeType.PIN1, pin1); setSecEnv((byte) 0xB8, null, (byte) 0x01); byte[] prefix = new byte[] {0x00}; - return reader.transmit(0x00, 0x2A, 0x80, 0x86, concat(prefix, data), 0x00); + return reader.transmit(0x00, 0x2A, 0x80, 0x86, concatenate(prefix, data), 0x00); } private void verifyCode(CodeType type, byte[] code) throws SmartCardReaderException { diff --git a/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/ThalesWithPace.java b/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/ThalesWithPace.java index b77d91f..debb130 100644 --- a/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/ThalesWithPace.java +++ b/libs/id-card-lib/src/main/java/ee/ria/DigiDoc/idcard/ThalesWithPace.java @@ -3,7 +3,7 @@ package ee.ria.DigiDoc.idcard; -import static com.google.common.primitives.Bytes.concat; +import static org.bouncycastle.util.Arrays.concatenate; import static ee.ria.DigiDoc.idcard.TLV.encodeTLV; import android.annotation.SuppressLint; @@ -238,7 +238,7 @@ private byte[] getDataForMac(byte[] publicKey) { byte[] prefix = new byte[] { 0x7f, 0x49, 0x6f, 0x06, 0x0a, 0x04, 0x00, 0x7f, 0x00, 0x07, 0x02, 0x02, 0x04, 0x02, 0x04, (byte)0x86, 0x61}; - return concat(prefix, publicKey); + return concatenate(prefix, publicKey); } /** @@ -250,7 +250,7 @@ private byte[] getDataForMac(byte[] publicKey) { */ private byte[] createKey(byte[] basis, byte lastByte) throws NoSuchAlgorithmException { byte[] padding = new byte[] {0x00, 0x00, 0x00, lastByte}; - byte[] padded = concat(basis, padding); + byte[] padded = concatenate(basis, padding); MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); return messageDigest.digest(padded); } @@ -281,7 +281,7 @@ private byte[] decryptNonce(byte[] encryptedNonce, byte[] CAN) throws * @return MAC */ private byte[] getMAC(byte[] data, byte[] keyMAC) { - BlockCipher blockCipher = new AESEngine(); + BlockCipher blockCipher = AESEngine.newInstance(); CMac cmac = new CMac(blockCipher); cmac.init(new KeyParameter(keyMAC)); cmac.update(data, 0, data.length); @@ -530,14 +530,14 @@ public byte[] encryptAndMac(int cla, int ins, int p1, int p2, byte[] data, Integ newLength += (byte) do97.length; newLength += (byte) do8e.length; - byte[] result = concat( + byte[] result = concatenate(new byte[][] { maskedHeader, new byte[] {newLength}, do8587, do97, do8e, new byte[] {0x00} - ); + }); incrementSSC(ssc); LoggingUtil.Companion.debugLog(TAG, String.format("Encrypted C-APDU: %s", toHexString(result)), null); return result; @@ -554,12 +554,12 @@ public byte[] encryptAndMac(int cla, int ins, int p1, int p2, byte[] data, Integ @NonNull private byte[] getDo8e(byte[] maskedHeader, byte[] do8587, byte[] do97) { byte[] paddedMaskedHeader = pad(maskedHeader, BLOCK_SIZE); - byte[] macData = concat(ssc, paddedMaskedHeader, do8587, do97); + byte[] macData = concatenate(ssc, paddedMaskedHeader, do8587, do97); byte[] paddedMacData = macData; if (macData.length % BLOCK_SIZE != 0) { paddedMacData = pad(macData, BLOCK_SIZE); } - return concat(new byte[] {DO8E, MAC_LENGTH}, getMAC(paddedMacData, keyMAC)); + return concatenate(new byte[] {DO8E, MAC_LENGTH}, getMAC(paddedMacData, keyMAC)); } /** @@ -601,10 +601,10 @@ private byte[] getDo8587(int ins, byte[] data) throws paddedData = pad(data, BLOCK_SIZE); byte[] dataEncrypted = encryptData(paddedData); if (ins % 2 == 0) { - do8587 = concat(new byte[]{DO87, (byte) (dataEncrypted.length + 1), 0x01}, + do8587 = concatenate(new byte[]{DO87, (byte) (dataEncrypted.length + 1), 0x01}, dataEncrypted); } else { - do8587 = concat(new byte[]{DO85, (byte) dataEncrypted.length}, dataEncrypted); + do8587 = concatenate(new byte[]{DO85, (byte) dataEncrypted.length}, dataEncrypted); } } return do8587; @@ -685,7 +685,7 @@ public byte[] decryptAndVerify(byte[] response) throws currentByte += MAC_LENGTH; byte[] rdata = Arrays.copyOfRange(response, 0, macStart); - byte[] macData = pad(concat(ssc, rdata), BLOCK_SIZE); + byte[] macData = pad(concatenate(ssc, rdata), BLOCK_SIZE); byte[] ourMac = getMAC(macData, keyMAC); LoggingUtil.Companion.debugLog(TAG, String.format("Card MAC: %s, our MAC: %s", Hex.toHexString(cardMac), Hex.toHexString(ourMac)), null); diff --git a/libs/smart-card-reader-lib/build.gradle.kts b/libs/smart-card-reader-lib/build.gradle.kts index 567de66..f64bb05 100644 --- a/libs/smart-card-reader-lib/build.gradle.kts +++ b/libs/smart-card-reader-lib/build.gradle.kts @@ -7,31 +7,35 @@ plugins { android { namespace = "ee.ria.DigiDoc.smartcardreader" - compileSdk = 36 + compileSdk = 37 defaultConfig { - minSdk = 26 + minSdk = 34 } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + testOptions { + unitTests.all { + it.useJUnitPlatform() + } } - lint { - lintConfig = file("../lint.xml") + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } } dependencies { implementation(libs.androidx.annotation) implementation(libs.bcprov.jdk18on) - implementation(libs.guava) implementation(project(":libs:card-utils-lib")) - testImplementation(libs.hamcrest) - testImplementation(libs.junit) + testImplementation(platform(libs.junit.bom)) + testImplementation(libs.junit.jupiter) + testRuntimeOnly(libs.junit.platform.launcher) testImplementation(libs.mockito.core) - testImplementation(libs.truth) + testImplementation(libs.truth) { + exclude(group = "junit", module = "junit") + } } \ No newline at end of file diff --git a/libs/smart-card-reader-lib/src/main/java/ee/ria/DigiDoc/smartcardreader/SmartCardReader.java b/libs/smart-card-reader-lib/src/main/java/ee/ria/DigiDoc/smartcardreader/SmartCardReader.java index 01de057..f6fc461 100644 --- a/libs/smart-card-reader-lib/src/main/java/ee/ria/DigiDoc/smartcardreader/SmartCardReader.java +++ b/libs/smart-card-reader-lib/src/main/java/ee/ria/DigiDoc/smartcardreader/SmartCardReader.java @@ -3,7 +3,7 @@ package ee.ria.DigiDoc.smartcardreader; -import static com.google.common.primitives.Bytes.concat; +import static org.bouncycastle.util.Arrays.concatenate; import static java.util.Arrays.copyOf; import static java.util.Arrays.copyOfRange; @@ -44,7 +44,7 @@ public byte[] transmit(int cla, int ins, int p1, int p2, byte[] data, Integer le le)); } else if (data.length < 256) { response = transmit(SmartCardReader.appendLe( - concat( + concatenate( new byte[] {(byte) cla, (byte) ins, (byte) p1, (byte) p2, (byte) data.length}, data), @@ -53,7 +53,7 @@ public byte[] transmit(int cla, int ins, int p1, int p2, byte[] data, Integer le int remaining = data.length; while (remaining >= 256) { transmit(SmartCardReader.appendLe( - concat( + concatenate( new byte[] {0x10, (byte) ins, (byte) p1, (byte) p2, (byte) 0xFF}, copyOfRange(data, data.length - remaining, data.length - remaining + 255)), @@ -61,7 +61,7 @@ public byte[] transmit(int cla, int ins, int p1, int p2, byte[] data, Integer le remaining -= 255; } response = transmit(SmartCardReader.appendLe( - concat( + concatenate( new byte[] {(byte) cla, (byte) ins, (byte) p1, (byte) p2, (byte) remaining}, copyOfRange(data, data.length - remaining, data.length)), @@ -73,7 +73,7 @@ public byte[] transmit(int cla, int ins, int p1, int p2, byte[] data, Integer le if (sw1 == (byte) 0x90 && sw2 == 0x00) { return copyOf(response, response.length - 2); } else if (sw1 == 0x61) { - return concat( + return concatenate( copyOf(response, response.length - 2), transmit(0x00, 0xC0, 0x00, 0x00, null, (int) sw2)); } @@ -84,7 +84,7 @@ protected static byte[] appendLe(byte[] apdu, Integer le) { if (le == null) { return apdu; } else { - return concat(apdu, new byte[] {le.byteValue()}); + return concatenate(apdu, new byte[] {le.byteValue()}); } } } diff --git a/libs/smart-card-reader-lib/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/NfcSmartCardReader.java b/libs/smart-card-reader-lib/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/NfcSmartCardReader.java index 84bd324..cbbfe87 100644 --- a/libs/smart-card-reader-lib/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/NfcSmartCardReader.java +++ b/libs/smart-card-reader-lib/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/NfcSmartCardReader.java @@ -136,9 +136,7 @@ public byte[] transmit(int cla, int ins, int p1, int p2, byte[] data, Integer le try { // Encryption of the C-APDU byte[] response; - if (data == null || data.length == 0) { - response = transmit(apduEncryptor.encryptAndMac(cla, ins, p1, p2, data, le)); - } else if (data.length < 256) { + if (data == null || data.length < 256) { response = transmit(apduEncryptor.encryptAndMac(cla, ins, p1, p2, data, le)); } else { int remaining = data.length; diff --git a/libs/smart-card-reader-lib/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/NfcSmartCardReaderManager.java b/libs/smart-card-reader-lib/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/NfcSmartCardReaderManager.java index f780a6a..c34258f 100644 --- a/libs/smart-card-reader-lib/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/NfcSmartCardReaderManager.java +++ b/libs/smart-card-reader-lib/src/main/java/ee/ria/DigiDoc/smartcardreader/nfc/NfcSmartCardReaderManager.java @@ -109,8 +109,9 @@ public void onTagDiscovered(Tag tag) { clientCallback.onNfcReader(reader, ex); } - assert reader != null; - reader.close(); + if (reader != null) { + reader.close(); + } disableNfcReaderMode(); } diff --git a/libs/smart-card-reader-lib/src/test/java/ee/ria/DigiDoc/smartcardreader/SmartCardReaderTest.java b/libs/smart-card-reader-lib/src/test/java/ee/ria/DigiDoc/smartcardreader/SmartCardReaderTest.java index 9a458da..25d0a24 100644 --- a/libs/smart-card-reader-lib/src/test/java/ee/ria/DigiDoc/smartcardreader/SmartCardReaderTest.java +++ b/libs/smart-card-reader-lib/src/test/java/ee/ria/DigiDoc/smartcardreader/SmartCardReaderTest.java @@ -4,31 +4,29 @@ package ee.ria.DigiDoc.smartcardreader; import static com.google.common.truth.Truth.assertThat; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.mockito.Mock; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.MockMakers; @SuppressWarnings("ResultOfMethodCallIgnored") public final class SmartCardReaderTest { - @Rule public final ExpectedException exception = ExpectedException.none(); - - @Mock(mockMaker = MockMakers.SUBCLASS) private SmartCardReader smartCardReader; - @Before + @BeforeEach public void before() throws Exception { - smartCardReader = mock(SmartCardReader.class, withSettings().mockMaker(MockMakers.SUBCLASS)); + smartCardReader = mock(SmartCardReader.class, withSettings() + .mockMaker(MockMakers.SUBCLASS) + .defaultAnswer(CALLS_REAL_METHODS)); when(smartCardReader.transmit(any())) .thenReturn(new byte[] {(byte) 0x90, 0x00}); } @@ -734,8 +732,10 @@ public void transmit_responseDataNot9000Nor61XX() throws Exception { when(smartCardReader.transmit(any())) .thenReturn(new byte[] {0x67, 0x69}); - exception.expect(equalTo(new ApduResponseException((byte) 0x67, (byte) 0x69))); + ApduResponseException thrown = assertThrows( + ApduResponseException.class, + () -> smartCardReader.transmit(0x00, 0x00, 0x00, 0x00, null, null)); - smartCardReader.transmit(0x00, 0x00, 0x00, 0x00, null, null); + assertEquals(new ApduResponseException((byte) 0x67, (byte) 0x69), thrown); } }