Skip to content

Commit

Permalink
Add PROBE request to WebUSB protocol, implement in FlutterNfcKitWeb
Browse files Browse the repository at this point in the history
Signed-off-by: Harry Chen <[email protected]>
  • Loading branch information
Harry-Chen committed May 17, 2022
1 parent a33f08c commit 597f227
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 20 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Thank [nfc_manager](https://pub.dev/packages/nfc_manager) plugin for these instr

## Web

The web version of this plugin **does not actually support NFC** in browsers, but uses a specific [WebUSB protocol](WebUSB.md), so that Flutter programs can communicate with dual-interface (NFC / USB) devices in a platform-independent way.
The web version of this plugin **does not actually support NFC** in browsers, but uses a specific [WebUSB protocol](https://github.com/nfcim/flutter_nfc_kit/blob/master/WebUSB.md), so that Flutter programs can communicate with dual-interface (NFC / USB) devices in a platform-independent way.

Make sure you understand the statement above and the protocol before using this plugin.

Expand Down
36 changes: 24 additions & 12 deletions WebUSB.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,46 @@ Each type of message is a vendor-specific request, defined as:
| CMD | 00h |
| RESP | 01h |
| STAT | 02h |
| PROBE | FFh |

1. Command APDU
1. Probe device

The following control pipe request is used to probe whether the device supports this protocol.

| bmRequestType | bRequest | wValue | wIndex | wLength | Data |
| ------------- | -------- | ------ | ------ | ------- | ---- |
| 11000001B | PROBE | 0000h | 1 | 0 | N/A |

The response data **MUST** begin with magic bytes `0x5f4e46435f494d5f` (`_NFC_IM_`) in order to be recognized.
The remaining bytes can be used as custom information provided by the device.

2. Send command APDU

The following control pipe request is used to send a command APDU.

| bmRequestType | bRequest | wValue | wIndex | wLength | Data |
| ------------- | -------- | ------ | ------ | -------------- | ----- |
| 01000001B | CMD | 0000h | 1 | length of data | bytes |

2. Get the response APDU
3. Get execution status

The following control pipe request is used to get the response APDU.
The following control pipe request is used to get the status of the device.

| bmRequestType | bRequest | wValue | wIndex | wLength | Data |
| ------------- | -------- | ------ | ------ | ------- | ---- |
| 11000001B | RESP | 0000h | 1 | 0 | N/A |
| 11000001B | STAT | 0000h | 1 | 0 | N/A |

The device will send the response no more than 1500 bytes.
The response data is 1-byte long, `0x01` for in progress and `0x00` for finishing processing,
and you can fetch the result using `RESP` command, and other values for invalid states.

If the command is still under processing, the response will be empty.

3. Get the execution status
4. Get response APDU

The following control pipe request is used to get the status of the card.
The following control pipe request is used to get the response APDU.

| bmRequestType | bRequest | wValue | wIndex | wLength | Data |
| ------------- | -------- | ------ | ------ | ------- | ---- |
| 11000001B | STAT | 0000h | 1 | 0 | N/A |

The response data is 1-byte long, `0x01` for in progress and `0x00` for finishing processing,
and you can fetch the result using `RESP` command, and other values for invalid states.
| 11000001B | RESP | 0000h | 1 | 0 | N/A |

If the command is still under processing, the response will be empty.
The device will send the response no more than 1500 bytes.
14 changes: 11 additions & 3 deletions lib/flutter_nfc_kit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class NFCTag {
/// Indicates whether this NDEF tag can be made read-only (only works on Android, always false on iOS)
final bool? ndefCanMakeReadOnly;

/// Custom probe data returned by WebUSB device (see [FlutterNfcKitWeb] for detail, only on Web)
final String? webUSBCustomProbeData;

NFCTag(
this.type,
this.id,
Expand All @@ -105,7 +108,8 @@ class NFCTag {
this.ndefType,
this.ndefCapacity,
this.ndefWritable,
this.ndefCanMakeReadOnly);
this.ndefCanMakeReadOnly,
this.webUSBCustomProbeData);

factory NFCTag.fromJson(Map<String, dynamic> json) => _$NFCTagFromJson(json);
Map<String, dynamic> toJson() => _$NFCTagToJson(this);
Expand Down Expand Up @@ -175,7 +179,6 @@ class FlutterNfcKit {
/// If tag is successfully polled, a session is started.
///
/// The [timeout] parameter only works on Android & Web (default to be 20 seconds). On iOS it is ignored and decided by the OS.
/// On Web, all parameters are ignored except [timeout].
///
/// On iOS, set [iosAlertMessage] to display a message when the session starts (to guide users to scan a tag),
/// and set [iosMultipleTagMessage] to display a message when multiple tags are found.
Expand All @@ -186,6 +189,9 @@ class FlutterNfcKit {
/// The four boolean flags [readIso14443A], [readIso14443B], [readIso18092], [readIso15693] controls the NFC technology that would be tried.
/// On iOS, setting any of [readIso14443A] and [readIso14443B] will enable `iso14443` in `pollingOption`.
///
/// On Web, all parameters are ignored except [timeout] and [probeWebUSBMagic].
/// If [probeWebUSBMagic] is set, the library will use the `PROBE` request to check whether the device supports our API (see [FlutterNfcKitWeb] for details).
///
/// Note: Sometimes NDEF check [leads to error](https://github.com/nfcim/flutter_nfc_kit/issues/11), and disabling it might help.
/// If disabled, you will not be able to use any NDEF-related methods in the current session.
///
Expand All @@ -203,6 +209,7 @@ class FlutterNfcKit {
bool readIso14443B = true,
bool readIso18092 = false,
bool readIso15693 = true,
bool probeWebUSBMagic = false,
}) async {
// use a bitmask for compact representation
int technologies = 0x0;
Expand All @@ -218,7 +225,8 @@ class FlutterNfcKit {
'timeout': timeout?.inMilliseconds ?? POLL_TIIMEOUT,
'iosAlertMessage': iosAlertMessage,
'iosMultipleTagMessage': iosMultipleTagMessage,
'technologies': technologies
'technologies': technologies,
'probeWebUSBMagic': probeWebUSBMagic,
});
return NFCTag.fromJson(jsonDecode(data));
}
Expand Down
2 changes: 2 additions & 0 deletions lib/flutter_nfc_kit.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion lib/flutter_nfc_kit_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ class FlutterNfcKitWeb {

case 'poll':
int timeout = call.arguments["timeout"];
return await WebUSB.poll(timeout);
bool probe = call.arguments["probeWebUSBMagic"];
return await WebUSB.poll(timeout, probe);

case 'transceive':
var data = call.arguments["data"];
Expand Down
53 changes: 50 additions & 3 deletions lib/webusb_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ import 'package:js/js.dart';
import 'package:logging/logging.dart';

final log = Logger('FlutterNFCKit:WebUSB');

/// The USB class code used to identify a WebUSB device that supports this protocol.
const int USB_CLASS_CODE_VENDOR_SPECIFIC = 0xFF;

@JS('navigator.usb')
class _USB {
external static dynamic requestDevice(_USBDeviceRequestOptions options);
// ignore: unused_field
external static Function ondisconnect;
}

Expand Down Expand Up @@ -52,6 +55,7 @@ class _USBControlTransferParameters {
/// Note: you should **NEVER use this class directly**, but instead use the [FlutterNfcKit] class in your project.
class WebUSB {
static dynamic _device;
static String customProbeData = "";

static bool _deviceAvailable() {
return _device != null && getProperty(_device, 'opened');
Expand All @@ -62,8 +66,10 @@ class WebUSB {
log.info('device is disconnected from WebUSB API');
}

static const USB_PROBE_MAGIC = '_NFC_IM_';

/// Try to poll a WebUSB device according to our protocol.
static Future<String> poll(int timeout) async {
static Future<String> poll(int timeout, bool probeMagic) async {
// request WebUSB device with custom classcode
if (!_deviceAvailable()) {
var devicePromise = _USB.requestDevice(new _USBDeviceRequestOptions(
Expand All @@ -87,14 +93,55 @@ class WebUSB {
throw PlatformException(
code: "500", message: "WebUSB API error", details: e);
}

if (probeMagic) {
try {
// PROBE request
var promise = callMethod(_device, 'controlTransferIn', [
new _USBControlTransferParameters(
requestType: 'vendor',
recipient: 'interface',
request: 0xff,
value: 0,
index: 1),
1
]);
var resp = await promiseToFuture(promise);
if (getProperty(resp, 'status') == 'stalled') {
throw PlatformException(
code: "500", message: "Device error: transfer stalled");
}
var result =
(getProperty(resp, 'data').buffer as ByteBuffer).asUint8List();
if (result.length < USB_PROBE_MAGIC.length ||
result.sublist(0, USB_PROBE_MAGIC.length) !=
Uint8List.fromList(USB_PROBE_MAGIC.codeUnits)) {
throw PlatformException(
code: "500",
message:
"Device error: invalid probe response: ${hex.encode(result)}, should begin with $USB_PROBE_MAGIC");
}
customProbeData = hex.encode(result.sublist(USB_PROBE_MAGIC.length));
} on Exception catch (e) {
log.severe("Probe error", e);
throw PlatformException(
code: "500", message: "WebUSB API error", details: e);
}
} else {
customProbeData = "";
}
}
// get VID & PID
int vendorId = getProperty(_device, 'vendorId');
int productId = getProperty(_device, 'productId');
String id =
'${vendorId.toRadixString(16).padLeft(4, '0')}:${productId.toRadixString(16).padLeft(4, '0')}';
return json
.encode({'type': 'webusb', 'id': id, 'standard': 'canokey-procotol'});
return json.encode({
'type': 'webusb',
'id': id,
'standard': 'nfc-im-webusb-protocol',
'customProbeData': customProbeData
});
}

static Future<Uint8List> _doTransceive(Uint8List capdu) async {
Expand Down

0 comments on commit 597f227

Please sign in to comment.