payto.c (20391B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2019-2024 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 13 You should have received a copy of the GNU General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file payto.c 18 * @brief Common utility functions for dealing with payto://-URIs 19 * @author Florian Dold 20 */ 21 #include "taler/platform.h" 22 #include "taler/taler_util.h" 23 24 25 /** 26 * Prefix of PAYTO URLs. 27 */ 28 #define PAYTO "payto://" 29 30 31 int 32 TALER_full_payto_cmp (const struct TALER_FullPayto a, 33 const struct TALER_FullPayto b) 34 { 35 if ( (NULL == a.full_payto) && 36 (NULL == b.full_payto) ) 37 return 0; 38 if (NULL == a.full_payto) 39 return -1; 40 if (NULL == b.full_payto) 41 return 1; 42 return strcmp (a.full_payto, 43 b.full_payto); 44 } 45 46 47 bool 48 TALER_payto_is_wallet (const char *payto_uri) 49 { 50 return 51 (0 == strncasecmp (payto_uri, 52 "payto://taler-reserve/", 53 strlen ("payto://taler-reserve/"))) || 54 (0 == strncasecmp (payto_uri, 55 "payto://taler-reserve-http/", 56 strlen ("payto://taler-reserve-http/"))); 57 } 58 59 60 int 61 TALER_normalized_payto_cmp (const struct TALER_NormalizedPayto a, 62 const struct TALER_NormalizedPayto b) 63 { 64 if ( (NULL == a.normalized_payto) && 65 (NULL == b.normalized_payto) ) 66 return 0; 67 if (NULL == a.normalized_payto) 68 return -1; 69 if (NULL == b.normalized_payto) 70 return 1; 71 return strcmp (a.normalized_payto, 72 b.normalized_payto); 73 } 74 75 76 void 77 TALER_full_payto_normalize_and_hash (const struct TALER_FullPayto in, 78 struct TALER_NormalizedPaytoHashP *out) 79 { 80 struct TALER_NormalizedPayto normalized_payto_uri; 81 82 normalized_payto_uri 83 = TALER_payto_normalize (in); 84 TALER_normalized_payto_hash (normalized_payto_uri, 85 out); 86 GNUNET_free (normalized_payto_uri.normalized_payto); 87 } 88 89 90 /** 91 * Compare two full payto URIs for equality in their normalized form. 92 * 93 * @param a a full payto URI, NULL is permitted 94 * @param b a full payto URI, NULL is permitted 95 * @return 0 if both are equal, otherwise -1 or 1 96 */ 97 int 98 TALER_full_payto_normalize_and_cmp (const struct TALER_FullPayto a, 99 const struct TALER_FullPayto b) 100 { 101 struct TALER_NormalizedPayto an 102 = TALER_payto_normalize (a); 103 struct TALER_NormalizedPayto bn 104 = TALER_payto_normalize (b); 105 int ret; 106 107 ret = TALER_normalized_payto_cmp (an, 108 bn); 109 GNUNET_free (an.normalized_payto); 110 GNUNET_free (bn.normalized_payto); 111 return ret; 112 } 113 114 115 /** 116 * Extract the value under @a key from the URI parameters. 117 * 118 * @param fpayto_uri the full payto URL to parse 119 * @param search_key key to look for, including "=" 120 * @return NULL if the @a key parameter is not found. 121 * The caller should free the returned value. 122 */ 123 static char * 124 payto_get_key (const struct TALER_FullPayto fpayto_uri, 125 const char *search_key) 126 { 127 const char *payto_uri = fpayto_uri.full_payto; 128 const char *key; 129 const char *value_start; 130 const char *value_end; 131 132 key = strchr (payto_uri, 133 (unsigned char) '?'); 134 if (NULL == key) 135 return NULL; 136 137 do { 138 if (0 == strncasecmp (++key, 139 search_key, 140 strlen (search_key))) 141 { 142 value_start = strchr (key, 143 (unsigned char) '='); 144 if (NULL == value_start) 145 return NULL; 146 value_end = strchrnul (value_start, 147 (unsigned char) '&'); 148 149 return GNUNET_strndup (value_start + 1, 150 value_end - value_start - 1); 151 } 152 } while ( (key = strchr (key, 153 (unsigned char) '&')) ); 154 return NULL; 155 } 156 157 158 char * 159 TALER_payto_get_subject (const struct TALER_FullPayto payto_uri) 160 { 161 return payto_get_key (payto_uri, 162 "subject="); 163 } 164 165 166 char * 167 TALER_payto_get_method (const char *payto_uri) 168 { 169 const char *start; 170 const char *end; 171 172 if (0 != strncasecmp (payto_uri, 173 PAYTO, 174 strlen (PAYTO))) 175 return NULL; 176 start = &payto_uri[strlen (PAYTO)]; 177 end = strchr (start, 178 (unsigned char) '/'); 179 if (NULL == end) 180 return NULL; 181 return GNUNET_strndup (start, 182 end - start); 183 } 184 185 186 char * 187 TALER_xtalerbank_account_from_payto (const struct TALER_FullPayto payto) 188 { 189 const char *host; 190 const char *beg; 191 const char *nxt; 192 const char *end; 193 194 if (0 != strncasecmp (payto.full_payto, 195 PAYTO "x-taler-bank/", 196 strlen (PAYTO "x-taler-bank/"))) 197 { 198 GNUNET_break_op (0); 199 return NULL; 200 } 201 host = &payto.full_payto[strlen (PAYTO "x-taler-bank/")]; 202 beg = strchr (host, 203 '/'); 204 if (NULL == beg) 205 { 206 GNUNET_break_op (0); 207 return NULL; 208 } 209 beg++; /* now points to $ACCOUNT or $PATH */ 210 nxt = strchr (beg, 211 '/'); 212 end = strchr (beg, 213 '?'); 214 if (NULL == end) 215 end = &beg[strlen (beg)]; 216 while ( (NULL != nxt) && 217 (end - nxt > 0) ) 218 { 219 beg = nxt + 1; 220 nxt = strchr (beg, 221 '/'); 222 } 223 return GNUNET_strndup (beg, 224 end - beg); 225 } 226 227 228 /** 229 * Validate payto://iban/ account URL (only account information, 230 * wire subject and amount are ignored). 231 * 232 * @param payto_uri payto URL to parse 233 * @return NULL on success, otherwise an error message 234 * to be freed by the caller 235 */ 236 static char * 237 validate_payto_iban (const char *payto_uri) 238 { 239 const char *iban; 240 const char *q; 241 char *result; 242 char *err; 243 244 #define IBAN_PREFIX "payto://iban/" 245 if (0 != strncasecmp (payto_uri, 246 IBAN_PREFIX, 247 strlen (IBAN_PREFIX))) 248 return NULL; /* not an IBAN */ 249 iban = strrchr (payto_uri, 250 '/') + 1; 251 #undef IBAN_PREFIX 252 q = strchr (iban, 253 '?'); 254 if (NULL != q) 255 { 256 result = GNUNET_strndup (iban, 257 q - iban); 258 } 259 else 260 { 261 result = GNUNET_strdup (iban); 262 } 263 if (NULL != 264 (err = TALER_iban_validate (result))) 265 { 266 GNUNET_free (result); 267 return err; 268 } 269 GNUNET_free (result); 270 return NULL; 271 } 272 273 274 /** 275 * Validate payto://x-taler-bank/ account URL (only account information, 276 * wire subject and amount are ignored). 277 * 278 * @param payto_uri payto URL to parse 279 * @return NULL on success, otherwise an error message 280 * to be freed by the caller 281 */ 282 static char * 283 validate_payto_xtalerbank (const char *payto_uri) 284 { 285 const char *user; 286 const char *nxt; 287 const char *beg; 288 const char *end; 289 const char *host; 290 bool dot_ok; 291 bool post_colon; 292 bool port_ok; 293 294 #define XTALERBANK_PREFIX PAYTO "x-taler-bank/" 295 if (0 != strncasecmp (payto_uri, 296 XTALERBANK_PREFIX, 297 strlen (XTALERBANK_PREFIX))) 298 return NULL; /* not an x-taler-bank URI */ 299 host = &payto_uri[strlen (XTALERBANK_PREFIX)]; 300 #undef XTALERBANK_PREFIX 301 beg = strchr (host, 302 '/'); 303 if (NULL == beg) 304 { 305 return GNUNET_strdup ("account name missing"); 306 } 307 beg++; /* now points to $ACCOUNT or $PATH */ 308 nxt = strchr (beg, 309 '/'); 310 end = strchr (beg, 311 '?'); 312 if (NULL == end) 313 { 314 return GNUNET_strdup ("'receiver-name' parameter missing"); 315 } 316 while ( (NULL != nxt) && 317 (end - nxt > 0) ) 318 { 319 beg = nxt + 1; 320 nxt = strchr (beg, 321 '/'); 322 } 323 user = beg; 324 if (user == host + 1) 325 { 326 return GNUNET_strdup ("domain name missing"); 327 } 328 if ('-' == host[0]) 329 return GNUNET_strdup ("invalid character '-' at start of domain name"); 330 dot_ok = false; 331 post_colon = false; 332 port_ok = false; 333 while (host != user) 334 { 335 char c = host[0]; 336 337 if ('/' == c) 338 { 339 /* path started, do not care about characters 340 in the path */ 341 break; 342 } 343 if (':' == c) 344 { 345 post_colon = true; 346 host++; 347 continue; 348 } 349 if (post_colon) 350 { 351 if (! ( ('0' <= c) && ('9' >= c) ) ) 352 { 353 char *err; 354 355 GNUNET_asprintf (&err, 356 "invalid character '%c' in port", 357 c); 358 return err; 359 } 360 port_ok = true; 361 } 362 else 363 { 364 if ('.' == c) 365 { 366 if (! dot_ok) 367 return GNUNET_strdup ("invalid domain name (misplaced '.')"); 368 dot_ok = false; 369 } 370 else 371 { 372 if (! ( ('-' == c) || 373 ('_' == c) || 374 ( ('0' <= c) && ('9' >= c) ) || 375 ( ('a' <= c) && ('z' >= c) ) || 376 ( ('A' <= c) && ('Z' >= c) ) ) ) 377 { 378 char *err; 379 380 GNUNET_asprintf (&err, 381 "invalid character '%c' in domain name", 382 c); 383 return err; 384 } 385 dot_ok = true; 386 } 387 } 388 host++; 389 } 390 if (post_colon && (! port_ok) ) 391 { 392 return GNUNET_strdup ("port missing after ':'"); 393 } 394 return NULL; 395 } 396 397 398 /** 399 * Generic validation of a payto:// URI. Checks the prefix 400 * and character set. 401 * 402 * @param payto_uri URI to validate 403 * @return NULL on success, otherwise an error message 404 */ 405 static char * 406 payto_validate (const char *payto_uri) 407 { 408 char *ret; 409 const char *start; 410 const char *end; 411 412 if (0 != strncasecmp (payto_uri, 413 PAYTO, 414 strlen (PAYTO))) 415 return GNUNET_strdup ("invalid prefix"); 416 for (unsigned int i = 0; '\0' != payto_uri[i]; i++) 417 { 418 /* This is more strict than RFC 8905, alas we do not need to support messages/instructions/etc., 419 and it is generally better to start with a narrow whitelist; we can be more permissive later ...*/ 420 #define ALLOWED_CHARACTERS \ 421 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/:$&?!-_.,;=*+%~@()[]" 422 if (NULL == strchr (ALLOWED_CHARACTERS, 423 (int) payto_uri[i])) 424 { 425 GNUNET_asprintf (&ret, 426 "Encountered invalid character `%c' at offset %u in payto URI `%s'", 427 payto_uri[i], 428 i, 429 payto_uri); 430 return ret; 431 } 432 #undef ALLOWED_CHARACTERS 433 } 434 435 start = &payto_uri[strlen (PAYTO)]; 436 end = strchr (start, 437 (unsigned char) '/'); 438 if (NULL == end) 439 return GNUNET_strdup ("missing '/' in payload"); 440 441 if (NULL != (ret = validate_payto_iban (payto_uri))) 442 return ret; /* got a definitive answer */ 443 if (NULL != (ret = validate_payto_xtalerbank (payto_uri))) 444 return ret; /* got a definitive answer */ 445 446 /* Insert validation calls for other bank account validation methods here! */ 447 448 return NULL; 449 } 450 451 452 char * 453 TALER_normalized_payto_validate (const struct TALER_NormalizedPayto npayto_uri) 454 { 455 const char *payto_uri = npayto_uri.normalized_payto; 456 char *ret; 457 458 ret = payto_validate (payto_uri); 459 if (NULL != ret) 460 return ret; 461 return NULL; 462 } 463 464 465 char * 466 TALER_payto_validate (const struct TALER_FullPayto fpayto_uri) 467 { 468 const char *payto_uri = fpayto_uri.full_payto; 469 char *ret; 470 471 ret = payto_validate (payto_uri); 472 if (NULL != ret) 473 return ret; 474 { 475 char *target; 476 477 target = payto_get_key (fpayto_uri, 478 "receiver-name="); 479 if (NULL == target) 480 return GNUNET_strdup ("'receiver-name' parameter missing"); 481 GNUNET_free (target); 482 } 483 484 return NULL; 485 } 486 487 488 char * 489 TALER_payto_get_receiver_name (const struct TALER_FullPayto fpayto) 490 { 491 char *err; 492 493 err = TALER_payto_validate (fpayto); 494 if (NULL != err) 495 { 496 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 497 "Invalid payto://-URI `%s': %s\n", 498 fpayto.full_payto, 499 err); 500 GNUNET_free (err); 501 return NULL; 502 } 503 return payto_get_key (fpayto, 504 "receiver-name="); 505 } 506 507 508 /** 509 * Normalize "payto://x-taler-bank/$HOSTNAME/[$PATH/]$USERNAME" 510 * URI in @a input. 511 * 512 * Converts to lower-case, except for [$PATH/]$USERNAME which 513 * is case-sensitive. 514 * 515 * @param len number of bytes in @a input 516 * @param input input URL 517 * @return NULL on error, otherwise 0-terminated canonicalized URI. 518 */ 519 static char * 520 normalize_payto_x_taler_bank (size_t len, 521 const char input[static len]) 522 { 523 char *res = GNUNET_malloc (len + 1); 524 unsigned int sc = 0; 525 526 for (unsigned int i = 0; i<len; i++) 527 { 528 char c = input[i]; 529 530 if ('/' == c) 531 sc++; 532 if (sc < 4) 533 res[i] = (char) tolower ((int) c); 534 else 535 res[i] = c; 536 } 537 return res; 538 } 539 540 541 /** 542 * Normalize "payto://iban[/$BIC]/$IBAN" 543 * URI in @a input. 544 * 545 * Removes $BIC (if present) and converts $IBAN to upper-case and prefix to 546 * lower-case. 547 * 548 * @param len number of bytes in @a input 549 * @param input input URL 550 * @return NULL on error, otherwise 0-terminated canonicalized URI. 551 */ 552 static char * 553 normalize_payto_iban (size_t len, 554 const char input[static len]) 555 { 556 char *res; 557 size_t pos = 0; 558 unsigned int sc = 0; 559 bool have_bic; 560 561 for (unsigned int i = 0; i<len; i++) 562 if ('/' == input[i]) 563 sc++; 564 if ( (sc > 4) || 565 (sc < 3) ) 566 { 567 GNUNET_break (0); 568 return NULL; 569 } 570 have_bic = (4 == sc); 571 res = GNUNET_malloc (len + 1); 572 sc = 0; 573 for (unsigned int i = 0; i<len; i++) 574 { 575 char c = input[i]; 576 577 if ('/' == c) 578 sc++; 579 switch (sc) 580 { 581 case 0: /* payto: */ 582 case 1: /* / */ 583 case 2: /* /iban */ 584 res[pos++] = (char) tolower ((int) c); 585 break; 586 case 3: /* /$BIC or /$IBAN */ 587 if (have_bic) 588 continue; 589 res[pos++] = (char) toupper ((int) c); 590 break; 591 case 4: /* /$IBAN */ 592 res[pos++] = (char) toupper ((int) c); 593 break; 594 } 595 } 596 GNUNET_assert (pos <= len); 597 return res; 598 } 599 600 601 /** 602 * Normalize "payto://upi/$EMAIL" 603 * URI in @a input. 604 * 605 * Converts to lower-case. 606 * 607 * @param len number of bytes in @a input 608 * @param input input URL 609 * @return NULL on error, otherwise 0-terminated canonicalized URI. 610 */ 611 static char * 612 normalize_payto_upi (size_t len, 613 const char input[static len]) 614 { 615 char *res = GNUNET_malloc (len + 1); 616 617 for (unsigned int i = 0; i<len; i++) 618 { 619 char c = input[i]; 620 621 res[i] = (char) tolower ((int) c); 622 } 623 return res; 624 } 625 626 627 /** 628 * Normalize "payto://bitcoin/$ADDRESS" 629 * URI in @a input. 630 * 631 * Converts to lower-case, except for $ADDRESS which 632 * is case-sensitive. 633 * 634 * @param len number of bytes in @a input 635 * @param input input URL 636 * @return NULL on error, otherwise 0-terminated canonicalized URI. 637 */ 638 static char * 639 normalize_payto_bitcoin (size_t len, 640 const char input[static len]) 641 { 642 char *res = GNUNET_malloc (len + 1); 643 unsigned int sc = 0; 644 645 for (unsigned int i = 0; i<len; i++) 646 { 647 char c = input[i]; 648 649 if ('/' == c) 650 sc++; 651 if (sc < 3) 652 res[i] = (char) tolower ((int) c); 653 else 654 res[i] = c; 655 } 656 return res; 657 } 658 659 660 /** 661 * Normalize "payto://ilp/$NAME" 662 * URI in @a input. 663 * 664 * Converts to lower-case. 665 * 666 * @param len number of bytes in @a input 667 * @param input input URL 668 * @return NULL on error, otherwise 0-terminated canonicalized URI. 669 */ 670 static char * 671 normalize_payto_ilp (size_t len, 672 const char input[static len]) 673 { 674 char *res = GNUNET_malloc (len + 1); 675 676 for (unsigned int i = 0; i<len; i++) 677 { 678 char c = input[i]; 679 680 res[i] = (char) tolower ((int) c); 681 } 682 return res; 683 } 684 685 686 struct TALER_NormalizedPayto 687 TALER_payto_normalize (const struct TALER_FullPayto input) 688 { 689 struct TALER_NormalizedPayto npto = { 690 .normalized_payto = NULL 691 }; 692 char *method; 693 const char *end; 694 char *ret; 695 696 { 697 char *err; 698 699 err = TALER_payto_validate (input); 700 if (NULL != err) 701 { 702 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 703 "Malformed payto://-URI `%s': %s\n", 704 input.full_payto, 705 err); 706 GNUNET_free (err); 707 return npto; 708 } 709 } 710 method = TALER_payto_get_method (input.full_payto); 711 if (NULL == method) 712 { 713 GNUNET_break (0); 714 return npto; 715 } 716 end = strchr (input.full_payto, 717 '?'); 718 if (NULL == end) 719 end = &input.full_payto[strlen (input.full_payto)]; 720 if (0 == strcasecmp (method, 721 "x-taler-bank")) 722 ret = normalize_payto_x_taler_bank (end - input.full_payto, 723 input.full_payto); 724 else if (0 == strcasecmp (method, 725 "iban")) 726 ret = normalize_payto_iban (end - input.full_payto, 727 input.full_payto); 728 else if (0 == strcasecmp (method, 729 "upi")) 730 ret = normalize_payto_upi (end - input.full_payto, 731 input.full_payto); 732 else if (0 == strcasecmp (method, 733 "bitcoin")) 734 ret = normalize_payto_bitcoin (end - input.full_payto, 735 input.full_payto); 736 else if (0 == strcasecmp (method, 737 "ilp")) 738 ret = normalize_payto_ilp (end - input.full_payto, 739 input.full_payto); 740 else 741 ret = GNUNET_strndup (input.full_payto, 742 end - input.full_payto); 743 GNUNET_free (method); 744 npto.normalized_payto = ret; 745 return npto; 746 } 747 748 749 void 750 TALER_normalized_payto_hash (const struct TALER_NormalizedPayto npayto, 751 struct TALER_NormalizedPaytoHashP *h_npayto) 752 { 753 struct GNUNET_HashCode sha512; 754 755 GNUNET_CRYPTO_hash (npayto.normalized_payto, 756 strlen (npayto.normalized_payto) + 1, 757 &sha512); 758 GNUNET_static_assert (sizeof (sha512) > sizeof (*h_npayto)); 759 /* truncate */ 760 GNUNET_memcpy (h_npayto, 761 &sha512, 762 sizeof (*h_npayto)); 763 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 764 "Normalized hash of normalized payto `%s' is %16s\n", 765 npayto.normalized_payto, 766 GNUNET_h2s_full (&sha512)); 767 } 768 769 770 void 771 TALER_full_payto_hash (const struct TALER_FullPayto fpayto, 772 struct TALER_FullPaytoHashP *h_fpayto) 773 { 774 struct GNUNET_HashCode sha512; 775 776 GNUNET_CRYPTO_hash (fpayto.full_payto, 777 strlen (fpayto.full_payto) + 1, 778 &sha512); 779 GNUNET_static_assert (sizeof (sha512) > sizeof (*h_fpayto)); 780 /* truncate */ 781 GNUNET_memcpy (h_fpayto, 782 &sha512, 783 sizeof (*h_fpayto)); 784 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 785 "Full hash of full payto `%s' is %16s\n", 786 fpayto.full_payto, 787 GNUNET_h2s_full (&sha512)); 788 } 789 790 791 struct TALER_NormalizedPayto 792 TALER_reserve_make_payto (const char *exchange_url, 793 const struct TALER_ReservePublicKeyP *reserve_pub) 794 { 795 struct TALER_NormalizedPayto npto = { 796 .normalized_payto = NULL 797 }; 798 char pub_str[sizeof (*reserve_pub) * 2]; 799 char *end; 800 bool is_http; 801 char *reserve_url; 802 803 end = GNUNET_STRINGS_data_to_string ( 804 reserve_pub, 805 sizeof (*reserve_pub), 806 pub_str, 807 sizeof (pub_str)); 808 *end = '\0'; 809 if (0 == strncmp (exchange_url, 810 "http://", 811 strlen ("http://"))) 812 { 813 is_http = true; 814 exchange_url = &exchange_url[strlen ("http://")]; 815 } 816 else if (0 == strncmp (exchange_url, 817 "https://", 818 strlen ("https://"))) 819 { 820 is_http = false; 821 exchange_url = &exchange_url[strlen ("https://")]; 822 } 823 else 824 { 825 GNUNET_break (0); 826 return npto; 827 } 828 /* exchange_url includes trailing '/' */ 829 GNUNET_asprintf (&reserve_url, 830 "payto://%s/%s%s", 831 is_http ? "taler-reserve-http" : "taler-reserve", 832 exchange_url, 833 pub_str); 834 npto.normalized_payto = reserve_url; 835 return npto; 836 } 837 838 839 /* end of payto.c */