seos_native_peripheral.c 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. #include "seos_native_peripheral_i.h"
  2. #define TAG "SeosNativePeripheral"
  3. #define MESSAGE_QUEUE_SIZE 10
  4. static uint8_t standard_seos_aid[] = {0xa0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01};
  5. static uint8_t cd02[] = {0xcd, 0x02};
  6. static uint8_t general_authenticate_1[] =
  7. {0x00, 0x87, 0x00, 0x01, 0x04, 0x7c, 0x02, 0x81, 0x00, 0x00};
  8. static uint8_t ga1_response[] = {0x7c, 0x0a, 0x81, 0x08};
  9. // Emulation
  10. static uint8_t success[] = {0x90, 0x00};
  11. static uint8_t file_not_found[] = {0x6A, 0x82};
  12. static uint8_t select_header[] = {0x00, 0xa4, 0x04, 0x00};
  13. static uint8_t select_adf_header[] = {0x80, 0xa5, 0x04, 0x00};
  14. static uint8_t general_authenticate_2_header[] = {0x00, 0x87, 0x00, 0x01};
  15. static uint8_t secure_messaging_header[] = {0x0c, 0xcb, 0x3f, 0xff};
  16. int32_t seos_native_peripheral_task(void* context);
  17. typedef struct {
  18. size_t len;
  19. uint8_t buf[BLE_SVC_SEOS_CHAR_VALUE_LEN_MAX];
  20. } NativePeripheralMessage;
  21. static void seos_ble_connection_status_callback(BtStatus status, void* context) {
  22. furi_assert(context);
  23. SeosNativePeripheral* seos_native_peripheral = context;
  24. if(status == BtStatusConnected) {
  25. view_dispatcher_send_custom_event(
  26. seos_native_peripheral->seos->view_dispatcher, SeosCustomEventConnected);
  27. } else if(status == BtStatusAdvertising) {
  28. view_dispatcher_send_custom_event(
  29. seos_native_peripheral->seos->view_dispatcher, SeosCustomEventAdvertising);
  30. }
  31. }
  32. static uint16_t seos_svc_callback(SeosServiceEvent event, void* context) {
  33. SeosNativePeripheral* seos_native_peripheral = context;
  34. uint16_t bytes_available = 0;
  35. if(event.event == SeosServiceEventTypeDataReceived) {
  36. uint32_t space = furi_message_queue_get_space(seos_native_peripheral->messages);
  37. if(space > 0) {
  38. NativePeripheralMessage message = {.len = event.data.size};
  39. memcpy(message.buf, event.data.buffer, event.data.size);
  40. if(furi_mutex_acquire(seos_native_peripheral->mq_mutex, FuriWaitForever) ==
  41. FuriStatusOk) {
  42. furi_message_queue_put(
  43. seos_native_peripheral->messages, &message, FuriWaitForever);
  44. furi_mutex_release(seos_native_peripheral->mq_mutex);
  45. }
  46. if(space < MESSAGE_QUEUE_SIZE / 2) {
  47. FURI_LOG_D(TAG, "Queue message. %ld remaining", space);
  48. }
  49. bytes_available = (space - 1) * sizeof(NativePeripheralMessage);
  50. } else {
  51. FURI_LOG_E(TAG, "No space in message queue");
  52. }
  53. }
  54. return bytes_available;
  55. }
  56. SeosNativePeripheral* seos_native_peripheral_alloc(Seos* seos) {
  57. SeosNativePeripheral* seos_native_peripheral = malloc(sizeof(SeosNativePeripheral));
  58. memset(seos_native_peripheral, 0, sizeof(SeosNativePeripheral));
  59. seos_native_peripheral->seos = seos;
  60. seos_native_peripheral->credential = seos->credential;
  61. seos_native_peripheral->bt = furi_record_open(RECORD_BT);
  62. seos_native_peripheral->phase = SELECT_AID;
  63. seos_native_peripheral->secure_messaging = NULL;
  64. seos_native_peripheral->params.key_no = 1;
  65. memset(
  66. seos_native_peripheral->params.cNonce,
  67. 0x0c,
  68. sizeof(seos_native_peripheral->params.cNonce));
  69. memset(seos_native_peripheral->params.UID, 0x0d, sizeof(seos_native_peripheral->params.UID));
  70. seos_native_peripheral->thread = furi_thread_alloc_ex(
  71. "SeosNativePeripheralWorker",
  72. 5 * 1024,
  73. seos_native_peripheral_task,
  74. seos_native_peripheral);
  75. seos_native_peripheral->messages =
  76. furi_message_queue_alloc(MESSAGE_QUEUE_SIZE, sizeof(NativePeripheralMessage));
  77. seos_native_peripheral->mq_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
  78. return seos_native_peripheral;
  79. }
  80. void seos_native_peripheral_free(SeosNativePeripheral* seos_native_peripheral) {
  81. furi_assert(seos_native_peripheral);
  82. furi_record_close(RECORD_BT);
  83. furi_message_queue_free(seos_native_peripheral->messages);
  84. furi_mutex_free(seos_native_peripheral->mq_mutex);
  85. furi_thread_free(seos_native_peripheral->thread);
  86. free(seos_native_peripheral);
  87. }
  88. void seos_native_peripheral_start(SeosNativePeripheral* seos_native_peripheral, FlowMode mode) {
  89. seos_native_peripheral->flow_mode = mode;
  90. if(seos_native_peripheral->flow_mode == FLOW_CRED) {
  91. seos_native_peripheral->params.key_no = 0;
  92. seos_native_peripheral->params.cipher = TWO_KEY_3DES_CBC_MODE;
  93. seos_native_peripheral->params.hash = SHA1;
  94. memset(
  95. seos_native_peripheral->params.rndICC,
  96. 0x0d,
  97. sizeof(seos_native_peripheral->params.rndICC));
  98. memset(
  99. seos_native_peripheral->params.rNonce,
  100. 0x0c,
  101. sizeof(seos_native_peripheral->params.rNonce));
  102. memset(
  103. seos_native_peripheral->params.UID, 0x00, sizeof(seos_native_peripheral->params.UID));
  104. memset(
  105. seos_native_peripheral->params.cNonce,
  106. 0x00,
  107. sizeof(seos_native_peripheral->params.cNonce));
  108. }
  109. bt_disconnect(seos_native_peripheral->bt);
  110. BleProfileParams params = {
  111. .mode = mode,
  112. };
  113. // Wait 2nd core to update nvm storage
  114. furi_delay_ms(200);
  115. seos_native_peripheral->ble_profile =
  116. bt_profile_start(seos_native_peripheral->bt, ble_profile_seos, &params);
  117. furi_check(seos_native_peripheral->ble_profile);
  118. bt_set_status_changed_callback(
  119. seos_native_peripheral->bt, seos_ble_connection_status_callback, seos_native_peripheral);
  120. ble_profile_seos_set_event_callback(
  121. seos_native_peripheral->ble_profile,
  122. sizeof(seos_native_peripheral->event_buffer),
  123. seos_svc_callback,
  124. seos_native_peripheral);
  125. furi_hal_bt_start_advertising();
  126. view_dispatcher_send_custom_event(
  127. seos_native_peripheral->seos->view_dispatcher, SeosCustomEventAdvertising);
  128. furi_thread_start(seos_native_peripheral->thread);
  129. }
  130. void seos_native_peripheral_stop(SeosNativePeripheral* seos_native_peripheral) {
  131. furi_hal_bt_stop_advertising();
  132. bt_set_status_changed_callback(seos_native_peripheral->bt, NULL, NULL);
  133. bt_disconnect(seos_native_peripheral->bt);
  134. // Wait 2nd core to update nvm storage
  135. furi_delay_ms(200);
  136. bt_keys_storage_set_default_path(seos_native_peripheral->bt);
  137. furi_check(bt_profile_restore_default(seos_native_peripheral->bt));
  138. furi_thread_flags_set(furi_thread_get_id(seos_native_peripheral->thread), WorkerEvtStop);
  139. furi_thread_join(seos_native_peripheral->thread);
  140. }
  141. void seos_native_peripheral_process_message_cred(
  142. SeosNativePeripheral* seos_native_peripheral,
  143. NativePeripheralMessage message) {
  144. Seos* seos = seos_native_peripheral->seos;
  145. BitBuffer* response = bit_buffer_alloc(128); // TODO: MTU
  146. uint8_t* data = message.buf;
  147. if(data[0] != BLE_START && data[0] != 0xe1) {
  148. FURI_LOG_W(TAG, "Unexpected start of BLE packet");
  149. }
  150. const uint8_t* apdu = data + 1; // Match name to nfc version for easier copying
  151. if(memcmp(apdu, select_header, sizeof(select_header)) == 0) {
  152. if(memcmp(apdu + sizeof(select_header) + 1, standard_seos_aid, sizeof(standard_seos_aid)) ==
  153. 0) {
  154. seos_emulator_select_aid(response);
  155. bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
  156. } else {
  157. bit_buffer_append_bytes(response, (uint8_t*)file_not_found, sizeof(file_not_found));
  158. }
  159. } else if(memcmp(apdu, select_adf_header, sizeof(select_adf_header)) == 0) {
  160. void* p = NULL;
  161. // +1 to skip APDU length byte
  162. const uint8_t* oid_list = apdu + sizeof(select_adf_header) + 1;
  163. size_t oid_list_len = apdu[sizeof(select_adf_header)];
  164. // First we try to match the credential ADF OID
  165. SeosCredential* credential = seos_native_peripheral->credential;
  166. if(credential->adf_oid_len > 0) {
  167. p = memmem(oid_list, oid_list_len, credential->adf_oid, credential->adf_oid_len);
  168. if(p) {
  169. seos_log_buffer(TAG, "Select ADF OID(credential)", p, credential->adf_oid_len);
  170. view_dispatcher_send_custom_event(
  171. seos->view_dispatcher, SeosCustomEventADFMatched);
  172. seos_emulator_select_adf(&seos_native_peripheral->params, credential, response);
  173. return;
  174. }
  175. }
  176. p = memmem(oid_list, oid_list_len, SEOS_ADF_OID, SEOS_ADF_OID_LEN);
  177. if(p) {
  178. seos_log_buffer(TAG, "Matched ADF", p, SEOS_ADF_OID_LEN);
  179. seos_emulator_select_adf(
  180. &seos_native_peripheral->params, seos_native_peripheral->credential, response);
  181. bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
  182. } else {
  183. FURI_LOG_W(TAG, "Failed to match any ADF OID");
  184. }
  185. } else if(memcmp(apdu, general_authenticate_1, sizeof(general_authenticate_1)) == 0) {
  186. seos_emulator_general_authenticate_1(response, seos_native_peripheral->params);
  187. bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
  188. } else if(memcmp(apdu, general_authenticate_2_header, sizeof(general_authenticate_2_header)) == 0) {
  189. if(!seos_emulator_general_authenticate_2(
  190. apdu,
  191. message.len - 1,
  192. seos_native_peripheral->credential,
  193. &seos_native_peripheral->params,
  194. response)) {
  195. FURI_LOG_W(TAG, "Failure in General Authenticate 2");
  196. } else {
  197. bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
  198. }
  199. view_dispatcher_send_custom_event(
  200. seos_native_peripheral->seos->view_dispatcher, SeosCustomEventAuthenticated);
  201. // Prepare for future communication
  202. seos_native_peripheral->secure_messaging =
  203. secure_messaging_alloc(&seos_native_peripheral->params);
  204. } else if(memcmp(apdu, secure_messaging_header, sizeof(secure_messaging_header)) == 0) {
  205. uint8_t request_sio[] = {0x5c, 0x02, 0xff, 0x00};
  206. if(seos_native_peripheral->secure_messaging) {
  207. FURI_LOG_D(TAG, "Unwrap secure message");
  208. // c0 0ccb3fff 16 8508fa8395d30de4e8e097008e085da7edbd833b002d00
  209. // Ignore 1 BLE_START byte
  210. size_t bytes_to_ignore = 1;
  211. BitBuffer* tmp = bit_buffer_alloc(message.len);
  212. bit_buffer_append_bytes(
  213. tmp, message.buf + bytes_to_ignore, message.len - bytes_to_ignore);
  214. seos_log_bitbuffer(TAG, "received(wrapped)", tmp);
  215. secure_messaging_unwrap_apdu(seos_native_peripheral->secure_messaging, tmp);
  216. seos_log_bitbuffer(TAG, "received(clear)", tmp);
  217. const uint8_t* message = bit_buffer_get_data(tmp);
  218. if(memcmp(message, request_sio, sizeof(request_sio)) == 0) {
  219. view_dispatcher_send_custom_event(
  220. seos_native_peripheral->seos->view_dispatcher, SeosCustomEventSIORequested);
  221. BitBuffer* sio_file = bit_buffer_alloc(128);
  222. bit_buffer_append_bytes(sio_file, message + 2, 2); // fileId
  223. bit_buffer_append_byte(sio_file, seos_native_peripheral->credential->sio_len);
  224. bit_buffer_append_bytes(
  225. sio_file,
  226. seos_native_peripheral->credential->sio,
  227. seos_native_peripheral->credential->sio_len);
  228. secure_messaging_wrap_rapdu(
  229. seos_native_peripheral->secure_messaging,
  230. (uint8_t*)bit_buffer_get_data(sio_file),
  231. bit_buffer_get_size_bytes(sio_file),
  232. response);
  233. bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
  234. bit_buffer_free(sio_file);
  235. }
  236. bit_buffer_free(tmp);
  237. } else {
  238. uint8_t no_sm[] = {0x69, 0x88};
  239. bit_buffer_append_bytes(response, no_sm, sizeof(no_sm));
  240. }
  241. } else if(data[0] == 0xe1) {
  242. // ignore
  243. } else {
  244. FURI_LOG_W(TAG, "no match for message");
  245. }
  246. if(bit_buffer_get_size_bytes(response) > 0) {
  247. BitBuffer* tx = bit_buffer_alloc(1 + 2 + 1 + bit_buffer_get_size_bytes(response));
  248. bit_buffer_append_byte(tx, BLE_START);
  249. bit_buffer_append_bytes(
  250. tx, bit_buffer_get_data(response), bit_buffer_get_size_bytes(response));
  251. ble_profile_seos_tx(
  252. seos_native_peripheral->ble_profile,
  253. (uint8_t*)bit_buffer_get_data(tx),
  254. bit_buffer_get_size_bytes(tx));
  255. bit_buffer_free(tx);
  256. }
  257. bit_buffer_free(response);
  258. }
  259. void seos_native_peripheral_process_message_reader(
  260. SeosNativePeripheral* seos_native_peripheral,
  261. NativePeripheralMessage message) {
  262. BitBuffer* response = bit_buffer_alloc(128); // TODO: MTU
  263. uint8_t* data = message.buf;
  264. uint8_t* rx_data = data + 1; // Match name to nfc version for easier copying
  265. if(data[0] != BLE_START && data[0] != 0xe1) {
  266. FURI_LOG_W(TAG, "Unexpected start of BLE packet");
  267. }
  268. if(memcmp(data + 5, standard_seos_aid, sizeof(standard_seos_aid)) == 0) { // response to select
  269. FURI_LOG_I(TAG, "Select ADF");
  270. uint8_t select_adf_header[] = {
  271. 0x80, 0xa5, 0x04, 0x00, (uint8_t)SEOS_ADF_OID_LEN + 2, 0x06, (uint8_t)SEOS_ADF_OID_LEN};
  272. bit_buffer_append_bytes(response, select_adf_header, sizeof(select_adf_header));
  273. bit_buffer_append_bytes(response, SEOS_ADF_OID, SEOS_ADF_OID_LEN);
  274. seos_native_peripheral->phase = SELECT_ADF;
  275. } else if(memcmp(data + 1, cd02, sizeof(cd02)) == 0) {
  276. BitBuffer* attribute_value = bit_buffer_alloc(message.len);
  277. bit_buffer_append_bytes(attribute_value, message.buf, message.len);
  278. if(seos_reader_select_adf_response(
  279. attribute_value,
  280. 1,
  281. seos_native_peripheral->credential,
  282. &seos_native_peripheral->params)) {
  283. // Craft response
  284. general_authenticate_1[3] = seos_native_peripheral->params.key_no;
  285. bit_buffer_append_bytes(
  286. response, general_authenticate_1, sizeof(general_authenticate_1));
  287. seos_native_peripheral->phase = GENERAL_AUTHENTICATION_1;
  288. }
  289. bit_buffer_free(attribute_value);
  290. } else if(memcmp(data + 1, ga1_response, sizeof(ga1_response)) == 0) {
  291. memcpy(seos_native_peripheral->params.rndICC, data + 5, 8);
  292. // Craft response
  293. uint8_t cryptogram[32 + 8];
  294. memset(cryptogram, 0, sizeof(cryptogram));
  295. seos_reader_generate_cryptogram(
  296. seos_native_peripheral->credential, &seos_native_peripheral->params, cryptogram);
  297. uint8_t ga_header[] = {
  298. 0x00,
  299. 0x87,
  300. 0x00,
  301. seos_native_peripheral->params.key_no,
  302. sizeof(cryptogram) + 4,
  303. 0x7c,
  304. sizeof(cryptogram) + 2,
  305. 0x82,
  306. sizeof(cryptogram)};
  307. bit_buffer_append_bytes(response, ga_header, sizeof(ga_header));
  308. bit_buffer_append_bytes(response, cryptogram, sizeof(cryptogram));
  309. seos_native_peripheral->phase = GENERAL_AUTHENTICATION_2;
  310. } else if(rx_data[0] == 0x7C && rx_data[2] == 0x82) { // ga2 response
  311. if(rx_data[3] == 40) {
  312. if(!seos_reader_verify_cryptogram(&seos_native_peripheral->params, rx_data + 4)) {
  313. FURI_LOG_W(TAG, "Card cryptogram failed verification");
  314. bit_buffer_free(response);
  315. return;
  316. }
  317. FURI_LOG_I(TAG, "Authenticated");
  318. view_dispatcher_send_custom_event(
  319. seos_native_peripheral->seos->view_dispatcher, SeosCustomEventAuthenticated);
  320. } else {
  321. FURI_LOG_W(TAG, "Unhandled card cryptogram size %d", rx_data[3]);
  322. }
  323. seos_native_peripheral->secure_messaging =
  324. secure_messaging_alloc(&seos_native_peripheral->params);
  325. SecureMessaging* secure_messaging = seos_native_peripheral->secure_messaging;
  326. uint8_t message[] = {0x5c, 0x02, 0xff, 0x00};
  327. secure_messaging_wrap_apdu(secure_messaging, message, sizeof(message), response);
  328. seos_native_peripheral->phase = REQUEST_SIO;
  329. view_dispatcher_send_custom_event(
  330. seos_native_peripheral->seos->view_dispatcher, SeosCustomEventSIORequested);
  331. } else if(seos_native_peripheral->phase == REQUEST_SIO) {
  332. // TODO: consider seos_reader_request_sio
  333. SecureMessaging* secure_messaging = seos_native_peripheral->secure_messaging;
  334. SeosCredential* credential = seos_native_peripheral->credential;
  335. AuthParameters* params = &seos_native_peripheral->params;
  336. BitBuffer* rx_buffer = bit_buffer_alloc(message.len - 1);
  337. bit_buffer_append_bytes(rx_buffer, rx_data, message.len - 1);
  338. seos_log_bitbuffer(TAG, "BLE response(wrapped)", rx_buffer);
  339. secure_messaging_unwrap_rapdu(secure_messaging, rx_buffer);
  340. seos_log_bitbuffer(TAG, "BLE response(clear)", rx_buffer);
  341. // Skip fileId
  342. credential->sio_len = bit_buffer_get_byte(rx_buffer, 2);
  343. if(credential->sio_len > sizeof(credential->sio)) {
  344. FURI_LOG_W(TAG, "SIO too long to save");
  345. bit_buffer_free(response);
  346. return;
  347. }
  348. memcpy(credential->sio, bit_buffer_get_data(rx_buffer) + 3, credential->sio_len);
  349. memcpy(credential->priv_key, params->priv_key, sizeof(credential->priv_key));
  350. memcpy(credential->auth_key, params->auth_key, sizeof(credential->auth_key));
  351. credential->adf_oid_len = SEOS_ADF_OID_LEN;
  352. memcpy(credential->adf_oid, SEOS_ADF_OID, sizeof(credential->adf_oid));
  353. FURI_LOG_I(TAG, "SIO Captured, %d bytes", credential->sio_len);
  354. Seos* seos = seos_native_peripheral->seos;
  355. view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventReaderSuccess);
  356. bit_buffer_free(rx_buffer);
  357. seos_native_peripheral->phase = SELECT_AID;
  358. } else if(data[0] == 0xe1) {
  359. //ignore
  360. } else {
  361. FURI_LOG_W(TAG, "No match for write request");
  362. seos_log_buffer(TAG, "No match for reader incoming", message.buf, message.len);
  363. }
  364. if(bit_buffer_get_size_bytes(response) > 0) {
  365. BitBuffer* tx = bit_buffer_alloc(1 + 2 + 1 + bit_buffer_get_size_bytes(response));
  366. bit_buffer_append_byte(tx, BLE_START);
  367. bit_buffer_append_bytes(
  368. tx, bit_buffer_get_data(response), bit_buffer_get_size_bytes(response));
  369. ble_profile_seos_tx(
  370. seos_native_peripheral->ble_profile,
  371. (uint8_t*)bit_buffer_get_data(tx),
  372. bit_buffer_get_size_bytes(tx));
  373. bit_buffer_free(tx);
  374. }
  375. bit_buffer_free(response);
  376. }
  377. int32_t seos_native_peripheral_task(void* context) {
  378. SeosNativePeripheral* seos_native_peripheral = (SeosNativePeripheral*)context;
  379. bool running = true;
  380. while(running) {
  381. uint32_t events = furi_thread_flags_get();
  382. if(events & WorkerEvtStop) {
  383. running = false;
  384. break;
  385. }
  386. if(furi_mutex_acquire(seos_native_peripheral->mq_mutex, 1) == FuriStatusOk) {
  387. uint32_t count = furi_message_queue_get_count(seos_native_peripheral->messages);
  388. if(count > 0) {
  389. if(count > MESSAGE_QUEUE_SIZE / 2) {
  390. FURI_LOG_I(TAG, "Dequeue message [%ld messages]", count);
  391. }
  392. NativePeripheralMessage message = {};
  393. FuriStatus status = furi_message_queue_get(
  394. seos_native_peripheral->messages, &message, FuriWaitForever);
  395. if(status != FuriStatusOk) {
  396. FURI_LOG_W(TAG, "furi_message_queue_get fail %d", status);
  397. }
  398. if(seos_native_peripheral->flow_mode == FLOW_READER) {
  399. seos_native_peripheral_process_message_reader(seos_native_peripheral, message);
  400. } else if(seos_native_peripheral->flow_mode == FLOW_CRED) {
  401. seos_native_peripheral_process_message_cred(seos_native_peripheral, message);
  402. }
  403. }
  404. furi_mutex_release(seos_native_peripheral->mq_mutex);
  405. } else {
  406. FURI_LOG_W(TAG, "Failed to acquire mutex");
  407. }
  408. // A beat for event flags
  409. furi_delay_ms(1);
  410. }
  411. return 0;
  412. }