]>
Commit | Line | Data |
---|---|---|
3c5fce2b OM |
1 | //----------------------------------------------------------------------------- |
2 | // Copyright (C) 2017 Merlok | |
3 | // | |
4 | // This code is licensed to you under the terms of the GNU GPL, version 2 or, | |
5 | // at your option, any later version. See the LICENSE.txt file for the text of | |
6 | // the license. | |
7 | //----------------------------------------------------------------------------- | |
8 | // EMV commands | |
9 | //----------------------------------------------------------------------------- | |
10 | ||
3ded0f97 | 11 | #include <ctype.h> |
3c5fce2b | 12 | #include "cmdemv.h" |
d03fb293 | 13 | #include "test/cryptotest.h" |
556826b5 | 14 | #include <jansson.h> |
3c5fce2b OM |
15 | |
16 | int UsageCmdHFEMVSelect(void) { | |
17 | PrintAndLog("HELP : Executes select applet command:\n"); | |
18 | PrintAndLog("Usage: hf emv select [-s][-k][-a][-t] <HEX applet AID>\n"); | |
19 | PrintAndLog(" Options:"); | |
20 | PrintAndLog(" -s : select card"); | |
21 | PrintAndLog(" -k : keep field for next command"); | |
22 | PrintAndLog(" -a : show APDU reqests and responses\n"); | |
23 | PrintAndLog(" -t : TLV decode results\n"); | |
24 | PrintAndLog("Samples:"); | |
25 | PrintAndLog(" hf emv select -s a00000000101 -> select card, select applet"); | |
26 | PrintAndLog(" hf emv select -s -t a00000000101 -> select card, select applet, show result in TLV"); | |
27 | return 0; | |
28 | } | |
29 | ||
30 | int CmdHFEMVSelect(const char *cmd) { | |
31 | uint8_t data[APDU_AID_LEN] = {0}; | |
32 | int datalen = 0; | |
33 | bool activateField = false; | |
34 | bool leaveSignalON = false; | |
35 | bool decodeTLV = false; | |
36 | ||
37 | if (strlen(cmd) < 1) { | |
38 | UsageCmdHFEMVSelect(); | |
39 | return 0; | |
40 | } | |
41 | ||
42 | SetAPDULogging(false); | |
43 | ||
44 | int cmdp = 0; | |
45 | while(param_getchar(cmd, cmdp) != 0x00) { | |
46 | char c = param_getchar(cmd, cmdp); | |
47 | if ((c == '-') && (param_getlength(cmd, cmdp) == 2)) | |
48 | switch (param_getchar_indx(cmd, 1, cmdp)) { | |
49 | case 'h': | |
50 | case 'H': | |
51 | UsageCmdHFEMVSelect(); | |
52 | return 0; | |
53 | case 's': | |
54 | case 'S': | |
55 | activateField = true; | |
56 | break; | |
57 | case 'k': | |
58 | case 'K': | |
59 | leaveSignalON = true; | |
60 | break; | |
61 | case 'a': | |
62 | case 'A': | |
63 | SetAPDULogging(true); | |
64 | break; | |
65 | case 't': | |
66 | case 'T': | |
67 | decodeTLV = true; | |
68 | break; | |
69 | default: | |
70 | PrintAndLog("Unknown parameter '%c'", param_getchar_indx(cmd, 1, cmdp)); | |
71 | return 1; | |
72 | } | |
73 | ||
3ded0f97 | 74 | if (isxdigit((unsigned char)c)) { |
3c5fce2b OM |
75 | switch(param_gethex_to_eol(cmd, cmdp, data, sizeof(data), &datalen)) { |
76 | case 1: | |
77 | PrintAndLog("Invalid HEX value."); | |
78 | return 1; | |
79 | case 2: | |
80 | PrintAndLog("AID too large."); | |
81 | return 1; | |
82 | case 3: | |
83 | PrintAndLog("Hex must have even number of digits."); | |
84 | return 1; | |
85 | } | |
86 | ||
87 | // we get all the hex to end of line with spaces | |
88 | break; | |
89 | } | |
90 | ||
91 | ||
92 | cmdp++; | |
93 | } | |
94 | ||
95 | // exec | |
96 | uint8_t buf[APDU_RES_LEN] = {0}; | |
97 | size_t len = 0; | |
98 | uint16_t sw = 0; | |
99 | int res = EMVSelect(activateField, leaveSignalON, data, datalen, buf, sizeof(buf), &len, &sw, NULL); | |
100 | ||
101 | if (sw) | |
102 | PrintAndLog("APDU response status: %04x - %s", sw, GetAPDUCodeDescription(sw >> 8, sw & 0xff)); | |
103 | ||
104 | if (res) | |
105 | return res; | |
106 | ||
107 | if (decodeTLV) | |
108 | TLVPrintFromBuffer(buf, len); | |
109 | ||
110 | return 0; | |
111 | } | |
112 | ||
113 | int UsageCmdHFEMVSearch(void) { | |
114 | PrintAndLog("HELP : Tries to select all applets from applet list:\n"); | |
115 | PrintAndLog("Usage: hf emv search [-s][-k][-a][-t]\n"); | |
116 | PrintAndLog(" Options:"); | |
117 | PrintAndLog(" -s : select card"); | |
118 | PrintAndLog(" -k : keep field for next command"); | |
119 | PrintAndLog(" -a : show APDU reqests and responses\n"); | |
120 | PrintAndLog(" -t : TLV decode results of selected applets\n"); | |
121 | PrintAndLog("Samples:"); | |
122 | PrintAndLog(" hf emv search -s -> select card and search"); | |
123 | PrintAndLog(" hf emv search -s -t -> select card, search and show result in TLV"); | |
124 | return 0; | |
125 | } | |
126 | ||
127 | int CmdHFEMVSearch(const char *cmd) { | |
128 | ||
129 | bool activateField = false; | |
130 | bool leaveSignalON = false; | |
131 | bool decodeTLV = false; | |
132 | ||
133 | if (strlen(cmd) < 1) { | |
134 | UsageCmdHFEMVSearch(); | |
135 | return 0; | |
136 | } | |
137 | ||
138 | SetAPDULogging(false); | |
139 | ||
140 | int cmdp = 0; | |
141 | while(param_getchar(cmd, cmdp) != 0x00) { | |
142 | char c = param_getchar(cmd, cmdp); | |
143 | if ((c == '-') && (param_getlength(cmd, cmdp) == 2)) | |
144 | switch (param_getchar_indx(cmd, 1, cmdp)) { | |
145 | case 'h': | |
146 | case 'H': | |
147 | UsageCmdHFEMVSearch(); | |
148 | return 0; | |
149 | case 's': | |
150 | case 'S': | |
151 | activateField = true; | |
152 | break; | |
153 | case 'k': | |
154 | case 'K': | |
155 | leaveSignalON = true; | |
156 | break; | |
157 | case 'a': | |
158 | case 'A': | |
159 | SetAPDULogging(true); | |
160 | break; | |
161 | case 't': | |
162 | case 'T': | |
163 | decodeTLV = true; | |
164 | break; | |
165 | default: | |
166 | PrintAndLog("Unknown parameter '%c'", param_getchar_indx(cmd, 1, cmdp)); | |
167 | return 1; | |
168 | } | |
169 | cmdp++; | |
170 | } | |
171 | ||
172 | struct tlvdb *t = NULL; | |
173 | const char *al = "Applets list"; | |
174 | t = tlvdb_fixed(1, strlen(al), (const unsigned char *)al); | |
175 | ||
176 | if (EMVSearch(activateField, leaveSignalON, decodeTLV, t)) { | |
177 | tlvdb_free(t); | |
178 | return 2; | |
179 | } | |
180 | ||
181 | PrintAndLog("Search completed."); | |
182 | ||
183 | // print list here | |
184 | if (!decodeTLV) { | |
185 | TLVPrintAIDlistFromSelectTLV(t); | |
186 | } | |
187 | ||
188 | tlvdb_free(t); | |
189 | ||
190 | return 0; | |
191 | } | |
192 | ||
193 | int UsageCmdHFEMVPPSE(void) { | |
194 | PrintAndLog("HELP : Executes PSE/PPSE select command. It returns list of applet on the card:\n"); | |
195 | PrintAndLog("Usage: hf emv pse [-s][-k][-1][-2][-a][-t]\n"); | |
196 | PrintAndLog(" Options:"); | |
197 | PrintAndLog(" -s : select card"); | |
198 | PrintAndLog(" -k : keep field for next command"); | |
199 | PrintAndLog(" -1 : ppse (1PAY.SYS.DDF01)"); | |
200 | PrintAndLog(" -2 : pse (2PAY.SYS.DDF01)"); | |
201 | PrintAndLog(" -a : show APDU reqests and responses\n"); | |
202 | PrintAndLog(" -t : TLV decode results\n"); | |
203 | PrintAndLog("Samples:"); | |
204 | PrintAndLog(" hf emv pse -s -1 -> select, get pse"); | |
205 | PrintAndLog(" hf emv pse -s -k -2 -> select, get ppse, keep field"); | |
206 | PrintAndLog(" hf emv pse -s -t -2 -> select, get ppse, show result in TLV"); | |
207 | return 0; | |
208 | } | |
209 | ||
210 | int CmdHFEMVPPSE(const char *cmd) { | |
211 | ||
212 | uint8_t PSENum = 2; | |
213 | bool activateField = false; | |
214 | bool leaveSignalON = false; | |
215 | bool decodeTLV = false; | |
216 | ||
217 | if (strlen(cmd) < 1) { | |
218 | UsageCmdHFEMVPPSE(); | |
219 | return 0; | |
220 | } | |
221 | ||
222 | SetAPDULogging(false); | |
223 | ||
224 | int cmdp = 0; | |
225 | while(param_getchar(cmd, cmdp) != 0x00) { | |
226 | char c = param_getchar(cmd, cmdp); | |
227 | if ((c == '-') && (param_getlength(cmd, cmdp) == 2)) | |
228 | switch (param_getchar_indx(cmd, 1, cmdp)) { | |
229 | case 'h': | |
230 | case 'H': | |
231 | UsageCmdHFEMVPPSE(); | |
232 | return 0; | |
233 | case 's': | |
234 | case 'S': | |
235 | activateField = true; | |
236 | break; | |
237 | case 'k': | |
238 | case 'K': | |
239 | leaveSignalON = true; | |
240 | break; | |
241 | case 'a': | |
242 | case 'A': | |
243 | SetAPDULogging(true); | |
244 | break; | |
245 | case 't': | |
246 | case 'T': | |
247 | decodeTLV = true; | |
248 | break; | |
249 | case '1': | |
250 | PSENum = 1; | |
251 | break; | |
252 | case '2': | |
253 | PSENum = 2; | |
254 | break; | |
255 | default: | |
256 | PrintAndLog("Unknown parameter '%c'", param_getchar_indx(cmd, 1, cmdp)); | |
257 | return 1; | |
258 | } | |
259 | cmdp++; | |
260 | } | |
261 | ||
262 | // exec | |
263 | uint8_t buf[APDU_RES_LEN] = {0}; | |
264 | size_t len = 0; | |
265 | uint16_t sw = 0; | |
266 | int res = EMVSelectPSE(activateField, leaveSignalON, PSENum, buf, sizeof(buf), &len, &sw); | |
267 | ||
268 | if (sw) | |
269 | PrintAndLog("APDU response status: %04x - %s", sw, GetAPDUCodeDescription(sw >> 8, sw & 0xff)); | |
270 | ||
271 | if (res) | |
272 | return res; | |
273 | ||
274 | ||
275 | if (decodeTLV) | |
276 | TLVPrintFromBuffer(buf, len); | |
277 | ||
278 | return 0; | |
279 | } | |
280 | ||
281 | int UsageCmdHFEMVExec(void) { | |
282 | PrintAndLog("HELP : Executes EMV contactless transaction:\n"); | |
556826b5 | 283 | PrintAndLog("Usage: hf emv exec [-s][-a][-t][-j][-f][-v][-c][-x][-g]\n"); |
3c5fce2b OM |
284 | PrintAndLog(" Options:"); |
285 | PrintAndLog(" -s : select card"); | |
286 | PrintAndLog(" -a : show APDU reqests and responses\n"); | |
287 | PrintAndLog(" -t : TLV decode results\n"); | |
556826b5 | 288 | PrintAndLog(" -j : load transaction parameters from `emv/defparams.json` file\n"); |
3c5fce2b | 289 | PrintAndLog(" -f : force search AID. Search AID instead of execute PPSE.\n"); |
10d4f823 | 290 | PrintAndLog(" -v : transaction type - qVSDC or M/Chip.\n"); |
291 | PrintAndLog(" -c : transaction type - qVSDC or M/Chip plus CDA (SDAD generation).\n"); | |
292 | PrintAndLog(" -x : transaction type - VSDC. For test only. Not a standart behavior.\n"); | |
d03fb293 | 293 | PrintAndLog(" -g : VISA. generate AC from GPO\n"); |
10d4f823 | 294 | PrintAndLog("By default : transaction type - MSD.\n"); |
3c5fce2b | 295 | PrintAndLog("Samples:"); |
d03fb293 OM |
296 | PrintAndLog(" hf emv exec -s -a -t -> execute MSD transaction"); |
297 | PrintAndLog(" hf emv exec -s -a -t -c -> execute CDA transaction"); | |
3c5fce2b OM |
298 | return 0; |
299 | } | |
300 | ||
556826b5 | 301 | #define TLV_ADD(tag, value)( tlvdb_change_or_add_node(tlvRoot, tag, sizeof(value) - 1, (const unsigned char *)value) ) |
d03fb293 | 302 | #define dreturn(n) {free(pdol_data_tlv);tlvdb_free(tlvSelect);tlvdb_free(tlvRoot);DropField();return n;} |
3c5fce2b | 303 | |
556826b5 OM |
304 | bool HexToBuffer(const char *errormsg, const char *hexvalue, uint8_t * buffer, size_t maxbufferlen, size_t *bufferlen) { |
305 | int buflen = 0; | |
306 | ||
307 | switch(param_gethex_to_eol(hexvalue, 0, buffer, maxbufferlen, &buflen)) { | |
308 | case 1: | |
309 | PrintAndLog("%s Invalid HEX value.", errormsg); | |
310 | return false; | |
311 | case 2: | |
312 | PrintAndLog("%s Hex value too large.", errormsg); | |
313 | return false; | |
314 | case 3: | |
315 | PrintAndLog("%s Hex value must have even number of digits.", errormsg); | |
316 | return false; | |
317 | } | |
318 | ||
319 | if (buflen > maxbufferlen) { | |
320 | PrintAndLog("%s HEX length (%d) more than %d", errormsg, *bufferlen, maxbufferlen); | |
321 | return false; | |
322 | } | |
323 | ||
324 | *bufferlen = buflen; | |
325 | ||
326 | return true; | |
327 | } | |
328 | ||
329 | bool ParamLoadFromJson(struct tlvdb *tlv) { | |
330 | json_t *root; | |
331 | json_error_t error; | |
332 | ||
333 | if (!tlv) { | |
334 | PrintAndLog("ERROR load params: tlv tree is NULL."); | |
335 | return false; | |
336 | } | |
337 | ||
338 | // current path + file name | |
339 | const char *relfname = "emv/defparams.json"; | |
340 | char fname[strlen(get_my_executable_directory()) + strlen(relfname) + 1]; | |
341 | strcpy(fname, get_my_executable_directory()); | |
342 | strcat(fname, relfname); | |
343 | ||
344 | root = json_load_file(fname, 0, &error); | |
345 | if (!root) { | |
346 | PrintAndLog("Load params: json error on line %d: %s", error.line, error.text); | |
347 | return false; | |
348 | } | |
349 | ||
350 | if (!json_is_array(root)) { | |
351 | PrintAndLog("Load params: Invalid json format. root must be array."); | |
352 | return false; | |
353 | } | |
354 | ||
355 | PrintAndLog("Load params: json OK"); | |
356 | ||
357 | for(int i = 0; i < json_array_size(root); i++) { | |
358 | json_t *data, *jtype, *jlength, *jvalue; | |
359 | ||
360 | data = json_array_get(root, i); | |
361 | if(!json_is_object(data)) | |
362 | { | |
363 | PrintAndLog("Load params: data [%d] is not an object", i + 1); | |
364 | json_decref(root); | |
365 | return false; | |
366 | } | |
367 | ||
368 | jtype = json_object_get(data, "type"); | |
369 | if(!json_is_string(jtype)) | |
370 | { | |
371 | PrintAndLog("Load params: data [%d] type is not a string", i + 1); | |
372 | json_decref(root); | |
373 | return false; | |
374 | } | |
375 | const char *tlvType = json_string_value(jtype); | |
376 | ||
377 | jvalue = json_object_get(data, "value"); | |
378 | if(!json_is_string(jvalue)) | |
379 | { | |
380 | PrintAndLog("Load params: data [%d] value is not a string", i + 1); | |
381 | json_decref(root); | |
382 | return false; | |
383 | } | |
384 | const char *tlvValue = json_string_value(jvalue); | |
385 | ||
386 | jlength = json_object_get(data, "length"); | |
387 | if(!json_is_number(jlength)) | |
388 | { | |
389 | PrintAndLog("Load params: data [%d] length is not a number", i + 1); | |
390 | json_decref(root); | |
391 | return false; | |
392 | } | |
393 | ||
394 | int tlvLength = json_integer_value(jlength); | |
395 | if (tlvLength > 250) { | |
396 | PrintAndLog("Load params: data [%d] length more than 250", i + 1); | |
397 | json_decref(root); | |
398 | return false; | |
399 | } | |
400 | ||
401 | PrintAndLog("TLV param: %s[%d]=%s", tlvType, tlvLength, tlvValue); | |
402 | uint8_t buf[251] = {0}; | |
403 | size_t buflen = 0; | |
404 | ||
405 | // here max length must be 4, but now tlv_tag_t is 2-byte var. so let it be 2 by now... TODO: needs refactoring tlv_tag_t... | |
406 | if (!HexToBuffer("TLV Error type:", tlvType, buf, 2, &buflen)) { | |
407 | json_decref(root); | |
408 | return false; | |
409 | } | |
410 | tlv_tag_t tag = 0; | |
411 | for (int i = 0; i < buflen; i++) { | |
412 | tag = (tag << 8) + buf[i]; | |
413 | } | |
414 | ||
415 | if (!HexToBuffer("TLV Error value:", tlvValue, buf, sizeof(buf) - 1, &buflen)) { | |
416 | json_decref(root); | |
417 | return false; | |
418 | } | |
419 | ||
420 | if (buflen != tlvLength) { | |
421 | PrintAndLog("Load params: data [%d] length of HEX must(%d) be identical to length in TLV param(%d)", i + 1, buflen, tlvLength); | |
422 | json_decref(root); | |
423 | return false; | |
424 | } | |
425 | ||
426 | tlvdb_change_or_add_node(tlv, tag, tlvLength, (const unsigned char *)buf); | |
427 | } | |
428 | ||
429 | json_decref(root); | |
430 | ||
431 | return true; | |
432 | } | |
433 | ||
434 | void ParamLoadDefaults(struct tlvdb *tlvRoot) { | |
435 | //9F02:(Amount, authorized (Numeric)) len:6 | |
436 | TLV_ADD(0x9F02, "\x00\x00\x00\x00\x01\x00"); | |
437 | //9F1A:(Terminal Country Code) len:2 | |
438 | TLV_ADD(0x9F1A, "ru"); | |
439 | //5F2A:(Transaction Currency Code) len:2 | |
440 | // USD 840, EUR 978, RUR 810, RUB 643, RUR 810(old), UAH 980, AZN 031, n/a 999 | |
441 | TLV_ADD(0x5F2A, "\x09\x80"); | |
442 | //9A:(Transaction Date) len:3 | |
443 | TLV_ADD(0x9A, "\x00\x00\x00"); | |
444 | //9C:(Transaction Type) len:1 | 00 => Goods and service #01 => Cash | |
445 | TLV_ADD(0x9C, "\x00"); | |
446 | // 9F37 Unpredictable Number len:4 | |
447 | TLV_ADD(0x9F37, "\x01\x02\x03\x04"); | |
448 | // 9F6A Unpredictable Number (MSD for UDOL) len:4 | |
449 | TLV_ADD(0x9F6A, "\x01\x02\x03\x04"); | |
450 | //9F66:(Terminal Transaction Qualifiers (TTQ)) len:4 | |
451 | TLV_ADD(0x9F66, "\x26\x00\x00\x00"); // qVSDC | |
452 | } | |
453 | ||
3c5fce2b OM |
454 | int CmdHFEMVExec(const char *cmd) { |
455 | bool activateField = false; | |
456 | bool showAPDU = false; | |
457 | bool decodeTLV = false; | |
458 | bool forceSearch = false; | |
10d4f823 | 459 | enum TransactionType TrType = TT_MSD; |
d03fb293 | 460 | bool GenACGPO = false; |
556826b5 | 461 | bool paramLoadJSON = false; |
3c5fce2b OM |
462 | |
463 | uint8_t buf[APDU_RES_LEN] = {0}; | |
464 | size_t len = 0; | |
465 | uint16_t sw = 0; | |
466 | uint8_t AID[APDU_AID_LEN] = {0}; | |
467 | size_t AIDlen = 0; | |
d03fb293 OM |
468 | uint8_t ODAiList[4096]; |
469 | size_t ODAiListLen = 0; | |
3c5fce2b OM |
470 | |
471 | int res; | |
472 | ||
d03fb293 OM |
473 | struct tlvdb *tlvSelect = NULL; |
474 | struct tlvdb *tlvRoot = NULL; | |
475 | struct tlv *pdol_data_tlv = NULL; | |
476 | ||
3c5fce2b OM |
477 | if (strlen(cmd) < 1) { |
478 | UsageCmdHFEMVExec(); | |
479 | return 0; | |
480 | } | |
481 | ||
482 | int cmdp = 0; | |
483 | while(param_getchar(cmd, cmdp) != 0x00) { | |
484 | char c = param_getchar(cmd, cmdp); | |
485 | if ((c == '-') && (param_getlength(cmd, cmdp) == 2)) | |
486 | switch (param_getchar_indx(cmd, 1, cmdp)) { | |
487 | case 'h': | |
488 | case 'H': | |
7a7afeba | 489 | UsageCmdHFEMVExec(); |
3c5fce2b OM |
490 | return 0; |
491 | case 's': | |
492 | case 'S': | |
493 | activateField = true; | |
494 | break; | |
495 | case 'a': | |
496 | case 'A': | |
497 | showAPDU = true; | |
498 | break; | |
499 | case 't': | |
500 | case 'T': | |
501 | decodeTLV = true; | |
502 | break; | |
503 | case 'f': | |
504 | case 'F': | |
505 | forceSearch = true; | |
506 | break; | |
10d4f823 | 507 | case 'x': |
508 | case 'X': | |
509 | TrType = TT_VSDC; | |
510 | break; | |
511 | case 'v': | |
512 | case 'V': | |
513 | TrType = TT_QVSDCMCHIP; | |
514 | break; | |
515 | case 'c': | |
516 | case 'C': | |
517 | TrType = TT_CDA; | |
518 | break; | |
d03fb293 OM |
519 | case 'g': |
520 | case 'G': | |
521 | GenACGPO = true; | |
522 | break; | |
556826b5 OM |
523 | case 'j': |
524 | case 'J': | |
525 | paramLoadJSON = true; | |
526 | break; | |
3c5fce2b OM |
527 | default: |
528 | PrintAndLog("Unknown parameter '%c'", param_getchar_indx(cmd, 1, cmdp)); | |
529 | return 1; | |
530 | } | |
531 | cmdp++; | |
532 | } | |
533 | ||
534 | ||
535 | // init applets list tree | |
3c5fce2b OM |
536 | const char *al = "Applets list"; |
537 | tlvSelect = tlvdb_fixed(1, strlen(al), (const unsigned char *)al); | |
538 | ||
539 | // Application Selection | |
540 | // https://www.openscdp.org/scripts/tutorial/emv/applicationselection.html | |
541 | if (!forceSearch) { | |
542 | // PPSE | |
543 | PrintAndLog("\n* PPSE."); | |
544 | SetAPDULogging(showAPDU); | |
545 | res = EMVSearchPSE(activateField, true, decodeTLV, tlvSelect); | |
546 | ||
547 | // check PPSE and select application id | |
548 | if (!res) { | |
549 | TLVPrintAIDlistFromSelectTLV(tlvSelect); | |
550 | EMVSelectApplication(tlvSelect, AID, &AIDlen); | |
551 | } | |
552 | } | |
553 | ||
554 | // Search | |
555 | if (!AIDlen) { | |
556 | PrintAndLog("\n* Search AID in list."); | |
557 | SetAPDULogging(false); | |
558 | if (EMVSearch(activateField, true, decodeTLV, tlvSelect)) { | |
d03fb293 | 559 | dreturn(2); |
3c5fce2b OM |
560 | } |
561 | ||
562 | // check search and select application id | |
563 | TLVPrintAIDlistFromSelectTLV(tlvSelect); | |
564 | EMVSelectApplication(tlvSelect, AID, &AIDlen); | |
565 | } | |
566 | ||
567 | // Init TLV tree | |
3c5fce2b OM |
568 | const char *alr = "Root terminal TLV tree"; |
569 | tlvRoot = tlvdb_fixed(1, strlen(alr), (const unsigned char *)alr); | |
570 | ||
571 | // check if we found EMV application on card | |
572 | if (!AIDlen) { | |
573 | PrintAndLog("Can't select AID. EMV AID not found"); | |
d03fb293 | 574 | dreturn(2); |
3c5fce2b OM |
575 | } |
576 | ||
577 | // Select | |
578 | PrintAndLog("\n* Selecting AID:%s", sprint_hex_inrow(AID, AIDlen)); | |
579 | SetAPDULogging(showAPDU); | |
580 | res = EMVSelect(false, true, AID, AIDlen, buf, sizeof(buf), &len, &sw, tlvRoot); | |
581 | ||
582 | if (res) { | |
583 | PrintAndLog("Can't select AID (%d). Exit...", res); | |
d03fb293 | 584 | dreturn(3); |
3c5fce2b OM |
585 | } |
586 | ||
587 | if (decodeTLV) | |
588 | TLVPrintFromBuffer(buf, len); | |
589 | PrintAndLog("* Selected."); | |
590 | ||
3c5fce2b OM |
591 | PrintAndLog("\n* Init transaction parameters."); |
592 | ||
556826b5 OM |
593 | ParamLoadDefaults(tlvRoot); |
594 | ||
595 | if (paramLoadJSON) { | |
596 | PrintAndLog("* * Transaction parameters loading from JSON..."); | |
597 | ParamLoadFromJson(tlvRoot); | |
598 | } | |
599 | ||
600 | //9F66:(Terminal Transaction Qualifiers (TTQ)) len:4 | |
d03fb293 OM |
601 | char *qVSDC = "\x26\x00\x00\x00"; |
602 | if (GenACGPO) { | |
603 | qVSDC = "\x26\x80\x00\x00"; | |
604 | } | |
10d4f823 | 605 | switch(TrType) { |
606 | case TT_MSD: | |
607 | TLV_ADD(0x9F66, "\x86\x00\x00\x00"); // MSD | |
608 | break; | |
d03fb293 | 609 | // not standard for contactless. just for test. |
10d4f823 | 610 | case TT_VSDC: |
611 | TLV_ADD(0x9F66, "\x46\x00\x00\x00"); // VSDC | |
612 | break; | |
613 | case TT_QVSDCMCHIP: | |
d03fb293 | 614 | TLV_ADD(0x9F66, qVSDC); // qVSDC |
10d4f823 | 615 | break; |
616 | case TT_CDA: | |
d03fb293 | 617 | TLV_ADD(0x9F66, qVSDC); // qVSDC (VISA CDA not enabled) |
10d4f823 | 618 | break; |
619 | default: | |
10d4f823 | 620 | break; |
621 | } | |
d03fb293 | 622 | |
66efdc1f | 623 | TLVPrintFromTLV(tlvRoot); // TODO delete!!! |
3c5fce2b OM |
624 | |
625 | PrintAndLog("\n* Calc PDOL."); | |
d03fb293 | 626 | pdol_data_tlv = dol_process(tlvdb_get(tlvRoot, 0x9f38, NULL), tlvRoot, 0x83); |
3c5fce2b OM |
627 | if (!pdol_data_tlv){ |
628 | PrintAndLog("ERROR: can't create PDOL TLV."); | |
d03fb293 | 629 | dreturn(4); |
3c5fce2b OM |
630 | } |
631 | ||
632 | size_t pdol_data_tlv_data_len; | |
633 | unsigned char *pdol_data_tlv_data = tlv_encode(pdol_data_tlv, &pdol_data_tlv_data_len); | |
634 | if (!pdol_data_tlv_data) { | |
635 | PrintAndLog("ERROR: can't create PDOL data."); | |
d03fb293 | 636 | dreturn(4); |
3c5fce2b OM |
637 | } |
638 | PrintAndLog("PDOL data[%d]: %s", pdol_data_tlv_data_len, sprint_hex(pdol_data_tlv_data, pdol_data_tlv_data_len)); | |
639 | ||
3c5fce2b OM |
640 | PrintAndLog("\n* GPO."); |
641 | res = EMVGPO(true, pdol_data_tlv_data, pdol_data_tlv_data_len, buf, sizeof(buf), &len, &sw, tlvRoot); | |
642 | ||
10d4f823 | 643 | free(pdol_data_tlv_data); |
d03fb293 | 644 | //free(pdol_data_tlv); --- free on exit. |
3c5fce2b OM |
645 | |
646 | if (res) { | |
647 | PrintAndLog("GPO error(%d): %4x. Exit...", res, sw); | |
d03fb293 | 648 | dreturn(5); |
3c5fce2b OM |
649 | } |
650 | ||
651 | // process response template format 1 [id:80 2b AIP + x4b AFL] and format 2 [id:77 TLV] | |
652 | if (buf[0] == 0x80) { | |
3c5fce2b OM |
653 | if (decodeTLV){ |
654 | PrintAndLog("GPO response format1:"); | |
655 | TLVPrintFromBuffer(buf, len); | |
656 | } | |
3c5fce2b | 657 | |
66efdc1f | 658 | if (len < 4 || (len - 4) % 4) { |
659 | PrintAndLog("ERROR: GPO response format1 parsing error. length=%d", len); | |
660 | } else { | |
661 | // AIP | |
662 | struct tlvdb * f1AIP = tlvdb_fixed(0x82, 2, buf + 2); | |
663 | tlvdb_add(tlvRoot, f1AIP); | |
664 | if (decodeTLV){ | |
665 | PrintAndLog("\n* * Decode response format 1 (0x80) AIP and AFL:"); | |
666 | TLVPrintFromTLV(f1AIP); | |
667 | } | |
668 | ||
669 | // AFL | |
670 | struct tlvdb * f1AFL = tlvdb_fixed(0x94, len - 4, buf + 2 + 2); | |
671 | tlvdb_add(tlvRoot, f1AFL); | |
672 | if (decodeTLV) | |
673 | TLVPrintFromTLV(f1AFL); | |
674 | } | |
675 | } else { | |
3c5fce2b OM |
676 | if (decodeTLV) |
677 | TLVPrintFromBuffer(buf, len); | |
678 | } | |
679 | ||
66efdc1f | 680 | // extract PAN from track2 |
681 | { | |
682 | const struct tlv *track2 = tlvdb_get(tlvRoot, 0x57, NULL); | |
683 | if (!tlvdb_get(tlvRoot, 0x5a, NULL) && track2 && track2->len >= 8) { | |
684 | struct tlvdb *pan = GetPANFromTrack2(track2); | |
685 | if (pan) { | |
686 | tlvdb_add(tlvRoot, pan); | |
687 | ||
688 | const struct tlv *pantlv = tlvdb_get(tlvRoot, 0x5a, NULL); | |
689 | PrintAndLog("\n* * Extracted PAN from track2: %s", sprint_hex(pantlv->value, pantlv->len)); | |
690 | } else { | |
691 | PrintAndLog("\n* * WARNING: Can't extract PAN from track2."); | |
692 | } | |
693 | } | |
694 | } | |
695 | ||
3c5fce2b OM |
696 | PrintAndLog("\n* Read records from AFL."); |
697 | const struct tlv *AFL = tlvdb_get(tlvRoot, 0x94, NULL); | |
698 | if (!AFL || !AFL->len) { | |
699 | PrintAndLog("WARNING: AFL not found."); | |
700 | } | |
701 | ||
702 | while(AFL && AFL->len) { | |
703 | if (AFL->len % 4) { | |
704 | PrintAndLog("ERROR: Wrong AFL length: %d", AFL->len); | |
705 | break; | |
706 | } | |
707 | ||
708 | for (int i = 0; i < AFL->len / 4; i++) { | |
709 | uint8_t SFI = AFL->value[i * 4 + 0] >> 3; | |
710 | uint8_t SFIstart = AFL->value[i * 4 + 1]; | |
711 | uint8_t SFIend = AFL->value[i * 4 + 2]; | |
712 | uint8_t SFIoffline = AFL->value[i * 4 + 3]; | |
713 | ||
714 | PrintAndLog("* * SFI[%02x] start:%02x end:%02x offline:%02x", SFI, SFIstart, SFIend, SFIoffline); | |
715 | if (SFI == 0 || SFI == 31 || SFIstart == 0 || SFIstart > SFIend) { | |
716 | PrintAndLog("SFI ERROR! Skipped..."); | |
717 | continue; | |
718 | } | |
719 | ||
720 | for(int n = SFIstart; n <= SFIend; n++) { | |
721 | PrintAndLog("* * * SFI[%02x] %d", SFI, n); | |
722 | ||
723 | res = EMVReadRecord(true, SFI, n, buf, sizeof(buf), &len, &sw, tlvRoot); | |
724 | if (res) { | |
725 | PrintAndLog("ERROR SFI[%02x]. APDU error %4x", SFI, sw); | |
726 | continue; | |
727 | } | |
728 | ||
729 | if (decodeTLV) { | |
730 | TLVPrintFromBuffer(buf, len); | |
731 | PrintAndLog(""); | |
732 | } | |
733 | ||
d03fb293 OM |
734 | // Build Input list for Offline Data Authentication |
735 | // EMV 4.3 book3 10.3, page 96 | |
3c5fce2b | 736 | if (SFIoffline) { |
d03fb293 OM |
737 | if (SFI < 11) { |
738 | const unsigned char *abuf = buf; | |
739 | size_t elmlen = len; | |
740 | struct tlv e; | |
741 | if (tlv_parse_tl(&abuf, &elmlen, &e)) { | |
742 | memcpy(&ODAiList[ODAiListLen], &buf[len - elmlen], elmlen); | |
743 | ODAiListLen += elmlen; | |
744 | } else { | |
745 | PrintAndLog("ERROR SFI[%02x]. Creating input list for Offline Data Authentication error.", SFI); | |
746 | } | |
747 | } else { | |
748 | memcpy(&ODAiList[ODAiListLen], buf, len); | |
749 | ODAiListLen += len; | |
750 | } | |
3c5fce2b OM |
751 | } |
752 | } | |
753 | } | |
754 | ||
755 | break; | |
756 | } | |
757 | ||
d03fb293 OM |
758 | // copy Input list for Offline Data Authentication |
759 | if (ODAiListLen) { | |
760 | struct tlvdb *oda = tlvdb_fixed(0x21, ODAiListLen, ODAiList); // not a standard tag | |
761 | tlvdb_add(tlvRoot, oda); | |
762 | PrintAndLog("* Input list for Offline Data Authentication added to TLV. len=%d \n", ODAiListLen); | |
763 | } | |
764 | ||
10d4f823 | 765 | // get AIP |
66efdc1f | 766 | const struct tlv *AIPtlv = tlvdb_get(tlvRoot, 0x82, NULL); |
767 | uint16_t AIP = AIPtlv->value[0] + AIPtlv->value[1] * 0x100; | |
10d4f823 | 768 | PrintAndLog("* * AIP=%04x", AIP); |
66efdc1f | 769 | |
10d4f823 | 770 | // SDA |
771 | if (AIP & 0x0040) { | |
772 | PrintAndLog("\n* SDA"); | |
d03fb293 | 773 | trSDA(tlvRoot); |
10d4f823 | 774 | } |
775 | ||
776 | // DDA | |
777 | if (AIP & 0x0020) { | |
778 | PrintAndLog("\n* DDA"); | |
d03fb293 | 779 | trDDA(decodeTLV, tlvRoot); |
10d4f823 | 780 | } |
781 | ||
782 | // transaction check | |
783 | ||
66efdc1f | 784 | // qVSDC |
10d4f823 | 785 | if (TrType == TT_QVSDCMCHIP|| TrType == TT_CDA){ |
66efdc1f | 786 | // 9F26: Application Cryptogram |
787 | const struct tlv *AC = tlvdb_get(tlvRoot, 0x9F26, NULL); | |
788 | if (AC) { | |
789 | PrintAndLog("\n--> qVSDC transaction."); | |
790 | PrintAndLog("* AC path"); | |
791 | ||
792 | // 9F36: Application Transaction Counter (ATC) | |
793 | const struct tlv *ATC = tlvdb_get(tlvRoot, 0x9F36, NULL); | |
794 | if (ATC) { | |
795 | ||
796 | // 9F10: Issuer Application Data - optional | |
797 | const struct tlv *IAD = tlvdb_get(tlvRoot, 0x9F10, NULL); | |
798 | ||
799 | // print AC data | |
800 | PrintAndLog("ATC: %s", sprint_hex(ATC->value, ATC->len)); | |
801 | PrintAndLog("AC: %s", sprint_hex(AC->value, AC->len)); | |
802 | if (IAD){ | |
803 | PrintAndLog("IAD: %s", sprint_hex(IAD->value, IAD->len)); | |
804 | ||
805 | if (IAD->len >= IAD->value[0] + 1) { | |
806 | PrintAndLog("\tKey index: 0x%02x", IAD->value[1]); | |
807 | PrintAndLog("\tCrypto ver: 0x%02x(%03d)", IAD->value[2], IAD->value[2]); | |
808 | PrintAndLog("\tCVR:", sprint_hex(&IAD->value[3], IAD->value[0] - 2)); | |
809 | struct tlvdb * cvr = tlvdb_fixed(0x20, IAD->value[0] - 2, &IAD->value[3]); | |
810 | TLVPrintFromTLVLev(cvr, 1); | |
811 | } | |
812 | } else { | |
813 | PrintAndLog("WARNING: IAD not found."); | |
814 | } | |
815 | ||
816 | } else { | |
817 | PrintAndLog("ERROR AC: Application Transaction Counter (ATC) not found."); | |
818 | } | |
819 | } | |
820 | } | |
821 | ||
10d4f823 | 822 | // Mastercard M/CHIP |
823 | if (GetCardPSVendor(AID, AIDlen) == CV_MASTERCARD && (TrType == TT_QVSDCMCHIP || TrType == TT_CDA)){ | |
66efdc1f | 824 | const struct tlv *CDOL1 = tlvdb_get(tlvRoot, 0x8c, NULL); |
825 | if (CDOL1 && GetCardPSVendor(AID, AIDlen) == CV_MASTERCARD) { // and m/chip transaction flag | |
10d4f823 | 826 | PrintAndLog("\n--> Mastercard M/Chip transaction."); |
827 | ||
828 | PrintAndLog("* * Generate challenge"); | |
829 | res = EMVGenerateChallenge(true, buf, sizeof(buf), &len, &sw, tlvRoot); | |
830 | if (res) { | |
831 | PrintAndLog("ERROR GetChallenge. APDU error %4x", sw); | |
d03fb293 | 832 | dreturn(6); |
10d4f823 | 833 | } |
834 | if (len < 4) { | |
835 | PrintAndLog("ERROR GetChallenge. Wrong challenge length %d", len); | |
d03fb293 | 836 | dreturn(6); |
10d4f823 | 837 | } |
838 | ||
839 | // ICC Dynamic Number | |
840 | struct tlvdb * ICCDynN = tlvdb_fixed(0x9f4c, len, buf); | |
841 | tlvdb_add(tlvRoot, ICCDynN); | |
842 | if (decodeTLV){ | |
843 | PrintAndLog("\n* * ICC Dynamic Number:"); | |
844 | TLVPrintFromTLV(ICCDynN); | |
845 | } | |
846 | ||
847 | PrintAndLog("* * Calc CDOL1"); | |
848 | struct tlv *cdol_data_tlv = dol_process(tlvdb_get(tlvRoot, 0x8c, NULL), tlvRoot, 0x01); // 0x01 - dummy tag | |
849 | if (!cdol_data_tlv){ | |
850 | PrintAndLog("ERROR: can't create CDOL1 TLV."); | |
d03fb293 | 851 | dreturn(6); |
10d4f823 | 852 | } |
853 | PrintAndLog("CDOL1 data[%d]: %s", cdol_data_tlv->len, sprint_hex(cdol_data_tlv->value, cdol_data_tlv->len)); | |
854 | ||
855 | PrintAndLog("* * AC1"); | |
856 | // EMVAC_TC + EMVAC_CDAREQ --- to get SDAD | |
857 | res = EMVAC(true, (TrType == TT_CDA) ? EMVAC_TC + EMVAC_CDAREQ : EMVAC_TC, (uint8_t *)cdol_data_tlv->value, cdol_data_tlv->len, buf, sizeof(buf), &len, &sw, tlvRoot); | |
858 | ||
10d4f823 | 859 | if (res) { |
860 | PrintAndLog("AC1 error(%d): %4x. Exit...", res, sw); | |
d03fb293 | 861 | dreturn(7); |
10d4f823 | 862 | } |
863 | ||
864 | if (decodeTLV) | |
865 | TLVPrintFromBuffer(buf, len); | |
866 | ||
d03fb293 OM |
867 | // CDA |
868 | PrintAndLog("\n* CDA:"); | |
869 | struct tlvdb *ac_tlv = tlvdb_parse_multi(buf, len); | |
870 | res = trCDA(tlvRoot, ac_tlv, pdol_data_tlv, cdol_data_tlv); | |
871 | if (res) { | |
872 | PrintAndLog("CDA error (%d)", res); | |
873 | } | |
874 | free(ac_tlv); | |
875 | free(cdol_data_tlv); | |
876 | ||
877 | PrintAndLog("\n* M/Chip transaction result:"); | |
10d4f823 | 878 | // 9F27: Cryptogram Information Data (CID) |
879 | const struct tlv *CID = tlvdb_get(tlvRoot, 0x9F27, NULL); | |
880 | if (CID) { | |
881 | emv_tag_dump(CID, stdout, 0); | |
882 | PrintAndLog("------------------------------"); | |
883 | if (CID->len > 0) { | |
884 | switch(CID->value[0] & EMVAC_AC_MASK){ | |
885 | case EMVAC_AAC: | |
886 | PrintAndLog("Transaction DECLINED."); | |
887 | break; | |
888 | case EMVAC_TC: | |
889 | PrintAndLog("Transaction approved OFFLINE."); | |
890 | break; | |
891 | case EMVAC_ARQC: | |
892 | PrintAndLog("Transaction approved ONLINE."); | |
893 | break; | |
894 | default: | |
895 | PrintAndLog("ERROR: CID transaction code error %2x", CID->value[0] & EMVAC_AC_MASK); | |
896 | break; | |
897 | } | |
898 | } else { | |
899 | PrintAndLog("ERROR: Wrong CID length %d", CID->len); | |
900 | } | |
901 | } else { | |
902 | PrintAndLog("ERROR: CID(9F27) not found."); | |
903 | } | |
66efdc1f | 904 | |
905 | } | |
906 | } | |
907 | ||
908 | // MSD | |
10d4f823 | 909 | if (AIP & 0x8000 && TrType == TT_MSD) { |
66efdc1f | 910 | PrintAndLog("\n--> MSD transaction."); |
911 | ||
66efdc1f | 912 | PrintAndLog("* MSD dCVV path. Check dCVV"); |
913 | ||
914 | const struct tlv *track2 = tlvdb_get(tlvRoot, 0x57, NULL); | |
915 | if (track2) { | |
916 | PrintAndLog("Track2: %s", sprint_hex(track2->value, track2->len)); | |
917 | ||
918 | struct tlvdb *dCVV = GetdCVVRawFromTrack2(track2); | |
919 | PrintAndLog("dCVV raw data:"); | |
920 | TLVPrintFromTLV(dCVV); | |
921 | ||
922 | if (GetCardPSVendor(AID, AIDlen) == CV_MASTERCARD) { | |
923 | PrintAndLog("\n* Mastercard calculate UDOL"); | |
924 | ||
925 | // UDOL (9F69) | |
926 | const struct tlv *UDOL = tlvdb_get(tlvRoot, 0x9F69, NULL); | |
927 | // UDOL(9F69) default: 9F6A (Unpredictable number) 4 bytes | |
928 | const struct tlv defUDOL = { | |
929 | .tag = 0x01, | |
930 | .len = 3, | |
931 | .value = (uint8_t *)"\x9f\x6a\x04", | |
932 | }; | |
933 | if (!UDOL) | |
934 | PrintAndLog("Use default UDOL."); | |
935 | ||
10d4f823 | 936 | struct tlv *udol_data_tlv = dol_process(UDOL ? UDOL : &defUDOL, tlvRoot, 0x01); // 0x01 - dummy tag |
66efdc1f | 937 | if (!udol_data_tlv){ |
938 | PrintAndLog("ERROR: can't create UDOL TLV."); | |
d03fb293 | 939 | dreturn(8); |
66efdc1f | 940 | } |
66efdc1f | 941 | |
78528074 | 942 | PrintAndLog("UDOL data[%d]: %s", udol_data_tlv->len, sprint_hex(udol_data_tlv->value, udol_data_tlv->len)); |
66efdc1f | 943 | |
944 | PrintAndLog("\n* Mastercard compute cryptographic checksum(UDOL)"); | |
945 | ||
78528074 | 946 | res = MSCComputeCryptoChecksum(true, (uint8_t *)udol_data_tlv->value, udol_data_tlv->len, buf, sizeof(buf), &len, &sw, tlvRoot); |
66efdc1f | 947 | if (res) { |
948 | PrintAndLog("ERROR Compute Crypto Checksum. APDU error %4x", sw); | |
d03fb293 OM |
949 | free(udol_data_tlv); |
950 | dreturn(9); | |
66efdc1f | 951 | } |
952 | ||
953 | if (decodeTLV) { | |
954 | TLVPrintFromBuffer(buf, len); | |
955 | PrintAndLog(""); | |
956 | } | |
d03fb293 | 957 | free(udol_data_tlv); |
66efdc1f | 958 | |
959 | } | |
960 | } else { | |
961 | PrintAndLog("ERROR MSD: Track2 data not found."); | |
962 | } | |
963 | } | |
d03fb293 | 964 | |
3c5fce2b OM |
965 | // DropField |
966 | DropField(); | |
967 | ||
968 | // Destroy TLV's | |
d03fb293 | 969 | free(pdol_data_tlv); |
3c5fce2b OM |
970 | tlvdb_free(tlvSelect); |
971 | tlvdb_free(tlvRoot); | |
972 | ||
973 | PrintAndLog("\n* Transaction completed."); | |
974 | ||
975 | return 0; | |
976 | } | |
977 | ||
556826b5 OM |
978 | int UsageCmdHFEMVScan(void) { |
979 | PrintAndLog("HELP : Scan EMV card and save it contents to a file. \n"); | |
980 | PrintAndLog(" It executes EMV contactless transaction and saves result to a file which can be used for emulation.\n"); | |
981 | PrintAndLog("Usage: hf emv scan [-a][-t][-v][-c][-x][-g] <file_name>\n"); | |
982 | PrintAndLog(" Options:"); | |
983 | PrintAndLog(" -a : show APDU reqests and responses\n"); | |
984 | PrintAndLog(" -t : TLV decode results\n"); | |
985 | PrintAndLog(" -v : transaction type - qVSDC or M/Chip.\n"); | |
986 | PrintAndLog(" -c : transaction type - qVSDC or M/Chip plus CDA (SDAD generation).\n"); | |
987 | PrintAndLog(" -x : transaction type - VSDC. For test only. Not a standart behavior.\n"); | |
988 | PrintAndLog(" -g : VISA. generate AC from GPO\n"); | |
989 | PrintAndLog("By default : transaction type - MSD.\n"); | |
990 | PrintAndLog("Samples:"); | |
991 | PrintAndLog(" hf emv scan -a -t -> scan MSD transaction mode"); | |
992 | PrintAndLog(" hf emv scan -a -t -c -> scan CDA transaction mode"); | |
993 | return 0; | |
994 | } | |
995 | ||
996 | int CmdHFEMVScan(const char *cmd) { | |
997 | UsageCmdHFEMVScan(); | |
998 | ||
999 | return 0; | |
1000 | } | |
1001 | ||
d03fb293 OM |
1002 | int CmdHFEMVTest(const char *cmd) { |
1003 | return ExecuteCryptoTests(true); | |
1004 | } | |
1005 | ||
3c5fce2b OM |
1006 | int CmdHelp(const char *Cmd); |
1007 | static command_t CommandTable[] = { | |
1008 | {"help", CmdHelp, 1, "This help"}, | |
1009 | {"exec", CmdHFEMVExec, 0, "Executes EMV contactless transaction."}, | |
1010 | {"pse", CmdHFEMVPPSE, 0, "Execute PPSE. It selects 2PAY.SYS.DDF01 or 1PAY.SYS.DDF01 directory."}, | |
1011 | {"search", CmdHFEMVSearch, 0, "Try to select all applets from applets list and print installed applets."}, | |
1012 | {"select", CmdHFEMVSelect, 0, "Select applet."}, | |
556826b5 | 1013 | // {"scan", CmdHFEMVScan, 0, "Scan EMV card and save it contents to json file for emulator."}, |
d03fb293 | 1014 | {"test", CmdHFEMVTest, 0, "Crypto logic test."}, |
3c5fce2b OM |
1015 | {NULL, NULL, 0, NULL} |
1016 | }; | |
1017 | ||
1018 | int CmdHFEMV(const char *Cmd) { | |
1019 | CmdsParse(CommandTable, Cmd); | |
1020 | return 0; | |
1021 | } | |
1022 | ||
1023 | int CmdHelp(const char *Cmd) { | |
1024 | CmdsHelp(CommandTable); | |
1025 | return 0; | |
1026 | } |