Enable NFC for Linux and pcscd on Dell E7470 (and others) with ControlVault2
Jacek Kowalski
2020-05-20 4664a288e4b9fb39d26258d488e907721a5c2c33
commit | author | age
6b0ed4 1 #!/usr/bin/env python3
JK 2
3 # Enable NFC on Linux (CCID/PCSCD)
4 # Dell E7470
5 # Dell ControlVault2
6 # BCM20795 (20795A1)
7
8 import binascii
9 import logging
10 import math
11 import struct
12 import sys
13 import time
14 import usb.core
15 import usb.util
16
13e114 17 SUPPORTED_DEVICES = [
JK 18     {'idVendor': 0x0A5C, 'idProduct': 0x5834},
37f85f 19     {'idVendor': 0x0A5C, 'idProduct': 0x5832},
13e114 20 ]
6b0ed4 21
JK 22 logging.basicConfig(level=logging.DEBUG)
23 logger = logging.getLogger(__name__)
24
25 def to_hex(val):
26     return ' '.join([bytes([i]).hex() for i in val])
27
28 class BcmCommunicator:
29     def __init__(self, device, bulk_in, bulk_out):
30         self.logger = logging.getLogger(__name__)
31         self.device = device
32         self.bulk_in = bulk_in
33         self.bulk_out = bulk_out
34     
35     def ctrl_transfer(self, *args, **kwargs):
36         self.logger.debug('Control: {} {}'.format(args, kwargs))
37         return self.device.ctrl_transfer(*args, **kwargs)
38     
39     def write(self, *args, **kwargs):
40         return self.bulk_out.write(*args, **kwargs)
41     
42     def read(self, *args, **kwargs):
43         return self.bulk_in.read(*args, **kwargs)
44     
45     def send_packet(self, payload):
46         packet_type = 0x01
47         unknown1 = 0x00
48         length = len(payload)
49         
50         packet = struct.pack('>BBH', packet_type, unknown1, length) + payload
51         
52         self.logger.debug('Put: {}'.format(to_hex(packet)))
53         self.write(packet)
54
55     def recv_packet(self):
56         packet = self.read(64, timeout=5000).tobytes()
57         tag = packet[0:2]
58         if tag != b'\x00\x00':
59             raise Exception('Unknown tag: {}'.format(tag.hex()))
60         length = packet[2:4]
61         length = struct.unpack('>H', length)[0]
62         
63         for i in range(0, math.ceil(length/64) - 1):
64             packet += self.read(64).tobytes()
65         
66         self.logger.debug('Got: {}'.format(to_hex(packet)))
67         return packet[4:]
68     
69     def talk(self, exchange):
70         for packet in exchange:
71             self.send_packet(bytes.fromhex(packet))
72             data = self.recv_packet()
73             
74             if data[1] == 0x61:
75                 packet = self.recv_packet()
76     
13e114 77     @staticmethod
JK 78     def _dev_match(template, candidate):
79         for prop, value in template.items():
80             if prop not in candidate.__dict__ or candidate.__dict__[prop] != value:
81                 return False
82         return True
83     
6b0ed4 84     @classmethod
13e114 85     def _dev_matcher(cls, dev):
JK 86         for device in SUPPORTED_DEVICES:
87             if cls._dev_match(device, dev):
88                 return True
89         return False
90     
91     @classmethod
92     def find(cls):
6b0ed4 93         logger = logging.getLogger(__name__)
13e114 94         logger.info('Looking for BCM device...')
6b0ed4 95         
13e114 96         device = usb.core.find(custom_match=cls._dev_matcher)
6b0ed4 97         if device is None:
13e114 98             raise Exception('Cannot find BCM device - check list of supported devices')
JK 99         logger.info('Found {:04X}:{:04X}'.format(device.idVendor, device.idProduct))
6b0ed4 100         
JK 101         logger.debug('Enumerating interfaces...')
102         configuration = device.get_active_configuration()
103         bcm_interface = None
104         for interface in configuration:
105             if interface.bInterfaceClass == 0xff and interface.iInterface == 0x08:
36fd63 106                 if bcm_interface is not None:
JK 107                     raise Exception('More than one vendor-specific interface found!')
6b0ed4 108                 bcm_interface = interface
JK 109         if bcm_interface is None:
110             raise Exception('Cannot find vendor-specific interface')
111         logger.debug('Interface found: {}'.format(bcm_interface._str()))
112         
113         logger.debug('Enumerating endpoints...')
114         bulk_in = None
115         bulk_out = None
116         for endpoint in bcm_interface:
117             if endpoint.bmAttributes & usb.util._ENDPOINT_TRANSFER_TYPE_MASK == usb.util.ENDPOINT_TYPE_BULK:
118                 if endpoint.bEndpointAddress & usb.util._ENDPOINT_DIR_MASK == usb.util.ENDPOINT_IN:
119                     if bulk_in is not None:
120                         raise Exception('More than one BULK IN endpoint found!')
121                     bulk_in = endpoint
122                     logger.debug('BULK IN found: {}'.format(bulk_in._str()))
123                 if endpoint.bEndpointAddress & usb.util._ENDPOINT_DIR_MASK == usb.util.ENDPOINT_OUT:
124                     if bulk_out is not None:
125                         raise Exception('More than one BULK OUT endpoint found!')
126                     bulk_out = endpoint
127                     logger.debug('BULK OUT found: {}'.format(bulk_out._str()))
128         
129         if bulk_in is None:
130             raise Exception('BULK IN endpoint not found!')
131         if bulk_out is None:
132             raise Exception('BULK OUT endpoint not found!')
133         
134         logger.debug('Returning {} object...'.format(cls.__name__))
135         return cls(device, bulk_in, bulk_out)
136
137 turn_on_seq1 = [
138     "10 2f 04 00",
139     "10 2f 1d 03 05 90 65",
140     "10 2f 2d 00",
141     "10 2f 11 01 f7",
142     "01 27 fc 0c 08 00 01 00 01 00 00 00 00 00 00 00",
143 ]
144 turn_on_seq2 = [
145     "10 20 00 01 01",
146     "10 20 01 02 01 00",
147     "10 20 02 67 01 b9 64 01 00 ff ff 50 00 8b 13 00 10 00 06 00 00 00 00 00 ff 00 00 00 ff 00 00 04 00 00 00 00 03 00 00 00 03 00 0c 00 00 0d 00 00 00 00 00 00 00 00 00 00 33 23 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 02 53 3b 0f 00 00 00 00 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00",
148     "10 20 02 90 0a ca 05 00 00 00 00 2c 80 01 01 b0 05 01 03 03 03 08 b5 03 01 03 ff c9 0d 24 00 00 00 01 00 bb 00 e4 00 0a 01 02 d6 0d 01 02 00 00 00 00 00 01 00 01 5a 00 8a b2 02 e8 03 c8 1e 06 1f 00 0a 00 30 00 04 24 00 1c 00 75 00 77 00 76 00 1c 00 03 00 0a 00 56 01 00 00 40 04 d7 01 07 dd 32 00 00 00 29 16 08 08 06 04 00 00 00 1f 27 0a 6d 20 00 52 20 00 00 00 01 85 00 00 32 1f 00 00 02 0a 16 00 02 55 55 55 55 55 55 55 55 55 55 55 55 55 1e",
149     "10 20 02 06 01 b7 03 02 00 01",
150     "10 2f 06 01 01",
151     "10 20 02 0e 02 51 08 20 79 ff ff ff ff ff ff 58 01 07",
152     "10 21 00 07 02 04 03 02 05 03 03",
153     "10 20 02 17 01 29 14 46 66 6d 01 01 11 02 02 07 ff 03 02 00 13 04 01 64 07 01 03",
154     "10 20 02 1a 02 61 14 46 66 6d 01 01 11 02 02 07 ff 03 02 00 13 04 01 64 07 01 03 60 01 07",
155     "10 20 02 10 05 30 01 04 31 01 00 32 01 40 38 01 00 50 01 02",
156     "10 20 02 05 01 00 02 fa 00",
157     "10 20 02 0b 01 c2 08 01 08 00 04 80 c3 c9 01",
158     "10 21 03 0d 06 00 01 01 01 02 01 80 01 82 01 06 01",
159 ]
160
161 def turn_on(communicator):
162     communicator.ctrl_transfer(0x41, 0, 1, 3)
163     communicator.talk(turn_on_seq1)
164     communicator.ctrl_transfer(0x41, 1, 0, 3)
165     communicator.talk(turn_on_seq2)
166     communicator.ctrl_transfer(0x41, 1, 1, 3)
167
168 def turn_off(communicator):
169     communicator.ctrl_transfer(0x41, 1, 0, 3)
170     communicator.ctrl_transfer(0x41, 0, 0, 3)
171
172
173
174 if __name__ == "__main__":
175     if len(sys.argv) < 2:
176         print('Usage: {} [on|off]'.format(sys.argv[0]))
177         sys.exit(2)
178     
13e114 179     communicator = BcmCommunicator.find()
6b0ed4 180     if sys.argv[1] == 'on':
JK 181         logger.info('Turning NFC on...')
182         turn_on(communicator)
183         logger.info('NFC should be turned on now!')
184     elif sys.argv[1] == 'off':
185         logger.info('Turning NFC off...')
186         turn_off(communicator)
187         logger.info('NFC should be turned off now!')
188     else:
189         raise Exception('Unknown option: {}'.format(sys.argv[1]))