From 6d5d24877713fce2c784393bbb4d13b28b739ae1 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Tue, 13 Jan 2026 15:21:51 +0100 Subject: [PATCH 01/16] feat: add MIFARE Classic support --- gradle.properties | 2 +- plugin/build.gradle.kts | 2 +- plugin/src/main/kdoc/overview.md | 45 +++++++++- .../plugin/android/nfc/AndroidNfcConfig.kt | 5 +- .../android/nfc/AndroidNfcReaderAdapter.kt | 82 +++++++++++++++++-- .../nfc/AndroidNfcSupportedProtocols.kt | 10 ++- .../keyple/plugin/android/nfc/KeyProvider.kt | 28 +++++++ 7 files changed, 160 insertions(+), 14 deletions(-) create mode 100644 plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/KeyProvider.kt diff --git a/gradle.properties b/gradle.properties index 90a0e53..b42d8f0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ group = org.eclipse.keyple title = Keyple Plugin Android NFC Java Lib description = Keyple add-on to manage Android NFC readers -version = 3.1.1-SNAPSHOT +version = 3.2.0-SNAPSHOT # Java Configuration javaSourceLevel = 1.8 diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index fb88826..374d5de 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -20,7 +20,7 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib:1.7.20") implementation("org.eclipse.keyple:keyple-common-java-api:2.0.2") implementation("org.eclipse.keyple:keyple-plugin-java-api:2.3.2") - api("org.eclipse.keyple:keyple-plugin-storagecard-java-api:1.0.0") + api("org.eclipse.keyple:keyple-plugin-storagecard-java-api:1.1.0-SNAPSHOT") implementation("org.eclipse.keyple:keyple-util-java-lib:2.4.0") implementation("org.slf4j:slf4j-api:1.7.32") } diff --git a/plugin/src/main/kdoc/overview.md b/plugin/src/main/kdoc/overview.md index f2ddff9..8fa8552 100644 --- a/plugin/src/main/kdoc/overview.md +++ b/plugin/src/main/kdoc/overview.md @@ -10,6 +10,45 @@ To properly detect card presence and handle interactions, it is **mandatory** to objects as [ObservableReader] and implement the appropriate interfaces from the Keyple Service SPI package. -Since NFC events are managed asynchronously by the Android system, **working without observation is -not possible** in this context. Any integration of the Android NFC plugin must therefore be designed -around this event-driven approach. \ No newline at end of file +Since NFC events are managed asynchronously by the Android system, not be possible** in this context. Any integration of the Android NFC plugin must therefore be designed +around this event-driven approach. + +### Configuration & Instantiation + +To create an instance of the plugin, use the [AndroidNfcPluginFactoryProvider] which requires an +[AndroidNfcConfig] object. + +The configuration object allows you to customize the plugin behavior, including: +* [AndroidNfcConfig.activity]: The Android Activity context (mandatory). +* [AndroidNfcConfig.isPlatformSoundEnabled]: To enable/disable system sounds on tag discovery. +* [AndroidNfcConfig.skipNdefCheck]: To optimize detection speed by skipping NDEF checks. +* [AndroidNfcConfig.cardInsertionPollingInterval] & [AndroidNfcConfig.cardRemovalPollingInterval]: + To fine-tune presence check timings. + +```kotlin +val config = AndroidNfcConfig(activity = this) +val pluginFactory = AndroidNfcPluginFactoryProvider.provideFactory(config) +val plugin = pluginFactory.createPlugin() +``` + +### Storage Card Support + +To support storage cards (such as **Mifare Classic**, **Mifare Ultralight**, etc.), the plugin relies +on the `keyple-plugin-storagecard-java-api`. An implementation of `ApduInterpreterFactory` must be +provided via the [AndroidNfcConfig.apduInterpreterFactory] property. + +This factory is responsible for creating the interpreter that translates standard APDU commands into +specific Android NFC I/O operations (e.g. using `MifareClassic` or `MifareUltralight` Android tech +classes). + +#### Key Management (Mifare Classic) + +When using Mifare Classic cards, authentication requires keys. Since Keyple separates the "load key" +operation from the "authenticate" operation, the plugin emulates a reader's key memory: +* **Volatile**: For session keys (cleared when the card is removed). +* **Persistent**: For keys that must survive the card session (kept as long as the plugin is active). + +For enhanced security, the application can implement the [KeyProvider] interface and register it +in [AndroidNfcConfig.keyProvider]. This mechanism allows the application to keep master keys in a +secure vault (e.g. Android Keystore, HSM, or encrypted file) and provide them to the plugin only +when strictly requested during an authentication operation. \ No newline at end of file diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcConfig.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcConfig.kt index 51bcb45..7c933f7 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcConfig.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcConfig.kt @@ -30,6 +30,8 @@ import org.eclipse.keyple.core.plugin.storagecard.ApduInterpreterFactory * behavior (corresponds to EXTRA_READER_PRESENCE_CHECK_DELAY). * @property cardRemovalPollingInterval (optional, default value: `100`) Delay (in milliseconds) for * performing presence checks while waiting for card removal. + * @property keyProvider (optional, default value: `null`) A provider for retrieving persistent keys + * during authentication. * @since 3.0.0 */ data class AndroidNfcConfig( @@ -38,5 +40,6 @@ data class AndroidNfcConfig( val isPlatformSoundEnabled: Boolean = true, val skipNdefCheck: Boolean = true, val cardInsertionPollingInterval: Int = 0, - val cardRemovalPollingInterval: Int = 100 + val cardRemovalPollingInterval: Int = 100, + val keyProvider: KeyProvider? = null ) diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index 132a876..235c5f0 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -14,6 +14,7 @@ package org.eclipse.keyple.plugin.android.nfc import android.nfc.NfcAdapter import android.nfc.Tag import android.nfc.tech.IsoDep +import android.nfc.tech.MifareClassic import android.nfc.tech.MifareUltralight import android.nfc.tech.NfcA import android.nfc.tech.NfcB @@ -21,6 +22,7 @@ import android.nfc.tech.TagTechnology import android.os.Bundle import android.os.Handler import android.os.Looper +import android.util.SparseArray import org.eclipse.keyple.core.plugin.CardIOException import org.eclipse.keyple.core.plugin.CardInsertionWaiterAsynchronousApi import org.eclipse.keyple.core.plugin.ReaderIOException @@ -51,6 +53,9 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : private val handler = Handler(Looper.getMainLooper()) private val syncWaitRemoval = Object() private val apduInterpreter: ApduInterpreterSpi? + private val volatileKeyRegistry = SparseArray() + private val persistentKeyRegistry = SparseArray() + private val keyProvider: KeyProvider? = config.keyProvider private var flags: Int private var tagTechnology: TagTechnology? = null @@ -64,6 +69,8 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : private companion object { private const val MIFARE_ULTRALIGHT_READ_SIZE = 16 + private const val MIFARE_KEY_A = 0x60 + private const val MIFARE_KEY_B = 0x61 } init { @@ -98,6 +105,7 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : try { tagTechnology!!.connect() isCardChannelOpen = true + volatileKeyRegistry.clear() } catch (e: Exception) { throw CardIOException("Error while opening physical channel", e) } @@ -105,6 +113,7 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : override fun closePhysicalChannel() { isCardChannelOpen = false + volatileKeyRegistry.clear() } override fun isPhysicalChannelOpen(): Boolean { @@ -144,7 +153,8 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : when (AndroidNfcSupportedProtocols.valueOf(readerProtocol)) { AndroidNfcSupportedProtocols.ISO_14443_4 -> NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_NFC_A - AndroidNfcSupportedProtocols.MIFARE_ULTRALIGHT -> NfcAdapter.FLAG_READER_NFC_A + AndroidNfcSupportedProtocols.MIFARE_ULTRALIGHT, + AndroidNfcSupportedProtocols.MIFARE_CLASSIC -> NfcAdapter.FLAG_READER_NFC_A } } @@ -154,7 +164,8 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : when (AndroidNfcSupportedProtocols.valueOf(readerProtocol)) { AndroidNfcSupportedProtocols.ISO_14443_4 -> (NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_NFC_A).inv() - AndroidNfcSupportedProtocols.MIFARE_ULTRALIGHT -> NfcAdapter.FLAG_READER_NFC_A.inv() + AndroidNfcSupportedProtocols.MIFARE_ULTRALIGHT, + AndroidNfcSupportedProtocols.MIFARE_CLASSIC -> NfcAdapter.FLAG_READER_NFC_A.inv() } } @@ -233,16 +244,27 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : return uid } - override fun readBlock(blockNumber: Int, length: Int): ByteArray { + override fun readBlock(blockAddress: Int, length: Int): ByteArray { + if (tagTechnology is MifareClassic) { + val classic = tagTechnology as MifareClassic + val readData = classic.readBlock(blockAddress) + // Mifare Classic always reads 16 bytes. + return if (length < 16) { + readData.copyOf(length) + } else { + readData + } + } + require(length % MifareUltralight.PAGE_SIZE == 0) { "Requested length ($length) must be a multiple of PAGE_SIZE (${MifareUltralight.PAGE_SIZE})." } - require(blockNumber >= 0) { "Block number must be non-negative." } + require(blockAddress >= 0) { "Block number must be non-negative." } require(length <= MIFARE_ULTRALIGHT_READ_SIZE) { "Requested length ($length) exceeds maximum readable size 16 in a single operation." } val ultralight = tagTechnology as MifareUltralight - val readData = ultralight.readPages(blockNumber) + val readData = ultralight.readPages(blockAddress) return if (length < MIFARE_ULTRALIGHT_READ_SIZE) { readData.copyOf(length) } else { @@ -250,13 +272,55 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : } } - override fun writeBlock(blockNumber: Int, data: ByteArray?) { - (tagTechnology as MifareUltralight).writePage(blockNumber, data) + override fun writeBlock(blockAddress: Int, data: ByteArray) { + if (tagTechnology is MifareClassic) { + require(data.size == 16) { + "Data must be exactly 16 bytes for Mifare Classic write operations." + } + (tagTechnology as MifareClassic).writeBlock(blockAddress, data) + } else { + (tagTechnology as MifareUltralight).writePage(blockAddress, data) + } + } + + override fun loadKey(isVolatileMemory: Boolean, keyNumber: Int, key: ByteArray) { + if (isVolatileMemory) { + volatileKeyRegistry.put(keyNumber, key.copyOf()) + } else { + persistentKeyRegistry.put(keyNumber, key.copyOf()) + } + } + + override fun generalAuthenticate(blockAddress: Int, keyType: Int, keyNumber: Int) { + if (tagTechnology !is MifareClassic) { + throw CardIOException("General Authenticate is only supported for Mifare Classic.") + } + val classic = tagTechnology as MifareClassic + val key = + volatileKeyRegistry.get(keyNumber) + ?: persistentKeyRegistry.get(keyNumber) + ?: keyProvider?.getKey(keyNumber) + ?: throw IllegalArgumentException("No key found for key number: $keyNumber") + + val sectorIndex = classic.blockToSector(blockAddress) + val success = + when (keyType) { + MIFARE_KEY_A -> classic.authenticateSectorWithKeyA(sectorIndex, key) + MIFARE_KEY_B -> classic.authenticateSectorWithKeyB(sectorIndex, key) + else -> + throw IllegalArgumentException( + "Unsupported key type: 0x${Integer.toHexString(keyType)}") + } + + if (!success) { + throw CardIOException("Authentication failed for block $blockAddress (Sector $sectorIndex)") + } } override fun onTagDiscovered(tag: Tag) { logger.info("{}: card discovered: {}", name, tag) isCardChannelOpen = false + volatileKeyRegistry.clear() try { for (technology in tag.techList) when (technology) { IsoDep::class.qualifiedName -> { @@ -267,6 +331,10 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : currentCardProtocol = MifareUltralight::class.qualifiedName!! tagTechnology = MifareUltralight.get(tag) } + MifareClassic::class.qualifiedName -> { + currentCardProtocol = MifareClassic::class.qualifiedName!! + tagTechnology = MifareClassic.get(tag) + } NfcA::class.qualifiedName -> { val tagA = NfcA.get(tag) uid = tagA.tag.id diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcSupportedProtocols.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcSupportedProtocols.kt index 51aaad7..e88cc1e 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcSupportedProtocols.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcSupportedProtocols.kt @@ -12,6 +12,7 @@ package org.eclipse.keyple.plugin.android.nfc import android.nfc.tech.IsoDep +import android.nfc.tech.MifareClassic import android.nfc.tech.MifareUltralight /** @@ -33,7 +34,14 @@ enum class AndroidNfcSupportedProtocols(private val techId: String) { * * @since 3.1.0 */ - MIFARE_ULTRALIGHT(MifareUltralight::class.qualifiedName!!); + MIFARE_ULTRALIGHT(MifareUltralight::class.qualifiedName!!), + + /** + * NXP MIFARE Classic protocol. + * + * @since 3.2.0 + */ + MIFARE_CLASSIC(MifareClassic::class.qualifiedName!!); internal val androidNfcTechIdentifier: String get() = techId diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/KeyProvider.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/KeyProvider.kt new file mode 100644 index 0000000..59fb4ce --- /dev/null +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/KeyProvider.kt @@ -0,0 +1,28 @@ +/* ************************************************************************************** + * Copyright (c) 2025 Calypso Networks Association https://calypsonet.org/ + * + * See the NOTICE file(s) distributed with this work for additional information + * regarding copyright ownership. + * + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + ************************************************************************************** */ +package org.eclipse.keyple.plugin.android.nfc + +/** + * Interface allowing the application to provide authentication keys dynamically. + * + * @since 3.2.0 + */ +interface KeyProvider { + + /** + * Retrieves the key associated with the given key index. + * + * @param keyIndex The index of the key requested. + * @return The key as a byte array, or null if not found. + */ + fun getKey(keyIndex: Int): ByteArray? +} From 4e805a08857017e4448bb83ee0dbd007069a5e38 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 15 Jan 2026 11:01:27 +0100 Subject: [PATCH 02/16] feat: update Mifare Classic authentication and key management APIs - Updated `loadKey` to use `KeyStorageType` instead of a boolean. - Modified `generalAuthenticate` to return a boolean result and improved its internal logic for key retrieval and authentication. - Simplified `overview.md` by removing detailed configuration and key management sections. --- plugin/src/main/kdoc/overview.md | 30 ++------------- .../android/nfc/AndroidNfcReaderAdapter.kt | 37 ++++++++----------- 2 files changed, 18 insertions(+), 49 deletions(-) diff --git a/plugin/src/main/kdoc/overview.md b/plugin/src/main/kdoc/overview.md index 8fa8552..26be02d 100644 --- a/plugin/src/main/kdoc/overview.md +++ b/plugin/src/main/kdoc/overview.md @@ -10,7 +10,8 @@ To properly detect card presence and handle interactions, it is **mandatory** to objects as [ObservableReader] and implement the appropriate interfaces from the Keyple Service SPI package. -Since NFC events are managed asynchronously by the Android system, not be possible** in this context. Any integration of the Android NFC plugin must therefore be designed +Since NFC events are managed asynchronously by the Android system, **working without observation is +not possible** in this context. Any integration of the Android NFC plugin must therefore be designed around this event-driven approach. ### Configuration & Instantiation @@ -18,19 +19,6 @@ around this event-driven approach. To create an instance of the plugin, use the [AndroidNfcPluginFactoryProvider] which requires an [AndroidNfcConfig] object. -The configuration object allows you to customize the plugin behavior, including: -* [AndroidNfcConfig.activity]: The Android Activity context (mandatory). -* [AndroidNfcConfig.isPlatformSoundEnabled]: To enable/disable system sounds on tag discovery. -* [AndroidNfcConfig.skipNdefCheck]: To optimize detection speed by skipping NDEF checks. -* [AndroidNfcConfig.cardInsertionPollingInterval] & [AndroidNfcConfig.cardRemovalPollingInterval]: - To fine-tune presence check timings. - -```kotlin -val config = AndroidNfcConfig(activity = this) -val pluginFactory = AndroidNfcPluginFactoryProvider.provideFactory(config) -val plugin = pluginFactory.createPlugin() -``` - ### Storage Card Support To support storage cards (such as **Mifare Classic**, **Mifare Ultralight**, etc.), the plugin relies @@ -39,16 +27,4 @@ provided via the [AndroidNfcConfig.apduInterpreterFactory] property. This factory is responsible for creating the interpreter that translates standard APDU commands into specific Android NFC I/O operations (e.g. using `MifareClassic` or `MifareUltralight` Android tech -classes). - -#### Key Management (Mifare Classic) - -When using Mifare Classic cards, authentication requires keys. Since Keyple separates the "load key" -operation from the "authenticate" operation, the plugin emulates a reader's key memory: -* **Volatile**: For session keys (cleared when the card is removed). -* **Persistent**: For keys that must survive the card session (kept as long as the plugin is active). - -For enhanced security, the application can implement the [KeyProvider] interface and register it -in [AndroidNfcConfig.keyProvider]. This mechanism allows the application to keep master keys in a -secure vault (e.g. Android Keystore, HSM, or encrypted file) and provide them to the plugin only -when strictly requested during an authentication operation. \ No newline at end of file +classes). \ No newline at end of file diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index 235c5f0..d282619 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -31,6 +31,7 @@ import org.eclipse.keyple.core.plugin.spi.reader.observable.ObservableReaderSpi import org.eclipse.keyple.core.plugin.spi.reader.observable.state.insertion.CardInsertionWaiterAsynchronousSpi import org.eclipse.keyple.core.plugin.spi.reader.observable.state.removal.CardRemovalWaiterBlockingSpi import org.eclipse.keyple.core.plugin.storagecard.internal.CommandProcessorApi +import org.eclipse.keyple.core.plugin.storagecard.internal.KeyStorageType import org.eclipse.keyple.core.plugin.storagecard.internal.spi.ApduInterpreterFactorySpi import org.eclipse.keyple.core.plugin.storagecard.internal.spi.ApduInterpreterSpi import org.eclipse.keyple.core.util.HexUtil @@ -283,19 +284,18 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : } } - override fun loadKey(isVolatileMemory: Boolean, keyNumber: Int, key: ByteArray) { - if (isVolatileMemory) { - volatileKeyRegistry.put(keyNumber, key.copyOf()) - } else { - persistentKeyRegistry.put(keyNumber, key.copyOf()) - } + override fun loadKey(keyStorageType: KeyStorageType, keyNumber: Int, key: ByteArray) { + val targetRegistry = + if (keyStorageType == KeyStorageType.VOLATILE) volatileKeyRegistry + else persistentKeyRegistry + targetRegistry.put(keyNumber, key.copyOf()) } - override fun generalAuthenticate(blockAddress: Int, keyType: Int, keyNumber: Int) { - if (tagTechnology !is MifareClassic) { - throw CardIOException("General Authenticate is only supported for Mifare Classic.") - } - val classic = tagTechnology as MifareClassic + override fun generalAuthenticate(blockAddress: Int, keyType: Int, keyNumber: Int): Boolean { + val classic = + tagTechnology as? MifareClassic + ?: throw CardIOException("General Authenticate is only supported for Mifare Classic.") + val key = volatileKeyRegistry.get(keyNumber) ?: persistentKeyRegistry.get(keyNumber) @@ -303,17 +303,10 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : ?: throw IllegalArgumentException("No key found for key number: $keyNumber") val sectorIndex = classic.blockToSector(blockAddress) - val success = - when (keyType) { - MIFARE_KEY_A -> classic.authenticateSectorWithKeyA(sectorIndex, key) - MIFARE_KEY_B -> classic.authenticateSectorWithKeyB(sectorIndex, key) - else -> - throw IllegalArgumentException( - "Unsupported key type: 0x${Integer.toHexString(keyType)}") - } - - if (!success) { - throw CardIOException("Authentication failed for block $blockAddress (Sector $sectorIndex)") + return when (keyType) { + MIFARE_KEY_A -> classic.authenticateSectorWithKeyA(sectorIndex, key) + MIFARE_KEY_B -> classic.authenticateSectorWithKeyB(sectorIndex, key) + else -> throw IllegalArgumentException("Unsupported key type: 0x${keyType.toString(16)}") } } From 6e0f792a7128f66ce7f7852254bb0a23e1076a1b Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 15 Jan 2026 11:10:32 +0100 Subject: [PATCH 03/16] refactor: relocate `KeyProvider` to `spi` package and update dependencies --- plugin/build.gradle.kts | 2 +- .../keyple/plugin/android/nfc/{ => spi}/KeyProvider.kt | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) rename plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/{ => spi}/KeyProvider.kt (87%) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 374d5de..00575e2 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -20,7 +20,7 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib:1.7.20") implementation("org.eclipse.keyple:keyple-common-java-api:2.0.2") implementation("org.eclipse.keyple:keyple-plugin-java-api:2.3.2") - api("org.eclipse.keyple:keyple-plugin-storagecard-java-api:1.1.0-SNAPSHOT") + api("org.eclipse.keyple:keyple-plugin-storagecard-java-api:1.1.0-SNAPSHOT") { isChanging = true } implementation("org.eclipse.keyple:keyple-util-java-lib:2.4.0") implementation("org.slf4j:slf4j-api:1.7.32") } diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/KeyProvider.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/spi/KeyProvider.kt similarity index 87% rename from plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/KeyProvider.kt rename to plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/spi/KeyProvider.kt index 59fb4ce..65bb99f 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/KeyProvider.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/spi/KeyProvider.kt @@ -1,5 +1,5 @@ /* ************************************************************************************** - * Copyright (c) 2025 Calypso Networks Association https://calypsonet.org/ + * Copyright (c) 2026 Calypso Networks Association https://calypsonet.org/ * * See the NOTICE file(s) distributed with this work for additional information * regarding copyright ownership. @@ -9,7 +9,7 @@ * * SPDX-License-Identifier: EPL-2.0 ************************************************************************************** */ -package org.eclipse.keyple.plugin.android.nfc +package org.eclipse.keyple.plugin.android.nfc.spi /** * Interface allowing the application to provide authentication keys dynamically. @@ -23,6 +23,7 @@ interface KeyProvider { * * @param keyIndex The index of the key requested. * @return The key as a byte array, or null if not found. + * @since 3.2.0 */ fun getKey(keyIndex: Int): ByteArray? } From e5da03332266651922f831f5563c88936567d1b5 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 15 Jan 2026 11:26:15 +0100 Subject: [PATCH 04/16] refactor: rename `keyIndex` to `keyNumber` in `KeyProvider` interface --- .../eclipse/keyple/plugin/android/nfc/AndroidNfcConfig.kt | 5 +++-- .../keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt | 1 + .../eclipse/keyple/plugin/android/nfc/spi/KeyProvider.kt | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcConfig.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcConfig.kt index 7c933f7..2c41689 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcConfig.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcConfig.kt @@ -13,6 +13,7 @@ package org.eclipse.keyple.plugin.android.nfc import android.app.Activity import org.eclipse.keyple.core.plugin.storagecard.ApduInterpreterFactory +import org.eclipse.keyple.plugin.android.nfc.spi.KeyProvider /** * Configuration class holding all the plugin options. @@ -30,8 +31,8 @@ import org.eclipse.keyple.core.plugin.storagecard.ApduInterpreterFactory * behavior (corresponds to EXTRA_READER_PRESENCE_CHECK_DELAY). * @property cardRemovalPollingInterval (optional, default value: `100`) Delay (in milliseconds) for * performing presence checks while waiting for card removal. - * @property keyProvider (optional, default value: `null`) A provider for retrieving persistent keys - * during authentication. + * @property keyProvider (optional, default value: `null`) A provider for retrieving keys during + * authentication. * @since 3.0.0 */ data class AndroidNfcConfig( diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index d282619..16a727e 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -35,6 +35,7 @@ import org.eclipse.keyple.core.plugin.storagecard.internal.KeyStorageType import org.eclipse.keyple.core.plugin.storagecard.internal.spi.ApduInterpreterFactorySpi import org.eclipse.keyple.core.plugin.storagecard.internal.spi.ApduInterpreterSpi import org.eclipse.keyple.core.util.HexUtil +import org.eclipse.keyple.plugin.android.nfc.spi.KeyProvider import org.json.JSONObject import org.slf4j.LoggerFactory diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/spi/KeyProvider.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/spi/KeyProvider.kt index 65bb99f..24e41f2 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/spi/KeyProvider.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/spi/KeyProvider.kt @@ -19,11 +19,11 @@ package org.eclipse.keyple.plugin.android.nfc.spi interface KeyProvider { /** - * Retrieves the key associated with the given key index. + * Retrieves the key associated with the given key number. * - * @param keyIndex The index of the key requested. + * @param keyNumber The number of the key requested. * @return The key as a byte array, or null if not found. * @since 3.2.0 */ - fun getKey(keyIndex: Int): ByteArray? + fun getKey(keyNumber: Int): ByteArray? } From e06498f02023b6fb86e901a9ec12222da5f469bf Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 15 Jan 2026 11:39:00 +0100 Subject: [PATCH 05/16] refactor: simplify key management in `AndroidNfcReaderAdapter` - Replace `volatileKeyRegistry` and `persistentKeyRegistry` `SparseArray`s with a single `loadedKey` byte array. - Update `loadKey` to store only the most recently provided key. - Update `generalAuthenticate` to use the `loadedKey` (clearing it after use) or fallback to the `keyProvider`. - Ensure `loadedKey` is cleared when the physical channel is opened, closed, or a new tag is discovered. --- .../android/nfc/AndroidNfcReaderAdapter.kt | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index 16a727e..3b55e7d 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -22,7 +22,6 @@ import android.nfc.tech.TagTechnology import android.os.Bundle import android.os.Handler import android.os.Looper -import android.util.SparseArray import org.eclipse.keyple.core.plugin.CardIOException import org.eclipse.keyple.core.plugin.CardInsertionWaiterAsynchronousApi import org.eclipse.keyple.core.plugin.ReaderIOException @@ -55,8 +54,7 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : private val handler = Handler(Looper.getMainLooper()) private val syncWaitRemoval = Object() private val apduInterpreter: ApduInterpreterSpi? - private val volatileKeyRegistry = SparseArray() - private val persistentKeyRegistry = SparseArray() + private var loadedKey: ByteArray? = null private val keyProvider: KeyProvider? = config.keyProvider private var flags: Int @@ -107,7 +105,7 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : try { tagTechnology!!.connect() isCardChannelOpen = true - volatileKeyRegistry.clear() + loadedKey = null } catch (e: Exception) { throw CardIOException("Error while opening physical channel", e) } @@ -115,7 +113,7 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : override fun closePhysicalChannel() { isCardChannelOpen = false - volatileKeyRegistry.clear() + loadedKey = null } override fun isPhysicalChannelOpen(): Boolean { @@ -286,10 +284,7 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : } override fun loadKey(keyStorageType: KeyStorageType, keyNumber: Int, key: ByteArray) { - val targetRegistry = - if (keyStorageType == KeyStorageType.VOLATILE) volatileKeyRegistry - else persistentKeyRegistry - targetRegistry.put(keyNumber, key.copyOf()) + loadedKey = key.copyOf() } override fun generalAuthenticate(blockAddress: Int, keyType: Int, keyNumber: Int): Boolean { @@ -297,16 +292,22 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : tagTechnology as? MifareClassic ?: throw CardIOException("General Authenticate is only supported for Mifare Classic.") - val key = - volatileKeyRegistry.get(keyNumber) - ?: persistentKeyRegistry.get(keyNumber) - ?: keyProvider?.getKey(keyNumber) - ?: throw IllegalArgumentException("No key found for key number: $keyNumber") + val key = loadedKey + loadedKey = null + + val usedKey = + key + ?: if (keyProvider == null) { + throw IllegalStateException("No key loaded and no key provider available.") + } else { + keyProvider.getKey(keyNumber) + ?: throw IllegalStateException("No key found for key number: $keyNumber") + } val sectorIndex = classic.blockToSector(blockAddress) return when (keyType) { - MIFARE_KEY_A -> classic.authenticateSectorWithKeyA(sectorIndex, key) - MIFARE_KEY_B -> classic.authenticateSectorWithKeyB(sectorIndex, key) + MIFARE_KEY_A -> classic.authenticateSectorWithKeyA(sectorIndex, usedKey) + MIFARE_KEY_B -> classic.authenticateSectorWithKeyB(sectorIndex, usedKey) else -> throw IllegalArgumentException("Unsupported key type: 0x${keyType.toString(16)}") } } @@ -314,7 +315,7 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : override fun onTagDiscovered(tag: Tag) { logger.info("{}: card discovered: {}", name, tag) isCardChannelOpen = false - volatileKeyRegistry.clear() + loadedKey = null try { for (technology in tag.techList) when (technology) { IsoDep::class.qualifiedName -> { From 6207b8076b43b69c339ee061f9d705c6198e468d Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 15 Jan 2026 11:55:09 +0100 Subject: [PATCH 06/16] feat: refactor readBlock and add support for MIFARE Ultralight in AndroidNfcReaderAdapter --- .../android/nfc/AndroidNfcReaderAdapter.kt | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index 3b55e7d..cce4da0 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -113,7 +113,6 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : override fun closePhysicalChannel() { isCardChannelOpen = false - loadedKey = null } override fun isPhysicalChannelOpen(): Boolean { @@ -245,30 +244,45 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : } override fun readBlock(blockAddress: Int, length: Int): ByteArray { - if (tagTechnology is MifareClassic) { - val classic = tagTechnology as MifareClassic - val readData = classic.readBlock(blockAddress) - // Mifare Classic always reads 16 bytes. - return if (length < 16) { - readData.copyOf(length) - } else { - readData - } + return when (val tech = tagTechnology) { + is MifareClassic -> readMifareClassic(tech, blockAddress, length) + is MifareUltralight -> readMifareUltralight(tech, blockAddress, length) + else -> + throw UnsupportedOperationException( + "Unsupported tag technology: ${tech::class.java.simpleName}") } + } + + private fun readMifareClassic(classic: MifareClassic, blockAddress: Int, length: Int): ByteArray { + val readData = classic.readBlock(blockAddress) + return adjustBufferLength(readData, length) + } + private fun readMifareUltralight( + ultralight: MifareUltralight, + blockAddress: Int, + length: Int + ): ByteArray { + validateUltralightParams(blockAddress, length) + val readData = ultralight.readPages(blockAddress) + return adjustBufferLength(readData, length) + } + + private fun validateUltralightParams(blockAddress: Int, length: Int) { + require(blockAddress >= 0) { "Block number must be non-negative." } require(length % MifareUltralight.PAGE_SIZE == 0) { - "Requested length ($length) must be a multiple of PAGE_SIZE (${MifareUltralight.PAGE_SIZE})." + "Length ($length) must be a multiple of PAGE_SIZE (${MifareUltralight.PAGE_SIZE})." } - require(blockAddress >= 0) { "Block number must be non-negative." } require(length <= MIFARE_ULTRALIGHT_READ_SIZE) { - "Requested length ($length) exceeds maximum readable size 16 in a single operation." + "Length ($length) exceeds max readable size ($MIFARE_ULTRALIGHT_READ_SIZE)." } - val ultralight = tagTechnology as MifareUltralight - val readData = ultralight.readPages(blockAddress) - return if (length < MIFARE_ULTRALIGHT_READ_SIZE) { - readData.copyOf(length) + } + + private fun adjustBufferLength(data: ByteArray, expectedLength: Int): ByteArray { + return if (expectedLength < data.size) { + data.copyOf(expectedLength) } else { - readData + data } } From fe98f0b79423d56c07fc85ef026f8602fef01bc9 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 15 Jan 2026 11:58:42 +0100 Subject: [PATCH 07/16] refactor: remove redundant MIFARE Ultralight parameter validation in AndroidNfcReaderAdapter --- .../plugin/android/nfc/AndroidNfcReaderAdapter.kt | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index cce4da0..a194646 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -263,21 +263,10 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : blockAddress: Int, length: Int ): ByteArray { - validateUltralightParams(blockAddress, length) val readData = ultralight.readPages(blockAddress) return adjustBufferLength(readData, length) } - private fun validateUltralightParams(blockAddress: Int, length: Int) { - require(blockAddress >= 0) { "Block number must be non-negative." } - require(length % MifareUltralight.PAGE_SIZE == 0) { - "Length ($length) must be a multiple of PAGE_SIZE (${MifareUltralight.PAGE_SIZE})." - } - require(length <= MIFARE_ULTRALIGHT_READ_SIZE) { - "Length ($length) exceeds max readable size ($MIFARE_ULTRALIGHT_READ_SIZE)." - } - } - private fun adjustBufferLength(data: ByteArray, expectedLength: Int): ByteArray { return if (expectedLength < data.size) { data.copyOf(expectedLength) From d87265ca5804ea39fcf8f09f369fdd79e625fdc3 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 15 Jan 2026 12:01:34 +0100 Subject: [PATCH 08/16] refactor: rename variables in AndroidNfcReaderAdapter for consistency --- .../plugin/android/nfc/AndroidNfcReaderAdapter.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index a194646..a415ddd 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -253,17 +253,21 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : } } - private fun readMifareClassic(classic: MifareClassic, blockAddress: Int, length: Int): ByteArray { - val readData = classic.readBlock(blockAddress) + private fun readMifareClassic( + mifareClassic: MifareClassic, + blockAddress: Int, + length: Int + ): ByteArray { + val readData = mifareClassic.readBlock(blockAddress) return adjustBufferLength(readData, length) } private fun readMifareUltralight( - ultralight: MifareUltralight, + mifareUltralight: MifareUltralight, blockAddress: Int, length: Int ): ByteArray { - val readData = ultralight.readPages(blockAddress) + val readData = mifareUltralight.readPages(blockAddress) return adjustBufferLength(readData, length) } From 85783e78cb126f81bc25ef848a6509a61071bb0a Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 15 Jan 2026 12:04:24 +0100 Subject: [PATCH 09/16] refactor: simplify MIFARE read logic in AndroidNfcReaderAdapter --- .../android/nfc/AndroidNfcReaderAdapter.kt | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index a415ddd..41fe953 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -245,32 +245,14 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : override fun readBlock(blockAddress: Int, length: Int): ByteArray { return when (val tech = tagTechnology) { - is MifareClassic -> readMifareClassic(tech, blockAddress, length) - is MifareUltralight -> readMifareUltralight(tech, blockAddress, length) + is MifareClassic -> adjustBufferLength(tech.readBlock(blockAddress), length) + is MifareUltralight -> adjustBufferLength(tech.readPages(blockAddress), length) else -> throw UnsupportedOperationException( "Unsupported tag technology: ${tech::class.java.simpleName}") } } - private fun readMifareClassic( - mifareClassic: MifareClassic, - blockAddress: Int, - length: Int - ): ByteArray { - val readData = mifareClassic.readBlock(blockAddress) - return adjustBufferLength(readData, length) - } - - private fun readMifareUltralight( - mifareUltralight: MifareUltralight, - blockAddress: Int, - length: Int - ): ByteArray { - val readData = mifareUltralight.readPages(blockAddress) - return adjustBufferLength(readData, length) - } - private fun adjustBufferLength(data: ByteArray, expectedLength: Int): ByteArray { return if (expectedLength < data.size) { data.copyOf(expectedLength) From f07066ba3d7c5d79cbddd09843b82a14d91ffc02 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 15 Jan 2026 12:07:59 +0100 Subject: [PATCH 10/16] refactor: simplify MIFARE read logic in AndroidNfcReaderAdapter --- .../keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index 41fe953..7f64df7 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -249,7 +249,7 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : is MifareUltralight -> adjustBufferLength(tech.readPages(blockAddress), length) else -> throw UnsupportedOperationException( - "Unsupported tag technology: ${tech::class.java.simpleName}") + "Unsupported tag technology: ${tech?.let { it::class.java.simpleName } ?: "null"}") } } From 8edc0ed31e762df356adbd45cb6416912f322124 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 15 Jan 2026 12:11:30 +0100 Subject: [PATCH 11/16] refactor: improve writeBlock logic in AndroidNfcReaderAdapter Specifically, this change: - Replaces the if-else block with a when expression for better readability and safety. - Explicitly handles `MifareClassic`, `MifareUltralight`, and throws an `UnsupportedOperationException` for other technologies. - Removes the explicit 16-byte size check for `MifareClassic` within this method. --- .../plugin/android/nfc/AndroidNfcReaderAdapter.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index 7f64df7..eda2c6f 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -262,13 +262,12 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : } override fun writeBlock(blockAddress: Int, data: ByteArray) { - if (tagTechnology is MifareClassic) { - require(data.size == 16) { - "Data must be exactly 16 bytes for Mifare Classic write operations." - } - (tagTechnology as MifareClassic).writeBlock(blockAddress, data) - } else { - (tagTechnology as MifareUltralight).writePage(blockAddress, data) + when (val tech = tagTechnology) { + is MifareClassic -> tech.writeBlock(blockAddress, data) + is MifareUltralight -> tech.writePage(blockAddress, data) + else -> + throw UnsupportedOperationException( + "Unsupported tag technology: ${tech?.let { it::class.java.simpleName } ?: "null"}") } } From 455f6b081644995e546f0616807bec3a84d00d48 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 15 Jan 2026 12:18:00 +0100 Subject: [PATCH 12/16] refactor: improve MIFARE Classic authentication and tag discovery - Rename `classic` to `mifareClassic` for clarity in `generalAuthenticate`. - Simplify key retrieval logic using `checkNotNull`. - Stop resetting `loadedKey` to `null` in `onTagDiscovered`. --- .../android/nfc/AndroidNfcReaderAdapter.kt | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index eda2c6f..2182b02 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -276,7 +276,7 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : } override fun generalAuthenticate(blockAddress: Int, keyType: Int, keyNumber: Int): Boolean { - val classic = + val mifareClassic = tagTechnology as? MifareClassic ?: throw CardIOException("General Authenticate is only supported for Mifare Classic.") @@ -285,17 +285,14 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : val usedKey = key - ?: if (keyProvider == null) { - throw IllegalStateException("No key loaded and no key provider available.") - } else { - keyProvider.getKey(keyNumber) - ?: throw IllegalStateException("No key found for key number: $keyNumber") - } + ?: checkNotNull(keyProvider) { "No key loaded and no key provider available." } + .getKey(keyNumber) + ?: throw IllegalStateException("No key found for key number: $keyNumber") - val sectorIndex = classic.blockToSector(blockAddress) + val sectorIndex = mifareClassic.blockToSector(blockAddress) return when (keyType) { - MIFARE_KEY_A -> classic.authenticateSectorWithKeyA(sectorIndex, usedKey) - MIFARE_KEY_B -> classic.authenticateSectorWithKeyB(sectorIndex, usedKey) + MIFARE_KEY_A -> mifareClassic.authenticateSectorWithKeyA(sectorIndex, usedKey) + MIFARE_KEY_B -> mifareClassic.authenticateSectorWithKeyB(sectorIndex, usedKey) else -> throw IllegalArgumentException("Unsupported key type: 0x${keyType.toString(16)}") } } @@ -303,7 +300,6 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : override fun onTagDiscovered(tag: Tag) { logger.info("{}: card discovered: {}", name, tag) isCardChannelOpen = false - loadedKey = null try { for (technology in tag.techList) when (technology) { IsoDep::class.qualifiedName -> { From e37029d86bba11747defc141f33e23e9639f8f2c Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 15 Jan 2026 12:20:45 +0100 Subject: [PATCH 13/16] style: format AndroidNfcReaderAdapter.kt --- .../eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index 2182b02..91e59d9 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -290,6 +290,7 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : ?: throw IllegalStateException("No key found for key number: $keyNumber") val sectorIndex = mifareClassic.blockToSector(blockAddress) + return when (keyType) { MIFARE_KEY_A -> mifareClassic.authenticateSectorWithKeyA(sectorIndex, usedKey) MIFARE_KEY_B -> mifareClassic.authenticateSectorWithKeyB(sectorIndex, usedKey) From 06e6cbf9bf7a60c9a941086af2a813b86c398f86 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Thu, 15 Jan 2026 14:20:26 +0100 Subject: [PATCH 14/16] feat: add MIFARE Classic support, KeyProvider, and upgrade storage card API to 1.1.0 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 506a349..2d263a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added `MIFARE_CLASSIC` support in `AndroidNfcSupportedProtocols`. +- Added `KeyProvider` interface to allow providing authentication keys dynamically. +- Added `keyProvider` property to `AndroidNfcConfig`. +### Upgraded +- `keyple-plugin-storagecard-java-api` `1.0.0` -> `1.1.0` +### Documentation +- Updated overview documentation to clarify configuration and storage card support. ## [3.1.0] - 2025-07-08 ### Added From 20e8310e395c72f53e083f16b3e7d24b125648c8 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Fri, 16 Jan 2026 14:36:41 +0100 Subject: [PATCH 15/16] refactor: distinguish between MIFARE Classic 1K and 4K protocols - Replace the generic `MIFARE_CLASSIC` protocol with specific `MIFARE_CLASSIC_1K` and `MIFARE_CLASSIC_4K` constants. - Update `isProtocolSupported` to validate the physical card size against the requested MIFARE Classic protocol. - Update protocol activation and deactivation logic to handle the new specific MIFARE Classic types. --- .../android/nfc/AndroidNfcReaderAdapter.kt | 28 ++++++++++++++++--- .../nfc/AndroidNfcSupportedProtocols.kt | 11 ++++++-- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index 91e59d9..7a21d61 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -143,8 +143,26 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : // NOP } - override fun isProtocolSupported(readerProtocol: String): Boolean = - AndroidNfcSupportedProtocols.values().any { it.name == readerProtocol } + override fun isProtocolSupported(readerProtocol: String): Boolean { + val protocol = + AndroidNfcSupportedProtocols.values().firstOrNull { it.name == readerProtocol } + ?: return false + + // For MIFARE Classic, check the actual card size to distinguish between 1K and 4K + if (protocol == AndroidNfcSupportedProtocols.MIFARE_CLASSIC_1K || + protocol == AndroidNfcSupportedProtocols.MIFARE_CLASSIC_4K) { + val mifareClassic = tagTechnology as? MifareClassic ?: return false + return when (protocol) { + AndroidNfcSupportedProtocols.MIFARE_CLASSIC_1K -> + mifareClassic.size == MifareClassic.SIZE_1K + AndroidNfcSupportedProtocols.MIFARE_CLASSIC_4K -> + mifareClassic.size == MifareClassic.SIZE_4K + else -> false + } + } + + return true + } override fun activateProtocol(readerProtocol: String) { flags = @@ -153,7 +171,8 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : AndroidNfcSupportedProtocols.ISO_14443_4 -> NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_NFC_A AndroidNfcSupportedProtocols.MIFARE_ULTRALIGHT, - AndroidNfcSupportedProtocols.MIFARE_CLASSIC -> NfcAdapter.FLAG_READER_NFC_A + AndroidNfcSupportedProtocols.MIFARE_CLASSIC_1K, + AndroidNfcSupportedProtocols.MIFARE_CLASSIC_4K -> NfcAdapter.FLAG_READER_NFC_A } } @@ -164,7 +183,8 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : AndroidNfcSupportedProtocols.ISO_14443_4 -> (NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_NFC_A).inv() AndroidNfcSupportedProtocols.MIFARE_ULTRALIGHT, - AndroidNfcSupportedProtocols.MIFARE_CLASSIC -> NfcAdapter.FLAG_READER_NFC_A.inv() + AndroidNfcSupportedProtocols.MIFARE_CLASSIC_1K, + AndroidNfcSupportedProtocols.MIFARE_CLASSIC_4K -> NfcAdapter.FLAG_READER_NFC_A.inv() } } diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcSupportedProtocols.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcSupportedProtocols.kt index e88cc1e..cb0323b 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcSupportedProtocols.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcSupportedProtocols.kt @@ -37,11 +37,18 @@ enum class AndroidNfcSupportedProtocols(private val techId: String) { MIFARE_ULTRALIGHT(MifareUltralight::class.qualifiedName!!), /** - * NXP MIFARE Classic protocol. + * NXP MIFARE Classic 1K protocol. * * @since 3.2.0 */ - MIFARE_CLASSIC(MifareClassic::class.qualifiedName!!); + MIFARE_CLASSIC_1K(MifareClassic::class.qualifiedName!!), + + /** + * NXP MIFARE Classic 4K protocol. + * + * @since 3.2.0 + */ + MIFARE_CLASSIC_4K(MifareClassic::class.qualifiedName!!); internal val androidNfcTechIdentifier: String get() = techId From 1fd5e6f9d7794caba65a6fdb1b81904fe9173421 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Fortune Date: Fri, 16 Jan 2026 14:40:27 +0100 Subject: [PATCH 16/16] refactor: move MIFARE Classic size check from isProtocolSupported to isCurrentProtocol Specifically, this change: - Simplifies `isProtocolSupported` to only verify if the protocol name exists. - Moves the MIFARE Classic 1K vs 4K size validation logic to `isCurrentProtocol`. - Updates `CHANGELOG.md` to specify support for `MIFARE_CLASSIC_1K` and `MIFARE_CLASSIC_4K`. --- CHANGELOG.md | 2 +- .../android/nfc/AndroidNfcReaderAdapter.kt | 48 ++++++++++--------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d263a8..ce1993e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Added `MIFARE_CLASSIC` support in `AndroidNfcSupportedProtocols`. +- Added `MIFARE_CLASSIC_1K` and `MIFARE_CLASSIC_4K` support in `AndroidNfcSupportedProtocols`. - Added `KeyProvider` interface to allow providing authentication keys dynamically. - Added `keyProvider` property to `AndroidNfcConfig`. ### Upgraded diff --git a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt index 7a21d61..a8fd679 100644 --- a/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt +++ b/plugin/src/main/kotlin/org/eclipse/keyple/plugin/android/nfc/AndroidNfcReaderAdapter.kt @@ -143,26 +143,8 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : // NOP } - override fun isProtocolSupported(readerProtocol: String): Boolean { - val protocol = - AndroidNfcSupportedProtocols.values().firstOrNull { it.name == readerProtocol } - ?: return false - - // For MIFARE Classic, check the actual card size to distinguish between 1K and 4K - if (protocol == AndroidNfcSupportedProtocols.MIFARE_CLASSIC_1K || - protocol == AndroidNfcSupportedProtocols.MIFARE_CLASSIC_4K) { - val mifareClassic = tagTechnology as? MifareClassic ?: return false - return when (protocol) { - AndroidNfcSupportedProtocols.MIFARE_CLASSIC_1K -> - mifareClassic.size == MifareClassic.SIZE_1K - AndroidNfcSupportedProtocols.MIFARE_CLASSIC_4K -> - mifareClassic.size == MifareClassic.SIZE_4K - else -> false - } - } - - return true - } + override fun isProtocolSupported(readerProtocol: String): Boolean = + AndroidNfcSupportedProtocols.values().any { it.name == readerProtocol } override fun activateProtocol(readerProtocol: String) { flags = @@ -188,9 +170,29 @@ internal class AndroidNfcReaderAdapter(private val config: AndroidNfcConfig) : } } - override fun isCurrentProtocol(readerProtocol: String): Boolean = - AndroidNfcSupportedProtocols.valueOf(readerProtocol).androidNfcTechIdentifier == - currentCardProtocol + override fun isCurrentProtocol(readerProtocol: String): Boolean { + val protocol = AndroidNfcSupportedProtocols.valueOf(readerProtocol) + + // Check if the technology identifier matches + if (protocol.androidNfcTechIdentifier != currentCardProtocol) { + return false + } + + // For MIFARE Classic, check the actual card size to distinguish between 1K and 4K + if (protocol == AndroidNfcSupportedProtocols.MIFARE_CLASSIC_1K || + protocol == AndroidNfcSupportedProtocols.MIFARE_CLASSIC_4K) { + val mifareClassic = tagTechnology as? MifareClassic ?: return false + return when (protocol) { + AndroidNfcSupportedProtocols.MIFARE_CLASSIC_1K -> + mifareClassic.size == MifareClassic.SIZE_1K + AndroidNfcSupportedProtocols.MIFARE_CLASSIC_4K -> + mifareClassic.size == MifareClassic.SIZE_4K + else -> false + } + } + + return true + } override fun onStartDetection() { logger.info("{}: start card detection", name)