1#!/usr/bin/python
2#
3# Example nfcpy to hostapd 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 argparse
13
14import nfc
15import nfc.ndef
16import nfc.llcp
17import nfc.handover
18
19import logging
20
21import wpaspy
22
23wpas_ctrl = '/var/run/hostapd'
24continue_loop = True
25summary_file = None
26success_file = None
27
28def summary(txt):
29    print(txt)
30    if summary_file:
31        with open(summary_file, 'a') as f:
32            f.write(txt + "\n")
33
34def success_report(txt):
35    summary(txt)
36    if success_file:
37        with open(success_file, 'a') as f:
38            f.write(txt + "\n")
39
40def wpas_connect():
41    ifaces = []
42    if os.path.isdir(wpas_ctrl):
43        try:
44            ifaces = [os.path.join(wpas_ctrl, i) for i in os.listdir(wpas_ctrl)]
45        except OSError as error:
46            print("Could not find hostapd: ", error)
47            return None
48
49    if len(ifaces) < 1:
50        print("No hostapd control interface found")
51        return None
52
53    for ctrl in ifaces:
54        try:
55            wpas = wpaspy.Ctrl(ctrl)
56            return wpas
57        except Exception as e:
58            pass
59    return None
60
61
62def wpas_tag_read(message):
63    wpas = wpas_connect()
64    if (wpas == None):
65        return False
66    if "FAIL" in wpas.request("WPS_NFC_TAG_READ " + str(message).encode("hex")):
67        return False
68    return True
69
70
71def wpas_get_config_token():
72    wpas = wpas_connect()
73    if (wpas == None):
74        return None
75    ret = wpas.request("WPS_NFC_CONFIG_TOKEN NDEF")
76    if "FAIL" in ret:
77        return None
78    return ret.rstrip().decode("hex")
79
80
81def wpas_get_password_token():
82    wpas = wpas_connect()
83    if (wpas == None):
84        return None
85    ret = wpas.request("WPS_NFC_TOKEN NDEF")
86    if "FAIL" in ret:
87        return None
88    return ret.rstrip().decode("hex")
89
90
91def wpas_get_handover_sel():
92    wpas = wpas_connect()
93    if (wpas == None):
94        return None
95    ret = wpas.request("NFC_GET_HANDOVER_SEL NDEF WPS-CR")
96    if "FAIL" in ret:
97        return None
98    return ret.rstrip().decode("hex")
99
100
101def wpas_report_handover(req, sel):
102    wpas = wpas_connect()
103    if (wpas == None):
104        return None
105    return wpas.request("NFC_REPORT_HANDOVER RESP WPS " +
106                        str(req).encode("hex") + " " +
107                        str(sel).encode("hex"))
108
109
110class HandoverServer(nfc.handover.HandoverServer):
111    def __init__(self, llc):
112        super(HandoverServer, self).__init__(llc)
113        self.ho_server_processing = False
114        self.success = False
115
116    # override to avoid parser error in request/response.pretty() in nfcpy
117    # due to new WSC handover format
118    def _process_request(self, request):
119        summary("received handover request {}".format(request.type))
120        response = nfc.ndef.Message("\xd1\x02\x01Hs\x12")
121        if not request.type == 'urn:nfc:wkt:Hr':
122            summary("not a handover request")
123        else:
124            try:
125                request = nfc.ndef.HandoverRequestMessage(request)
126            except nfc.ndef.DecodeError as e:
127                summary("error decoding 'Hr' message: {}".format(e))
128            else:
129                response = self.process_request(request)
130        summary("send handover response {}".format(response.type))
131        return response
132
133    def process_request(self, request):
134        summary("HandoverServer - request received")
135        try:
136            print("Parsed handover request: " + request.pretty())
137        except Exception as e:
138            print(e)
139        print(str(request).encode("hex"))
140
141        sel = nfc.ndef.HandoverSelectMessage(version="1.2")
142
143        for carrier in request.carriers:
144            print("Remote carrier type: " + carrier.type)
145            if carrier.type == "application/vnd.wfa.wsc":
146                summary("WPS carrier type match - add WPS carrier record")
147                data = wpas_get_handover_sel()
148                if data is None:
149                    summary("Could not get handover select carrier record from hostapd")
150                    continue
151                print("Handover select carrier record from hostapd:")
152                print(data.encode("hex"))
153                if "OK" in wpas_report_handover(carrier.record, data):
154                    success_report("Handover reported successfully")
155                else:
156                    summary("Handover report rejected")
157
158                message = nfc.ndef.Message(data);
159                sel.add_carrier(message[0], "active", message[1:])
160
161        print("Handover select:")
162        try:
163            print(sel.pretty())
164        except Exception as e:
165            print(e)
166        print(str(sel).encode("hex"))
167
168        summary("Sending handover select")
169        self.success = True
170        return sel
171
172
173def wps_tag_read(tag):
174    success = False
175    if len(tag.ndef.message):
176        for record in tag.ndef.message:
177            print("record type " + record.type)
178            if record.type == "application/vnd.wfa.wsc":
179                summary("WPS tag - send to hostapd")
180                success = wpas_tag_read(tag.ndef.message)
181                break
182    else:
183        summary("Empty tag")
184
185    if success:
186        success_report("Tag read succeeded")
187
188    return success
189
190
191def rdwr_connected_write(tag):
192    summary("Tag found - writing - " + str(tag))
193    global write_data
194    tag.ndef.message = str(write_data)
195    success_report("Tag write succeeded")
196    print("Done - remove tag")
197    global only_one
198    if only_one:
199        global continue_loop
200        continue_loop = False
201    global write_wait_remove
202    while write_wait_remove and tag.is_present:
203        time.sleep(0.1)
204
205def wps_write_config_tag(clf, wait_remove=True):
206    summary("Write WPS config token")
207    global write_data, write_wait_remove
208    write_wait_remove = wait_remove
209    write_data = wpas_get_config_token()
210    if write_data == None:
211        summary("Could not get WPS config token from hostapd")
212        return
213
214    print("Touch an NFC tag")
215    clf.connect(rdwr={'on-connect': rdwr_connected_write})
216
217
218def wps_write_password_tag(clf, wait_remove=True):
219    summary("Write WPS password token")
220    global write_data, write_wait_remove
221    write_wait_remove = wait_remove
222    write_data = wpas_get_password_token()
223    if write_data == None:
224        summary("Could not get WPS password token from hostapd")
225        return
226
227    print("Touch an NFC tag")
228    clf.connect(rdwr={'on-connect': rdwr_connected_write})
229
230
231def rdwr_connected(tag):
232    global only_one, no_wait
233    summary("Tag connected: " + str(tag))
234
235    if tag.ndef:
236        print("NDEF tag: " + tag.type)
237        try:
238            print(tag.ndef.message.pretty())
239        except Exception as e:
240            print(e)
241        success = wps_tag_read(tag)
242        if only_one and success:
243            global continue_loop
244            continue_loop = False
245    else:
246        summary("Not an NDEF tag - remove tag")
247        return True
248
249    return not no_wait
250
251
252def llcp_startup(clf, llc):
253    print("Start LLCP server")
254    global srv
255    srv = HandoverServer(llc)
256    return llc
257
258def llcp_connected(llc):
259    print("P2P LLCP connected")
260    global wait_connection
261    wait_connection = False
262    global srv
263    srv.start()
264    return True
265
266
267def main():
268    clf = nfc.ContactlessFrontend()
269
270    parser = argparse.ArgumentParser(description='nfcpy to hostapd integration for WPS NFC operations')
271    parser.add_argument('-d', const=logging.DEBUG, default=logging.INFO,
272                        action='store_const', dest='loglevel',
273                        help='verbose debug output')
274    parser.add_argument('-q', const=logging.WARNING, action='store_const',
275                        dest='loglevel', help='be quiet')
276    parser.add_argument('--only-one', '-1', action='store_true',
277                        help='run only one operation and exit')
278    parser.add_argument('--no-wait', action='store_true',
279                        help='do not wait for tag to be removed before exiting')
280    parser.add_argument('--summary',
281                        help='summary file for writing status updates')
282    parser.add_argument('--success',
283                        help='success file for writing success update')
284    parser.add_argument('command', choices=['write-config',
285                                            'write-password'],
286                        nargs='?')
287    args = parser.parse_args()
288
289    global only_one
290    only_one = args.only_one
291
292    global no_wait
293    no_wait = args.no_wait
294
295    if args.summary:
296        global summary_file
297        summary_file = args.summary
298
299    if args.success:
300        global success_file
301        success_file = args.success
302
303    logging.basicConfig(level=args.loglevel)
304
305    try:
306        if not clf.open("usb"):
307            print("Could not open connection with an NFC device")
308            raise SystemExit
309
310        if args.command == "write-config":
311            wps_write_config_tag(clf, wait_remove=not args.no_wait)
312            raise SystemExit
313
314        if args.command == "write-password":
315            wps_write_password_tag(clf, wait_remove=not args.no_wait)
316            raise SystemExit
317
318        global continue_loop
319        while continue_loop:
320            print("Waiting for a tag or peer to be touched")
321            wait_connection = True
322            try:
323                if not clf.connect(rdwr={'on-connect': rdwr_connected},
324                                   llcp={'on-startup': llcp_startup,
325                                         'on-connect': llcp_connected}):
326                    break
327            except Exception as e:
328                print("clf.connect failed")
329
330            global srv
331            if only_one and srv and srv.success:
332                raise SystemExit
333
334    except KeyboardInterrupt:
335        raise SystemExit
336    finally:
337        clf.close()
338
339    raise SystemExit
340
341if __name__ == '__main__':
342    main()
343