blob: f49da34f4ca46a8ab0d41116c751c224212af0a4 [file] [log] [blame]
Hai Shalomfdcde762020-04-02 11:19:20 -07001#!/usr/bin/python3
2#
3# Example nfcpy to wpa_supplicant wrapper for DPP NFC operations
4# Copyright (c) 2012-2013, Jouni Malinen <j@w1.fi>
5# Copyright (c) 2019-2020, The Linux Foundation
6#
7# This software may be distributed under the terms of the BSD license.
8# See README for more details.
9
10import os
11import sys
12import time
13import threading
14import argparse
15
16import nfc
17import ndef
18
19import logging
20
21scriptsdir = os.path.dirname(os.path.realpath("dpp-nfc.py"))
22sys.path.append(os.path.join(scriptsdir, '..', '..', 'wpaspy'))
23import wpaspy
24
25wpas_ctrl = '/var/run/wpa_supplicant'
26ifname = None
27init_on_touch = False
28in_raw_mode = False
29prev_tcgetattr = 0
30no_input = False
31srv = None
32continue_loop = True
33terminate_now = False
34summary_file = None
35success_file = None
36
37def summary(txt):
38 print(txt)
39 if summary_file:
40 with open(summary_file, 'a') as f:
41 f.write(txt + "\n")
42
43def success_report(txt):
44 summary(txt)
45 if success_file:
46 with open(success_file, 'a') as f:
47 f.write(txt + "\n")
48
49def wpas_connect():
50 ifaces = []
51 if os.path.isdir(wpas_ctrl):
52 try:
53 ifaces = [os.path.join(wpas_ctrl, i) for i in os.listdir(wpas_ctrl)]
54 except OSError as error:
55 print("Could not find wpa_supplicant: ", error)
56 return None
57
58 if len(ifaces) < 1:
59 print("No wpa_supplicant control interface found")
60 return None
61
62 for ctrl in ifaces:
63 if ifname:
64 if ifname not in ctrl:
65 continue
66 try:
67 print("Trying to use control interface " + ctrl)
68 wpas = wpaspy.Ctrl(ctrl)
69 return wpas
70 except Exception as e:
71 pass
72 return None
73
74def dpp_nfc_uri_process(uri):
75 wpas = wpas_connect()
76 if wpas is None:
77 return False
78 peer_id = wpas.request("DPP_NFC_URI " + uri)
79 if "FAIL" in peer_id:
80 print("Could not parse DPP URI from NFC URI record")
81 return False
82 peer_id = int(peer_id)
83 print("peer_id=%d" % peer_id)
84 cmd = "DPP_AUTH_INIT peer=%d" % peer_id
85 res = wpas.request(cmd)
86 if "OK" not in res:
87 print("Failed to initiate DPP Authentication")
88 return False
89 print("DPP Authentication initiated")
90 return True
91
92def dpp_hs_tag_read(record):
93 wpas = wpas_connect()
94 if wpas is None:
95 return False
96 print(record)
97 if len(record.data) < 5:
98 print("Too short DPP HS")
99 return False
100 if record.data[0] != 0:
101 print("Unexpected URI Identifier Code")
102 return False
103 uribuf = record.data[1:]
104 try:
105 uri = uribuf.decode()
106 except:
107 print("Invalid URI payload")
108 return False
109 print("URI: " + uri)
110 if not uri.startswith("DPP:"):
111 print("Not a DPP URI")
112 return False
113 return dpp_nfc_uri_process(uri)
114
115def get_status(wpas, extra=None):
116 if extra:
117 extra = "-" + extra
118 else:
119 extra = ""
120 res = wpas.request("STATUS" + extra)
121 lines = res.splitlines()
122 vals = dict()
123 for l in lines:
124 try:
125 [name, value] = l.split('=', 1)
126 except ValueError:
127 logger.info("Ignore unexpected status line: " + l)
128 continue
129 vals[name] = value
130 return vals
131
132def get_status_field(wpas, field, extra=None):
133 vals = get_status(wpas, extra)
134 if field in vals:
135 return vals[field]
136 return None
137
138def own_addr(wpas):
139 return get_status_field(wpas, "address")
140
141def dpp_bootstrap_gen(wpas, type="qrcode", chan=None, mac=None, info=None,
142 curve=None, key=None):
143 cmd = "DPP_BOOTSTRAP_GEN type=" + type
144 if chan:
145 cmd += " chan=" + chan
146 if mac:
147 if mac is True:
148 mac = own_addr(wpas)
149 cmd += " mac=" + mac.replace(':', '')
150 if info:
151 cmd += " info=" + info
152 if curve:
153 cmd += " curve=" + curve
154 if key:
155 cmd += " key=" + key
156 res = wpas.request(cmd)
157 if "FAIL" in res:
158 raise Exception("Failed to generate bootstrapping info")
159 return int(res)
160
161def wpas_get_nfc_uri(start_listen=True):
162 wpas = wpas_connect()
163 if wpas is None:
164 return None
165 global own_id, chanlist
166 own_id = dpp_bootstrap_gen(wpas, type="nfc-uri", chan=chanlist, mac=True)
167 res = wpas.request("DPP_BOOTSTRAP_GET_URI %d" % own_id).rstrip()
168 if "FAIL" in res:
169 return None
170 if start_listen:
171 wpas.request("DPP_LISTEN 2412 netrole=configurator")
172 return res
173
174def wpas_report_handover_req(uri):
175 wpas = wpas_connect()
176 if wpas is None:
177 return None
178 global own_id
179 cmd = "DPP_NFC_HANDOVER_REQ own=%d uri=%s" % (own_id, uri)
180 return wpas.request(cmd)
181
182def wpas_report_handover_sel(uri):
183 wpas = wpas_connect()
184 if wpas is None:
185 return None
186 global own_id
187 cmd = "DPP_NFC_HANDOVER_SEL own=%d uri=%s" % (own_id, uri)
188 return wpas.request(cmd)
189
190def dpp_handover_client(llc):
191 uri = wpas_get_nfc_uri(start_listen=False)
192 uri = ndef.UriRecord(uri)
193 print("NFC URI record for DPP: " + str(uri))
194 carrier = ndef.Record('application/vnd.wfa.dpp', 'A', uri.data)
195 hr = ndef.HandoverRequestRecord(version="1.4", crn=os.urandom(2))
196 hr.add_alternative_carrier('active', carrier.name)
197 message = [hr, carrier]
198 print("NFC Handover Request message for DPP: " + str(message))
199
200 client = nfc.handover.HandoverClient(llc)
201 try:
202 summary("Trying to initiate NFC connection handover")
203 client.connect()
204 summary("Connected for handover")
205 except nfc.llcp.ConnectRefused:
206 summary("Handover connection refused")
207 client.close()
208 return
209 except Exception as e:
210 summary("Other exception: " + str(e))
211 client.close()
212 return
213
214 summary("Sending handover request")
215
216 if not client.send_records(message):
217 summary("Failed to send handover request")
218 client.close()
219 return
220
221 summary("Receiving handover response")
222 message = client.recv_records(timeout=3.0)
223 if message is None:
224 summary("No response received")
225 client.close()
226 return
227 print("Received message: " + str(message))
228 if len(message) < 1 or \
229 not isinstance(message[0], ndef.HandoverSelectRecord):
230 summary("Response was not Hs - received: " + message.type)
231 client.close()
232 return
233
234 print("Received message")
235 print("alternative carriers: " + str(message[0].alternative_carriers))
236
237 dpp_found = False
238 for carrier in message:
239 if isinstance(carrier, ndef.HandoverSelectRecord):
240 continue
241 print("Remote carrier type: " + carrier.type)
242 if carrier.type == "application/vnd.wfa.dpp":
243 if len(carrier.data) == 0 or carrier.data[0] != 0:
244 print("URI Identifier Code 'None' not seen")
245 continue
246 print("DPP carrier type match - send to wpa_supplicant")
247 dpp_found = True
248 uri = carrier.data[1:].decode("utf-8")
249 print("DPP URI: " + uri)
250 res = wpas_report_handover_sel(uri)
251 if res is None or "FAIL" in res:
252 summary("DPP handover report rejected")
253 break
254
255 success_report("DPP handover reported successfully (initiator)")
256 print("peer_id=" + res)
257 peer_id = int(res)
258 # TODO: Single Configurator instance
259 wpas = wpas_connect()
260 if wpas is None:
261 break
262 res = wpas.request("DPP_CONFIGURATOR_ADD")
263 if "FAIL" in res:
264 print("Failed to initiate Configurator")
265 break
266 conf_id = int(res)
267 global own_id
268 print("Initiate DPP authentication")
269 cmd = "DPP_AUTH_INIT peer=%d own=%d conf=sta-dpp configurator=%d" % (peer_id, own_id, conf_id)
270 res = wpas.request(cmd)
271 if "FAIL" in res:
272 print("Failed to initiate DPP authentication")
273 break
274
275 if not dpp_found:
276 print("DPP carrier not seen in response - allow peer to initiate a new handover with different parameters")
277 client.close()
278 print("Returning from dpp_handover_client")
279 return
280
281 print("Remove peer")
282 client.close()
283 print("Done with handover")
284 global only_one
285 if only_one:
286 print("only_one -> stop loop")
287 global continue_loop
288 continue_loop = False
289
290 global no_wait
291 if no_wait:
292 print("Trying to exit..")
293 global terminate_now
294 terminate_now = True
295
296 print("Returning from dpp_handover_client")
297
298class HandoverServer(nfc.handover.HandoverServer):
299 def __init__(self, llc):
300 super(HandoverServer, self).__init__(llc)
301 self.sent_carrier = None
302 self.ho_server_processing = False
303 self.success = False
304 self.try_own = False
305
306 def process_handover_request_message(self, records):
307 self.ho_server_processing = True
308 clear_raw_mode()
309 print("\nHandoverServer - request received: " + str(records))
310
311 carrier = None
312 hs = ndef.HandoverSelectRecord('1.4')
313 sel = [hs]
314
315 found = False
316
317 for carrier in records:
318 if isinstance(carrier, ndef.HandoverRequestRecord):
319 continue
320 print("Remote carrier type: " + carrier.type)
321 if carrier.type == "application/vnd.wfa.dpp":
322 print("DPP carrier type match - add DPP carrier record")
323 if len(carrier.data) == 0 or carrier.data[0] != 0:
324 print("URI Identifier Code 'None' not seen")
325 continue
326 uri = carrier.data[1:].decode("utf-8")
327 print("Received DPP URI: " + uri)
328
329 data = wpas_get_nfc_uri(start_listen=False)
330 print("Own URI (pre-processing): %s" % data)
331
332 res = wpas_report_handover_req(uri)
333 if res is None or "FAIL" in res:
334 print("DPP handover request processing failed")
335 continue
336
337 found = True
338 self.received_carrier = carrier
339
340 wpas = wpas_connect()
341 if wpas is None:
342 continue
343 global own_id
344 data = wpas.request("DPP_BOOTSTRAP_GET_URI %d" % own_id).rstrip()
345 if "FAIL" in data:
346 continue
347 print("Own URI (post-processing): %s" % data)
348 uri = ndef.UriRecord(data)
349 print("Own bootstrapping NFC URI record: " + str(uri))
350
351 info = wpas.request("DPP_BOOTSTRAP_INFO %d" % own_id)
352 freq = None
353 for line in info.splitlines():
354 if line.startswith("use_freq="):
355 freq = int(line.split('=')[1])
356 if freq is None:
357 print("No channel negotiated over NFC - use channel 1")
358 freq = 2412
359 res = wpas.request("DPP_LISTEN %d" % freq)
360 if "OK" not in res:
361 print("Failed to start DPP listen")
362 break
363
364 carrier = ndef.Record('application/vnd.wfa.dpp', 'A', uri.data)
365 print("Own DPP carrier record: " + str(carrier))
366 hs.add_alternative_carrier('active', carrier.name)
367 sel = [hs, carrier]
368 break
369
370 summary("Sending handover select: " + str(sel))
371 if found:
372 self.success = True
373 else:
374 self.try_own = True
375 return sel
376
377def clear_raw_mode():
378 import sys, tty, termios
379 global prev_tcgetattr, in_raw_mode
380 if not in_raw_mode:
381 return
382 fd = sys.stdin.fileno()
383 termios.tcsetattr(fd, termios.TCSADRAIN, prev_tcgetattr)
384 in_raw_mode = False
385
386def getch():
387 import sys, tty, termios, select
388 global prev_tcgetattr, in_raw_mode
389 fd = sys.stdin.fileno()
390 prev_tcgetattr = termios.tcgetattr(fd)
391 ch = None
392 try:
393 tty.setraw(fd)
394 in_raw_mode = True
395 [i, o, e] = select.select([fd], [], [], 0.05)
396 if i:
397 ch = sys.stdin.read(1)
398 finally:
399 termios.tcsetattr(fd, termios.TCSADRAIN, prev_tcgetattr)
400 in_raw_mode = False
401 return ch
402
403def dpp_tag_read(tag):
404 success = False
405 for record in tag.ndef.records:
406 print(record)
407 print("record type " + record.type)
408 if record.type == "application/vnd.wfa.dpp":
409 summary("DPP HS tag - send to wpa_supplicant")
410 success = dpp_hs_tag_read(record)
411 break
412 if isinstance(record, ndef.UriRecord):
413 print("URI record: uri=" + record.uri)
414 print("URI record: iri=" + record.iri)
415 if record.iri.startswith("DPP:"):
416 print("DPP URI")
417 if not dpp_nfc_uri_process(record.iri):
418 break
419 success = True
420 else:
421 print("Ignore unknown URI")
422 break
423
424 if success:
425 success_report("Tag read succeeded")
426
427 return success
428
429def rdwr_connected_write_tag(tag):
430 summary("Tag found - writing - " + str(tag))
431 if not tag.ndef.is_writeable:
432 print("Not a writable tag")
433 return
434 global dpp_tag_data
435 if tag.ndef.capacity < len(dpp_tag_data):
436 print("Not enough room for the message")
437 return
438 tag.ndef.records = dpp_tag_data
439 success_report("Tag write succeeded")
440 print("Done - remove tag")
441 global only_one
442 if only_one:
443 global continue_loop
444 continue_loop = False
445 global dpp_sel_wait_remove
446 return dpp_sel_wait_remove
447
448def write_nfc_uri(clf, wait_remove=True):
449 print("Write NFC URI record")
450 data = wpas_get_nfc_uri()
451 if data is None:
452 summary("Could not get NFC URI from wpa_supplicant")
453 return
454
455 global dpp_sel_wait_remove
456 dpp_sel_wait_remove = wait_remove
457 print("URI: %s" % data)
458 uri = ndef.UriRecord(data)
459 print(uri)
460
461 print("Touch an NFC tag")
462 global dpp_tag_data
463 dpp_tag_data = [uri]
464 clf.connect(rdwr={'on-connect': rdwr_connected_write_tag})
465
466def write_nfc_hs(clf, wait_remove=True):
467 print("Write NFC Handover Select record on a tag")
468 data = wpas_get_nfc_uri()
469 if data is None:
470 summary("Could not get NFC URI from wpa_supplicant")
471 return
472
473 global dpp_sel_wait_remove
474 dpp_sel_wait_remove = wait_remove
475 print("URI: %s" % data)
476 uri = ndef.UriRecord(data)
477 print(uri)
478 carrier = ndef.Record('application/vnd.wfa.dpp', 'A', uri.data)
479 hs = ndef.HandoverSelectRecord('1.4')
480 hs.add_alternative_carrier('active', carrier.name)
481 print(hs)
482 print(carrier)
483
484 print("Touch an NFC tag")
485 global dpp_tag_data
486 dpp_tag_data = [hs, carrier]
487 print(dpp_tag_data)
488 clf.connect(rdwr={'on-connect': rdwr_connected_write_tag})
489
490def rdwr_connected(tag):
491 global only_one, no_wait
492 summary("Tag connected: " + str(tag))
493
494 if tag.ndef:
495 print("NDEF tag: " + tag.type)
496 print(tag.ndef.records)
497 success = dpp_tag_read(tag)
498 if only_one and success:
499 global continue_loop
500 continue_loop = False
501 else:
502 summary("Not an NDEF tag - remove tag")
503 return True
504
505 return not no_wait
506
507def llcp_worker(llc):
508 global init_on_touch
509 if init_on_touch:
510 print("Starting handover client")
511 dpp_handover_client(llc)
512 print("Exiting llcp_worker thread (init_in_touch)")
513 return
514
515 global no_input
516 if no_input:
517 print("Wait for handover to complete")
518 else:
519 print("Wait for handover to complete - press 'i' to initiate")
520 global srv
521 global wait_connection
522 while not wait_connection and srv.sent_carrier is None:
523 if srv.try_own:
524 srv.try_own = False
525 print("Try to initiate another handover with own parameters")
526 dpp_handover_client(llc)
527 print("Exiting llcp_worker thread (retry with own parameters)")
528 return
529 if srv.ho_server_processing:
530 time.sleep(0.025)
531 elif no_input:
532 time.sleep(0.5)
533 else:
534 res = getch()
535 if res != 'i':
536 continue
537 clear_raw_mode()
538 print("Starting handover client")
539 dpp_handover_client(llc)
540 print("Exiting llcp_worker thread (manual init)")
541 return
542
543 clear_raw_mode()
544 print("\rExiting llcp_worker thread")
545
546def llcp_startup(llc):
547 print("Start LLCP server")
548 global srv
549 srv = HandoverServer(llc)
550 return llc
551
552def llcp_connected(llc):
553 print("P2P LLCP connected")
554 global wait_connection
555 wait_connection = False
556 global init_on_touch
557 if not init_on_touch:
558 global srv
559 srv.start()
560 if init_on_touch or not no_input:
561 threading.Thread(target=llcp_worker, args=(llc,)).start()
562 return True
563
564def llcp_release(llc):
565 print("LLCP release")
566 return True
567
568def terminate_loop():
569 global terminate_now
570 return terminate_now
571
572def main():
573 clf = nfc.ContactlessFrontend()
574
575 parser = argparse.ArgumentParser(description='nfcpy to wpa_supplicant integration for DPP NFC operations')
576 parser.add_argument('-d', const=logging.DEBUG, default=logging.INFO,
577 action='store_const', dest='loglevel',
578 help='verbose debug output')
579 parser.add_argument('-q', const=logging.WARNING, action='store_const',
580 dest='loglevel', help='be quiet')
581 parser.add_argument('--only-one', '-1', action='store_true',
582 help='run only one operation and exit')
583 parser.add_argument('--init-on-touch', '-I', action='store_true',
584 help='initiate handover on touch')
585 parser.add_argument('--no-wait', action='store_true',
586 help='do not wait for tag to be removed before exiting')
587 parser.add_argument('--ifname', '-i',
588 help='network interface name')
589 parser.add_argument('--no-input', '-a', action='store_true',
590 help='do not use stdout input to initiate handover')
591 parser.add_argument('--tag-read-only', '-t', action='store_true',
592 help='tag read only (do not allow connection handover)')
593 parser.add_argument('--handover-only', action='store_true',
594 help='connection handover only (do not allow tag read)')
595 parser.add_argument('--summary',
596 help='summary file for writing status updates')
597 parser.add_argument('--success',
598 help='success file for writing success update')
599 parser.add_argument('--device', default='usb', help='NFC device to open')
600 parser.add_argument('--chan', default='81/1', help='channel list')
601 parser.add_argument('command', choices=['write-nfc-uri',
602 'write-nfc-hs'],
603 nargs='?')
604 args = parser.parse_args()
605 print(args)
606
607 global only_one
608 only_one = args.only_one
609
610 global no_wait
611 no_wait = args.no_wait
612
613 global chanlist
614 chanlist = args.chan
615
616 logging.basicConfig(level=args.loglevel)
617
618 global init_on_touch
619 init_on_touch = args.init_on_touch
620
621 if args.ifname:
622 global ifname
623 ifname = args.ifname
624 print("Selected ifname " + ifname)
625
626 if args.summary:
627 global summary_file
628 summary_file = args.summary
629
630 if args.success:
631 global success_file
632 success_file = args.success
633
634 if args.no_input:
635 global no_input
636 no_input = True
637
638 clf = nfc.ContactlessFrontend()
639 global wait_connection
640
641 try:
642 if not clf.open(args.device):
643 print("Could not open connection with an NFC device")
644 raise SystemExit
645
646 if args.command == "write-nfc-uri":
647 write_nfc_uri(clf, wait_remove=not args.no_wait)
648 raise SystemExit
649
650 if args.command == "write-nfc-hs":
651 write_nfc_hs(clf, wait_remove=not args.no_wait)
652 raise SystemExit
653
654 global continue_loop
655 while continue_loop:
656 clear_raw_mode()
657 print("\rWaiting for a tag or peer to be touched")
658 wait_connection = True
659 try:
660 if args.tag_read_only:
661 if not clf.connect(rdwr={'on-connect': rdwr_connected}):
662 break
663 elif args.handover_only:
664 if not clf.connect(llcp={'on-startup': llcp_startup,
665 'on-connect': llcp_connected,
666 'on-release': llcp_release},
667 terminate=terminate_loop):
668 break
669 else:
670 if not clf.connect(rdwr={'on-connect': rdwr_connected},
671 llcp={'on-startup': llcp_startup,
672 'on-connect': llcp_connected,
673 'on-release': llcp_release},
674 terminate=terminate_loop):
675 break
676 except Exception as e:
677 print("clf.connect failed: " + str(e))
678 break
679
680 global srv
681 if only_one and srv and srv.success:
682 raise SystemExit
683
684 except KeyboardInterrupt:
685 raise SystemExit
686 finally:
687 clf.close()
688
689 raise SystemExit
690
691if __name__ == '__main__':
692 main()