1#!/usr/bin/python
2#
3# Example nfcpy to wpa_supplicant wrapper for WPS NFC operations
4# Copyright (c) 2012-2013, Jouni Malinen <j@w1.fi>
5#
6# This software may be distributed under the terms of the BSD license.
7# See README for more details.
8
9import os
10import sys
11import time
12import random
13import threading
14import argparse
15
16import nfc
17import nfc.ndef
18import nfc.llcp
19import nfc.handover
20
21import logging
22
23import wpaspy
24
25wpas_ctrl = '/var/run/wpa_supplicant'
26srv = None
27continue_loop = True
28terminate_now = False
29summary_file = None
30success_file = None
31
32def summary(txt):
33    print txt
34    if summary_file:
35        with open(summary_file, 'a') as f:
36            f.write(txt + "\n")
37
38def success_report(txt):
39    summary(txt)
40    if success_file:
41        with open(success_file, 'a') as f:
42            f.write(txt + "\n")
43
44def wpas_connect():
45    ifaces = []
46    if os.path.isdir(wpas_ctrl):
47        try:
48            ifaces = [os.path.join(wpas_ctrl, i) for i in os.listdir(wpas_ctrl)]
49        except OSError, error:
50            print "Could not find wpa_supplicant: ", error
51            return None
52
53    if len(ifaces) < 1:
54        print "No wpa_supplicant control interface found"
55        return None
56
57    for ctrl in ifaces:
58        try:
59            wpas = wpaspy.Ctrl(ctrl)
60            return wpas
61        except Exception, e:
62            pass
63    return None
64
65
66def wpas_tag_read(message):
67    wpas = wpas_connect()
68    if (wpas == None):
69        return False
70    if "FAIL" in wpas.request("WPS_NFC_TAG_READ " + str(message).encode("hex")):
71        return False
72    return True
73
74def wpas_get_config_token(id=None):
75    wpas = wpas_connect()
76    if (wpas == None):
77        return None
78    if id:
79        ret = wpas.request("WPS_NFC_CONFIG_TOKEN NDEF " + id)
80    else:
81        ret = wpas.request("WPS_NFC_CONFIG_TOKEN NDEF")
82    if "FAIL" in ret:
83        return None
84    return ret.rstrip().decode("hex")
85
86
87def wpas_get_er_config_token(uuid):
88    wpas = wpas_connect()
89    if (wpas == None):
90        return None
91    ret = wpas.request("WPS_ER_NFC_CONFIG_TOKEN NDEF " + uuid)
92    if "FAIL" in ret:
93        return None
94    return ret.rstrip().decode("hex")
95
96
97def wpas_get_password_token():
98    wpas = wpas_connect()
99    if (wpas == None):
100        return None
101    ret = wpas.request("WPS_NFC_TOKEN NDEF")
102    if "FAIL" in ret:
103        return None
104    return ret.rstrip().decode("hex")
105
106def wpas_get_handover_req():
107    wpas = wpas_connect()
108    if (wpas == None):
109        return None
110    ret = wpas.request("NFC_GET_HANDOVER_REQ NDEF WPS-CR")
111    if "FAIL" in ret:
112        return None
113    return ret.rstrip().decode("hex")
114
115
116def wpas_get_handover_sel(uuid):
117    wpas = wpas_connect()
118    if (wpas == None):
119        return None
120    if uuid is None:
121        res = wpas.request("NFC_GET_HANDOVER_SEL NDEF WPS-CR").rstrip()
122    else:
123	res = wpas.request("NFC_GET_HANDOVER_SEL NDEF WPS-CR " + uuid).rstrip()
124    if "FAIL" in res:
125	return None
126    return res.decode("hex")
127
128
129def wpas_report_handover(req, sel, type):
130    wpas = wpas_connect()
131    if (wpas == None):
132        return None
133    return wpas.request("NFC_REPORT_HANDOVER " + type + " WPS " +
134                        str(req).encode("hex") + " " +
135                        str(sel).encode("hex"))
136
137
138class HandoverServer(nfc.handover.HandoverServer):
139    def __init__(self, llc):
140        super(HandoverServer, self).__init__(llc)
141        self.sent_carrier = None
142        self.ho_server_processing = False
143        self.success = False
144
145    # override to avoid parser error in request/response.pretty() in nfcpy
146    # due to new WSC handover format
147    def _process_request(self, request):
148        summary("received handover request {}".format(request.type))
149        response = nfc.ndef.Message("\xd1\x02\x01Hs\x12")
150        if not request.type == 'urn:nfc:wkt:Hr':
151            summary("not a handover request")
152        else:
153            try:
154                request = nfc.ndef.HandoverRequestMessage(request)
155            except nfc.ndef.DecodeError as e:
156                summary("error decoding 'Hr' message: {}".format(e))
157            else:
158                response = self.process_request(request)
159        summary("send handover response {}".format(response.type))
160        return response
161
162    def process_request(self, request):
163        self.ho_server_processing = True
164        summary("HandoverServer - request received")
165        try:
166            print "Parsed handover request: " + request.pretty()
167        except Exception, e:
168            print e
169
170        sel = nfc.ndef.HandoverSelectMessage(version="1.2")
171
172        for carrier in request.carriers:
173            print "Remote carrier type: " + carrier.type
174            if carrier.type == "application/vnd.wfa.wsc":
175                summary("WPS carrier type match - add WPS carrier record")
176                data = wpas_get_handover_sel(self.uuid)
177                if data is None:
178                    summary("Could not get handover select carrier record from wpa_supplicant")
179                    continue
180                print "Handover select carrier record from wpa_supplicant:"
181                print data.encode("hex")
182                self.sent_carrier = data
183                if "OK" in wpas_report_handover(carrier.record, self.sent_carrier, "RESP"):
184                    success_report("Handover reported successfully (responder)")
185                else:
186                    summary("Handover report rejected (responder)")
187
188                message = nfc.ndef.Message(data);
189                sel.add_carrier(message[0], "active", message[1:])
190
191        print "Handover select:"
192        try:
193            print sel.pretty()
194        except Exception, e:
195            print e
196        print str(sel).encode("hex")
197
198        summary("Sending handover select")
199        self.success = True
200        return sel
201
202
203def wps_handover_init(llc):
204    summary("Trying to initiate WPS handover")
205
206    data = wpas_get_handover_req()
207    if (data == None):
208        summary("Could not get handover request carrier record from wpa_supplicant")
209        return
210    print "Handover request carrier record from wpa_supplicant: " + data.encode("hex")
211
212    message = nfc.ndef.HandoverRequestMessage(version="1.2")
213    message.nonce = random.randint(0, 0xffff)
214    datamsg = nfc.ndef.Message(data)
215    message.add_carrier(datamsg[0], "active", datamsg[1:])
216
217    print "Handover request:"
218    try:
219        print message.pretty()
220    except Exception, e:
221        print e
222    print str(message).encode("hex")
223
224    client = nfc.handover.HandoverClient(llc)
225    try:
226        summary("Trying to initiate NFC connection handover")
227        client.connect()
228        summary("Connected for handover")
229    except nfc.llcp.ConnectRefused:
230        summary("Handover connection refused")
231        client.close()
232        return
233    except Exception, e:
234        summary("Other exception: " + str(e))
235        client.close()
236        return
237
238    summary("Sending handover request")
239
240    if not client.send(message):
241        summary("Failed to send handover request")
242        client.close()
243        return
244
245    summary("Receiving handover response")
246    message = client._recv()
247    if message is None:
248        summary("No response received")
249        client.close()
250        return
251    if message.type != "urn:nfc:wkt:Hs":
252        summary("Response was not Hs - received: " + message.type)
253        client.close()
254        return
255
256    print "Received message"
257    try:
258        print message.pretty()
259    except Exception, e:
260        print e
261    print str(message).encode("hex")
262    message = nfc.ndef.HandoverSelectMessage(message)
263    summary("Handover select received")
264    try:
265        print message.pretty()
266    except Exception, e:
267        print e
268
269    for carrier in message.carriers:
270        print "Remote carrier type: " + carrier.type
271        if carrier.type == "application/vnd.wfa.wsc":
272            print "WPS carrier type match - send to wpa_supplicant"
273            if "OK" in wpas_report_handover(data, carrier.record, "INIT"):
274                success_report("Handover reported successfully (initiator)")
275            else:
276                summary("Handover report rejected (initiator)")
277            # nfcpy does not support the new format..
278            #wifi = nfc.ndef.WifiConfigRecord(carrier.record)
279            #print wifi.pretty()
280
281    print "Remove peer"
282    client.close()
283    print "Done with handover"
284    global only_one
285    if only_one:
286        global continue_loop
287        continue_loop = False
288
289    global no_wait
290    if no_wait:
291        print "Trying to exit.."
292        global terminate_now
293        terminate_now = True
294
295def wps_tag_read(tag, wait_remove=True):
296    success = False
297    if len(tag.ndef.message):
298        for record in tag.ndef.message:
299            print "record type " + record.type
300            if record.type == "application/vnd.wfa.wsc":
301                summary("WPS tag - send to wpa_supplicant")
302                success = wpas_tag_read(tag.ndef.message)
303                break
304    else:
305        summary("Empty tag")
306
307    if success:
308        success_report("Tag read succeeded")
309
310    if wait_remove:
311        print "Remove tag"
312        while tag.is_present:
313            time.sleep(0.1)
314
315    return success
316
317
318def rdwr_connected_write(tag):
319    summary("Tag found - writing - " + str(tag))
320    global write_data
321    tag.ndef.message = str(write_data)
322    success_report("Tag write succeeded")
323    print "Done - remove tag"
324    global only_one
325    if only_one:
326        global continue_loop
327        continue_loop = False
328    global write_wait_remove
329    while write_wait_remove and tag.is_present:
330        time.sleep(0.1)
331
332def wps_write_config_tag(clf, id=None, wait_remove=True):
333    print "Write WPS config token"
334    global write_data, write_wait_remove
335    write_wait_remove = wait_remove
336    write_data = wpas_get_config_token(id)
337    if write_data == None:
338        print "Could not get WPS config token from wpa_supplicant"
339        sys.exit(1)
340        return
341    print "Touch an NFC tag"
342    clf.connect(rdwr={'on-connect': rdwr_connected_write})
343
344
345def wps_write_er_config_tag(clf, uuid, wait_remove=True):
346    print "Write WPS ER config token"
347    global write_data, write_wait_remove
348    write_wait_remove = wait_remove
349    write_data = wpas_get_er_config_token(uuid)
350    if write_data == None:
351        print "Could not get WPS config token from wpa_supplicant"
352        return
353
354    print "Touch an NFC tag"
355    clf.connect(rdwr={'on-connect': rdwr_connected_write})
356
357
358def wps_write_password_tag(clf, wait_remove=True):
359    print "Write WPS password token"
360    global write_data, write_wait_remove
361    write_wait_remove = wait_remove
362    write_data = wpas_get_password_token()
363    if write_data == None:
364        print "Could not get WPS password token from wpa_supplicant"
365        return
366
367    print "Touch an NFC tag"
368    clf.connect(rdwr={'on-connect': rdwr_connected_write})
369
370
371def rdwr_connected(tag):
372    global only_one, no_wait
373    summary("Tag connected: " + str(tag))
374
375    if tag.ndef:
376        print "NDEF tag: " + tag.type
377        try:
378            print tag.ndef.message.pretty()
379        except Exception, e:
380            print e
381        success = wps_tag_read(tag, not only_one)
382        if only_one and success:
383            global continue_loop
384            continue_loop = False
385    else:
386        summary("Not an NDEF tag - remove tag")
387        return True
388
389    return not no_wait
390
391
392def llcp_worker(llc):
393    global arg_uuid
394    if arg_uuid is None:
395        wps_handover_init(llc)
396        print "Exiting llcp_worker thread"
397        return
398
399    global srv
400    global wait_connection
401    while not wait_connection and srv.sent_carrier is None:
402        if srv.ho_server_processing:
403            time.sleep(0.025)
404
405def llcp_startup(clf, llc):
406    global arg_uuid
407    if arg_uuid:
408        print "Start LLCP server"
409        global srv
410        srv = HandoverServer(llc)
411        if arg_uuid is "ap":
412            print "Trying to handle WPS handover"
413            srv.uuid = None
414        else:
415            print "Trying to handle WPS handover with AP " + arg_uuid
416            srv.uuid = arg_uuid
417    return llc
418
419def llcp_connected(llc):
420    print "P2P LLCP connected"
421    global wait_connection
422    wait_connection = False
423    global arg_uuid
424    if arg_uuid:
425        global srv
426        srv.start()
427    else:
428        threading.Thread(target=llcp_worker, args=(llc,)).start()
429    print "llcp_connected returning"
430    return True
431
432
433def terminate_loop():
434    global terminate_now
435    return terminate_now
436
437def main():
438    clf = nfc.ContactlessFrontend()
439
440    parser = argparse.ArgumentParser(description='nfcpy to wpa_supplicant integration for WPS NFC operations')
441    parser.add_argument('-d', const=logging.DEBUG, default=logging.INFO,
442                        action='store_const', dest='loglevel',
443                        help='verbose debug output')
444    parser.add_argument('-q', const=logging.WARNING, action='store_const',
445                        dest='loglevel', help='be quiet')
446    parser.add_argument('--only-one', '-1', action='store_true',
447                        help='run only one operation and exit')
448    parser.add_argument('--no-wait', action='store_true',
449                        help='do not wait for tag to be removed before exiting')
450    parser.add_argument('--uuid',
451                        help='UUID of an AP (used for WPS ER operations)')
452    parser.add_argument('--id',
453                        help='network id (used for WPS ER operations)')
454    parser.add_argument('--summary',
455                        help='summary file for writing status updates')
456    parser.add_argument('--success',
457                        help='success file for writing success update')
458    parser.add_argument('command', choices=['write-config',
459                                            'write-er-config',
460                                            'write-password'],
461                        nargs='?')
462    args = parser.parse_args()
463
464    global arg_uuid
465    arg_uuid = args.uuid
466
467    global only_one
468    only_one = args.only_one
469
470    global no_wait
471    no_wait = args.no_wait
472
473    if args.summary:
474        global summary_file
475        summary_file = args.summary
476
477    if args.success:
478        global success_file
479        success_file = args.success
480
481    logging.basicConfig(level=args.loglevel)
482
483    try:
484        if not clf.open("usb"):
485            print "Could not open connection with an NFC device"
486            raise SystemExit
487
488        if args.command == "write-config":
489            wps_write_config_tag(clf, id=args.id, wait_remove=not args.no_wait)
490            raise SystemExit
491
492        if args.command == "write-er-config":
493            wps_write_er_config_tag(clf, args.uuid, wait_remove=not args.no_wait)
494            raise SystemExit
495
496        if args.command == "write-password":
497            wps_write_password_tag(clf, wait_remove=not args.no_wait)
498            raise SystemExit
499
500        global continue_loop
501        while continue_loop:
502            print "Waiting for a tag or peer to be touched"
503            wait_connection = True
504            try:
505                if not clf.connect(rdwr={'on-connect': rdwr_connected},
506                                   llcp={'on-startup': llcp_startup,
507                                         'on-connect': llcp_connected},
508                                   terminate=terminate_loop):
509                    break
510            except Exception, e:
511                print "clf.connect failed"
512
513            global srv
514            if only_one and srv and srv.success:
515                raise SystemExit
516
517    except KeyboardInterrupt:
518        raise SystemExit
519    finally:
520        clf.close()
521
522    raise SystemExit
523
524if __name__ == '__main__':
525    main()
526