有没有办法检查终端是否收到主机卡仿真中的最后一条消息?
Is there a way to check if the last message in Host Card Emulation was received by the terminal?
在我的 Android 应用程序中,我实现了 HostApduService
。这是它的一个实现片段:
public final class MyHostApduService extends HostApduService {
private boolean disconnected = false;
@Override
public byte[] processCommandApdu(byte[] commandApdu, @Nullable Bundle extras) {
//process apdu in a background thread and call sendResponseApdu() when ready
Single.fromCallable(() -> processInternal(commandApdu))
.subscribeOn(nfcScheduler)
.subscribe(this::sendResponseApdu, t -> Log.e("could not create response", t));
return null;
}
...
@Override
public void onDeactivated(int reason) {
disconnected = true;
}
private void processInternal(byte[] apdu) {
//business logic
if(!disconnected) {
//last message was probably received by the terminal
}
}
}
所以根据我的观察,onDeactivated()
回调可以恰好在 processCommandApdu()
的中间出现,即使这样 OS 似乎更早地认识到 NFC 字段丢失了比 onDeactivated()
被调用。
以下是通信过程中丢失字段的示例:
16:21:16.808 I/MyHostApduService : processApdu[request|13bytes] 0A4040007A000000004306000
16:21:16.811 D/MyHostApduService : do business logic
16:21:16.890 D/HostEmulationManager: notifyHostEmulationDeactivated
16:21:16.890 D/HostEmulationManager: Unbinding from service ComponentInfo{app.debug/internal.MyHostApduService}
16:21:16.890 I/MyHostApduService : onDeactivated LINK_LOSS
16:21:16.898 I/MyHostApduService : processApdu[response|2bytes|90ms] 6A82
问题是我需要自信地检查最后一条消息是收到还是丢弃,因为必须执行一些重要的终结代码(但前提是终端收到消息)。有没有比使用 onDeactivated()
更好的方法来检查是否收到消息(这在时间上似乎非常不确定)?
简而言之:终端作为卡片的角色(在 HCE 中)无法检查消息是否收到。 T=CL (ISO7816) 协议在 NFC 通信期间对最后一个块的数据进行分块时不提供 ack
。
此外,在 Android 中使用 NFC 与 com.android.nfc.NfcService
through a Messenger
. This is exactly what e.g. HostApduService
or HostNfcFService
进行通信。
有3种基本信号:
MSG_COMMAND_APDU = 0
MSG_RESPONSE_APDU = 1
MSG_DEACTIVATED = 2
应用程序服务首先接收信号 0 (MSG_COMMAND_APDU
) 并使用 1 (MSG_RESPONSE_APDU
) 类型信号进行响应。在任何时间点都可能发生 2 (MSG_DEACTIVATED
)。现在,应用程序可以检查在发出命令请求后,是否在发送响应之前收到了停用消息。这在大多数情况下都很有效,但并不一致。在实际停用后 100+ms 收到停用信号的情况并不少见,因此似乎已发送响应。即使进行此检查,您也只能知道何时将消息传递给 NfcService
,实际传输也需要一些时间(每个字节大约几微秒)。
所以最后,你只能判断终端是否肯定没有收到响应(当停用在响应之前),否则你就任凭摆布了NfcService
的 HAL 实现或驱动程序。
你不能。相反,如果您确实需要可靠地检测这种情况,则需要调整您的通信协议。
问题不是真正的 Android,而是底层的通信协议(ISO/IEC 7816-4 而不是 ISO/IEC 14443-4)。这些协议是为与常规智能卡通信而构建的。智能卡是完全无源设备,当从 reader 或远离 NFC 射频场时无法继续处理(由于缺乏能量)。
协议栈专为询问器驱动的通信而设计(询问器是终端)。通信以 command-response 顺序执行。原则上,每个 command-response 序列都包含以下步骤(还有一些额外的极端情况):
- 终端发送命令。
- 智能卡接收并处理命令。
- 智能卡发送响应数据并以状态字结束。
应用协议 (ISO/IEC 7816-4) 和传输协议 (ISO/IEC 14443-4 aka ISO-DEP) 均未通过任何形式的确认确认智能卡响应。一旦智能卡发送了它的响应,它就被认为已经完成处理。
实际上,这对于经典智能卡(接触式或非接触式)来说不是问题。中断的通信将导致卡为 power-cycled(因为 link-loss 也意味着 power-loss 或因为终端执行显式重置)。因此智能卡此时将无法依赖清理序列。
然而,这并不意味着没有办法克服这个限制。经典智能卡即使在 power-cycles 期间也保持持久状态。关键操作作为原子事务执行。在 power-loss 的情况下,cleanup/rollback 通常在重置时执行 (boot-up)。但是,映射到 Android 并不容易,因为 link-loss 不会导致 HCE 端的执行中断。因此,在将响应发送回 reader 之前,无法检测到 HCE 智能卡已被拔出。尽管如此,也没有原子事务会被 link-loss 中断。因此,重置(即接收选择您的应用程序的 SELECT(按 DF 名称)命令)仍然是执行清理(例如重置应用程序状态)的正确位置。
根据您的具体要求,典型的方法是调整 application-level 协议并添加一个确认命令以确认收到(然后是第二个)最后一个响应。 IE。如果你目前有类似的东西:
T---> SELECT APPLICATION
<---C FCI | 9000
T---> PERFORM CRITICAL OPERATION
<---C CRITICAL OPERATION RESULT
您可以调整协议以包含最终确认:
T---> SELECT APPLICATION
<---C FCI | 9000
T---> PERFORM CRITICAL OPERATION
<---C CRITICAL OPERATION RESULT
T---> CONFIRM RECEPTION OF RESULT
<---C 9000
现在您不会真正关心最终响应 (9000) 是否在发送到终端的途中丢失了。
在我的 Android 应用程序中,我实现了 HostApduService
。这是它的一个实现片段:
public final class MyHostApduService extends HostApduService {
private boolean disconnected = false;
@Override
public byte[] processCommandApdu(byte[] commandApdu, @Nullable Bundle extras) {
//process apdu in a background thread and call sendResponseApdu() when ready
Single.fromCallable(() -> processInternal(commandApdu))
.subscribeOn(nfcScheduler)
.subscribe(this::sendResponseApdu, t -> Log.e("could not create response", t));
return null;
}
...
@Override
public void onDeactivated(int reason) {
disconnected = true;
}
private void processInternal(byte[] apdu) {
//business logic
if(!disconnected) {
//last message was probably received by the terminal
}
}
}
所以根据我的观察,onDeactivated()
回调可以恰好在 processCommandApdu()
的中间出现,即使这样 OS 似乎更早地认识到 NFC 字段丢失了比 onDeactivated()
被调用。
以下是通信过程中丢失字段的示例:
16:21:16.808 I/MyHostApduService : processApdu[request|13bytes] 0A4040007A000000004306000
16:21:16.811 D/MyHostApduService : do business logic
16:21:16.890 D/HostEmulationManager: notifyHostEmulationDeactivated
16:21:16.890 D/HostEmulationManager: Unbinding from service ComponentInfo{app.debug/internal.MyHostApduService}
16:21:16.890 I/MyHostApduService : onDeactivated LINK_LOSS
16:21:16.898 I/MyHostApduService : processApdu[response|2bytes|90ms] 6A82
问题是我需要自信地检查最后一条消息是收到还是丢弃,因为必须执行一些重要的终结代码(但前提是终端收到消息)。有没有比使用 onDeactivated()
更好的方法来检查是否收到消息(这在时间上似乎非常不确定)?
简而言之:终端作为卡片的角色(在 HCE 中)无法检查消息是否收到。 T=CL (ISO7816) 协议在 NFC 通信期间对最后一个块的数据进行分块时不提供 ack
。
此外,在 Android 中使用 NFC 与 com.android.nfc.NfcService
through a Messenger
. This is exactly what e.g. HostApduService
or HostNfcFService
进行通信。
有3种基本信号:
MSG_COMMAND_APDU = 0
MSG_RESPONSE_APDU = 1
MSG_DEACTIVATED = 2
应用程序服务首先接收信号 0 (MSG_COMMAND_APDU
) 并使用 1 (MSG_RESPONSE_APDU
) 类型信号进行响应。在任何时间点都可能发生 2 (MSG_DEACTIVATED
)。现在,应用程序可以检查在发出命令请求后,是否在发送响应之前收到了停用消息。这在大多数情况下都很有效,但并不一致。在实际停用后 100+ms 收到停用信号的情况并不少见,因此似乎已发送响应。即使进行此检查,您也只能知道何时将消息传递给 NfcService
,实际传输也需要一些时间(每个字节大约几微秒)。
所以最后,你只能判断终端是否肯定没有收到响应(当停用在响应之前),否则你就任凭摆布了NfcService
的 HAL 实现或驱动程序。
你不能。相反,如果您确实需要可靠地检测这种情况,则需要调整您的通信协议。
问题不是真正的 Android,而是底层的通信协议(ISO/IEC 7816-4 而不是 ISO/IEC 14443-4)。这些协议是为与常规智能卡通信而构建的。智能卡是完全无源设备,当从 reader 或远离 NFC 射频场时无法继续处理(由于缺乏能量)。
协议栈专为询问器驱动的通信而设计(询问器是终端)。通信以 command-response 顺序执行。原则上,每个 command-response 序列都包含以下步骤(还有一些额外的极端情况):
- 终端发送命令。
- 智能卡接收并处理命令。
- 智能卡发送响应数据并以状态字结束。
应用协议 (ISO/IEC 7816-4) 和传输协议 (ISO/IEC 14443-4 aka ISO-DEP) 均未通过任何形式的确认确认智能卡响应。一旦智能卡发送了它的响应,它就被认为已经完成处理。
实际上,这对于经典智能卡(接触式或非接触式)来说不是问题。中断的通信将导致卡为 power-cycled(因为 link-loss 也意味着 power-loss 或因为终端执行显式重置)。因此智能卡此时将无法依赖清理序列。
然而,这并不意味着没有办法克服这个限制。经典智能卡即使在 power-cycles 期间也保持持久状态。关键操作作为原子事务执行。在 power-loss 的情况下,cleanup/rollback 通常在重置时执行 (boot-up)。但是,映射到 Android 并不容易,因为 link-loss 不会导致 HCE 端的执行中断。因此,在将响应发送回 reader 之前,无法检测到 HCE 智能卡已被拔出。尽管如此,也没有原子事务会被 link-loss 中断。因此,重置(即接收选择您的应用程序的 SELECT(按 DF 名称)命令)仍然是执行清理(例如重置应用程序状态)的正确位置。
根据您的具体要求,典型的方法是调整 application-level 协议并添加一个确认命令以确认收到(然后是第二个)最后一个响应。 IE。如果你目前有类似的东西:
T---> SELECT APPLICATION <---C FCI | 9000 T---> PERFORM CRITICAL OPERATION <---C CRITICAL OPERATION RESULT
您可以调整协议以包含最终确认:
T---> SELECT APPLICATION <---C FCI | 9000 T---> PERFORM CRITICAL OPERATION <---C CRITICAL OPERATION RESULT T---> CONFIRM RECEPTION OF RESULT <---C 9000
现在您不会真正关心最终响应 (9000) 是否在发送到终端的途中丢失了。