payto.c (20872B)
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 "platform.h" /* UNNECESSARY? */ 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 size_t slen = strlen (payto_uri); 240 const char *iban; 241 const char *q; 242 char *result; 243 char *err; 244 245 #define IBAN_PREFIX "payto://iban/" 246 if (0 != strncasecmp (payto_uri, 247 IBAN_PREFIX, 248 strlen (IBAN_PREFIX))) 249 return NULL; /* not an IBAN */ 250 q = memchr (payto_uri, 251 '?', 252 slen); 253 /* Find the last '/' that is part of the path, i.e. before any '?' 254 query part ('/' is a legal character in a query value and must 255 not be mistaken for the path separator). */ 256 { 257 const char *slash; 258 259 slash = memrchr (payto_uri, 260 '/', 261 (NULL == q) 262 ? slen 263 : q - payto_uri); 264 GNUNET_assert (NULL != slash); /* prefix guarantees a '/' */ 265 iban = slash + 1; 266 } 267 #undef IBAN_PREFIX 268 if (NULL != q) 269 { 270 result = GNUNET_strndup (iban, 271 q - iban); 272 } 273 else 274 { 275 result = GNUNET_strdup (iban); 276 } 277 if (NULL != 278 (err = TALER_iban_validate (result))) 279 { 280 GNUNET_free (result); 281 return err; 282 } 283 GNUNET_free (result); 284 return NULL; 285 } 286 287 288 /** 289 * Validate payto://x-taler-bank/ account URL (only account information, 290 * wire subject and amount are ignored). 291 * 292 * @param payto_uri payto URL to parse 293 * @return NULL on success, otherwise an error message 294 * to be freed by the caller 295 */ 296 static char * 297 validate_payto_xtalerbank (const char *payto_uri) 298 { 299 const char *user; 300 const char *nxt; 301 const char *beg; 302 const char *end; 303 const char *host; 304 bool dot_ok; 305 bool post_colon; 306 bool port_ok; 307 308 #define XTALERBANK_PREFIX PAYTO "x-taler-bank/" 309 if (0 != strncasecmp (payto_uri, 310 XTALERBANK_PREFIX, 311 strlen (XTALERBANK_PREFIX))) 312 return NULL; /* not an x-taler-bank URI */ 313 host = &payto_uri[strlen (XTALERBANK_PREFIX)]; 314 #undef XTALERBANK_PREFIX 315 beg = strchr (host, 316 '/'); 317 if (NULL == beg) 318 { 319 return GNUNET_strdup ("account name missing"); 320 } 321 beg++; /* now points to $ACCOUNT or $PATH */ 322 nxt = strchr (beg, 323 '/'); 324 end = strchr (beg, 325 '?'); 326 if (NULL == end) 327 { 328 return GNUNET_strdup ("'receiver-name' parameter missing"); 329 } 330 while ( (NULL != nxt) && 331 (end - nxt > 0) ) 332 { 333 beg = nxt + 1; 334 nxt = strchr (beg, 335 '/'); 336 } 337 user = beg; 338 if (user == host + 1) 339 { 340 return GNUNET_strdup ("domain name missing"); 341 } 342 if ('-' == host[0]) 343 return GNUNET_strdup ("invalid character '-' at start of domain name"); 344 dot_ok = false; 345 post_colon = false; 346 port_ok = false; 347 while (host != user) 348 { 349 char c = host[0]; 350 351 if ('/' == c) 352 { 353 /* path started, do not care about characters 354 in the path */ 355 break; 356 } 357 if (':' == c) 358 { 359 post_colon = true; 360 host++; 361 continue; 362 } 363 if (post_colon) 364 { 365 if (! ( ('0' <= c) && ('9' >= c) ) ) 366 { 367 char *err; 368 369 GNUNET_asprintf (&err, 370 "invalid character '%c' in port", 371 c); 372 return err; 373 } 374 port_ok = true; 375 } 376 else 377 { 378 if ('.' == c) 379 { 380 if (! dot_ok) 381 return GNUNET_strdup ("invalid domain name (misplaced '.')"); 382 dot_ok = false; 383 } 384 else 385 { 386 if (! ( ('-' == c) || 387 ('_' == c) || 388 ( ('0' <= c) && ('9' >= c) ) || 389 ( ('a' <= c) && ('z' >= c) ) || 390 ( ('A' <= c) && ('Z' >= c) ) ) ) 391 { 392 char *err; 393 394 GNUNET_asprintf (&err, 395 "invalid character '%c' in domain name", 396 c); 397 return err; 398 } 399 dot_ok = true; 400 } 401 } 402 host++; 403 } 404 if (post_colon && (! port_ok) ) 405 { 406 return GNUNET_strdup ("port missing after ':'"); 407 } 408 return NULL; 409 } 410 411 412 /** 413 * Generic validation of a payto:// URI. Checks the prefix 414 * and character set. 415 * 416 * @param payto_uri URI to validate 417 * @return NULL on success, otherwise an error message 418 */ 419 static char * 420 payto_validate (const char *payto_uri) 421 { 422 char *ret; 423 const char *start; 424 const char *end; 425 426 if (0 != strncasecmp (payto_uri, 427 PAYTO, 428 strlen (PAYTO))) 429 return GNUNET_strdup ("invalid prefix"); 430 for (unsigned int i = 0; '\0' != payto_uri[i]; i++) 431 { 432 /* This is more strict than RFC 8905, alas we do not need to support messages/instructions/etc., 433 and it is generally better to start with a narrow whitelist; we can be more permissive later ...*/ 434 #define ALLOWED_CHARACTERS \ 435 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/:$&?!-_.,;=*+%~@()[]" 436 if (NULL == strchr (ALLOWED_CHARACTERS, 437 (int) payto_uri[i])) 438 { 439 GNUNET_asprintf (&ret, 440 "Encountered invalid character `%c' at offset %u in payto URI `%s'", 441 payto_uri[i], 442 i, 443 payto_uri); 444 return ret; 445 } 446 #undef ALLOWED_CHARACTERS 447 } 448 449 start = &payto_uri[strlen (PAYTO)]; 450 end = strchr (start, 451 (unsigned char) '/'); 452 if (NULL == end) 453 return GNUNET_strdup ("missing '/' in payload"); 454 455 if (NULL != (ret = validate_payto_iban (payto_uri))) 456 return ret; /* got a definitive answer */ 457 if (NULL != (ret = validate_payto_xtalerbank (payto_uri))) 458 return ret; /* got a definitive answer */ 459 460 /* Insert validation calls for other bank account validation methods here! */ 461 462 return NULL; 463 } 464 465 466 char * 467 TALER_normalized_payto_validate (const struct TALER_NormalizedPayto npayto_uri) 468 { 469 const char *payto_uri = npayto_uri.normalized_payto; 470 char *ret; 471 472 ret = payto_validate (payto_uri); 473 if (NULL != ret) 474 return ret; 475 return NULL; 476 } 477 478 479 char * 480 TALER_payto_validate (const struct TALER_FullPayto fpayto_uri) 481 { 482 const char *payto_uri = fpayto_uri.full_payto; 483 char *ret; 484 485 ret = payto_validate (payto_uri); 486 if (NULL != ret) 487 return ret; 488 { 489 char *target; 490 491 target = payto_get_key (fpayto_uri, 492 "receiver-name="); 493 if (NULL == target) 494 return GNUNET_strdup ("'receiver-name' parameter missing"); 495 GNUNET_free (target); 496 } 497 498 return NULL; 499 } 500 501 502 char * 503 TALER_payto_get_receiver_name (const struct TALER_FullPayto fpayto) 504 { 505 char *err; 506 507 err = TALER_payto_validate (fpayto); 508 if (NULL != err) 509 { 510 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 511 "Invalid payto://-URI `%s': %s\n", 512 fpayto.full_payto, 513 err); 514 GNUNET_free (err); 515 return NULL; 516 } 517 return payto_get_key (fpayto, 518 "receiver-name="); 519 } 520 521 522 /** 523 * Normalize "payto://x-taler-bank/$HOSTNAME/[$PATH/]$USERNAME" 524 * URI in @a input. 525 * 526 * Converts to lower-case, except for [$PATH/]$USERNAME which 527 * is case-sensitive. 528 * 529 * @param len number of bytes in @a input 530 * @param input input URL 531 * @return NULL on error, otherwise 0-terminated canonicalized URI. 532 */ 533 static char * 534 normalize_payto_x_taler_bank (size_t len, 535 const char input[static len]) 536 { 537 char *res = GNUNET_malloc (len + 1); 538 unsigned int sc = 0; 539 540 for (unsigned int i = 0; i<len; i++) 541 { 542 char c = input[i]; 543 544 if ('/' == c) 545 sc++; 546 if (sc < 4) 547 res[i] = (char) tolower ((int) c); 548 else 549 res[i] = c; 550 } 551 return res; 552 } 553 554 555 /** 556 * Normalize "payto://iban[/$BIC]/$IBAN" 557 * URI in @a input. 558 * 559 * Removes $BIC (if present) and converts $IBAN to upper-case and prefix to 560 * lower-case. 561 * 562 * @param len number of bytes in @a input 563 * @param input input URL 564 * @return NULL on error, otherwise 0-terminated canonicalized URI. 565 */ 566 static char * 567 normalize_payto_iban (size_t len, 568 const char input[static len]) 569 { 570 char *res; 571 size_t pos = 0; 572 unsigned int sc = 0; 573 bool have_bic; 574 575 for (unsigned int i = 0; i<len; i++) 576 if ('/' == input[i]) 577 sc++; 578 if ( (sc > 4) || 579 (sc < 3) ) 580 { 581 GNUNET_break (0); 582 return NULL; 583 } 584 have_bic = (4 == sc); 585 res = GNUNET_malloc (len + 1); 586 sc = 0; 587 for (unsigned int i = 0; i<len; i++) 588 { 589 char c = input[i]; 590 591 if ('/' == c) 592 sc++; 593 switch (sc) 594 { 595 case 0: /* payto: */ 596 case 1: /* / */ 597 case 2: /* /iban */ 598 res[pos++] = (char) tolower ((int) c); 599 break; 600 case 3: /* /$BIC or /$IBAN */ 601 if (have_bic) 602 continue; 603 res[pos++] = (char) toupper ((int) c); 604 break; 605 case 4: /* /$IBAN */ 606 res[pos++] = (char) toupper ((int) c); 607 break; 608 } 609 } 610 GNUNET_assert (pos <= len); 611 return res; 612 } 613 614 615 /** 616 * Normalize "payto://upi/$EMAIL" 617 * URI in @a input. 618 * 619 * Converts to lower-case. 620 * 621 * @param len number of bytes in @a input 622 * @param input input URL 623 * @return NULL on error, otherwise 0-terminated canonicalized URI. 624 */ 625 static char * 626 normalize_payto_upi (size_t len, 627 const char input[static len]) 628 { 629 char *res = GNUNET_malloc (len + 1); 630 631 for (unsigned int i = 0; i<len; i++) 632 { 633 char c = input[i]; 634 635 res[i] = (char) tolower ((int) c); 636 } 637 return res; 638 } 639 640 641 /** 642 * Normalize "payto://bitcoin/$ADDRESS" 643 * URI in @a input. 644 * 645 * Converts to lower-case, except for $ADDRESS which 646 * is case-sensitive. 647 * 648 * @param len number of bytes in @a input 649 * @param input input URL 650 * @return NULL on error, otherwise 0-terminated canonicalized URI. 651 */ 652 static char * 653 normalize_payto_bitcoin (size_t len, 654 const char input[static len]) 655 { 656 char *res = GNUNET_malloc (len + 1); 657 unsigned int sc = 0; 658 659 for (unsigned int i = 0; i<len; i++) 660 { 661 char c = input[i]; 662 663 if ('/' == c) 664 sc++; 665 if (sc < 3) 666 res[i] = (char) tolower ((int) c); 667 else 668 res[i] = c; 669 } 670 return res; 671 } 672 673 674 /** 675 * Normalize "payto://ilp/$NAME" 676 * URI in @a input. 677 * 678 * Converts to lower-case. 679 * 680 * @param len number of bytes in @a input 681 * @param input input URL 682 * @return NULL on error, otherwise 0-terminated canonicalized URI. 683 */ 684 static char * 685 normalize_payto_ilp (size_t len, 686 const char input[static len]) 687 { 688 char *res = GNUNET_malloc (len + 1); 689 690 for (unsigned int i = 0; i<len; i++) 691 { 692 char c = input[i]; 693 694 res[i] = (char) tolower ((int) c); 695 } 696 return res; 697 } 698 699 700 struct TALER_NormalizedPayto 701 TALER_payto_normalize (const struct TALER_FullPayto input) 702 { 703 struct TALER_NormalizedPayto npto = { 704 .normalized_payto = NULL 705 }; 706 char *method; 707 const char *end; 708 char *ret; 709 710 { 711 char *err; 712 713 err = TALER_payto_validate (input); 714 if (NULL != err) 715 { 716 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 717 "Malformed payto://-URI `%s': %s\n", 718 input.full_payto, 719 err); 720 GNUNET_free (err); 721 return npto; 722 } 723 } 724 method = TALER_payto_get_method (input.full_payto); 725 if (NULL == method) 726 { 727 GNUNET_break (0); 728 return npto; 729 } 730 end = strchr (input.full_payto, 731 '?'); 732 if (NULL == end) 733 end = &input.full_payto[strlen (input.full_payto)]; 734 if (0 == strcasecmp (method, 735 "x-taler-bank")) 736 ret = normalize_payto_x_taler_bank (end - input.full_payto, 737 input.full_payto); 738 else if (0 == strcasecmp (method, 739 "iban")) 740 ret = normalize_payto_iban (end - input.full_payto, 741 input.full_payto); 742 else if (0 == strcasecmp (method, 743 "upi")) 744 ret = normalize_payto_upi (end - input.full_payto, 745 input.full_payto); 746 else if (0 == strcasecmp (method, 747 "bitcoin")) 748 ret = normalize_payto_bitcoin (end - input.full_payto, 749 input.full_payto); 750 else if (0 == strcasecmp (method, 751 "ilp")) 752 ret = normalize_payto_ilp (end - input.full_payto, 753 input.full_payto); 754 else 755 ret = GNUNET_strndup (input.full_payto, 756 end - input.full_payto); 757 GNUNET_free (method); 758 npto.normalized_payto = ret; 759 return npto; 760 } 761 762 763 void 764 TALER_normalized_payto_hash (const struct TALER_NormalizedPayto npayto, 765 struct TALER_NormalizedPaytoHashP *h_npayto) 766 { 767 struct GNUNET_HashCode sha512; 768 769 GNUNET_CRYPTO_hash (npayto.normalized_payto, 770 strlen (npayto.normalized_payto) + 1, 771 &sha512); 772 GNUNET_static_assert (sizeof (sha512) > sizeof (*h_npayto)); 773 /* truncate */ 774 GNUNET_memcpy (h_npayto, 775 &sha512, 776 sizeof (*h_npayto)); 777 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 778 "Normalized hash of normalized payto `%s' is %16s\n", 779 npayto.normalized_payto, 780 GNUNET_h2s_full (&sha512)); 781 } 782 783 784 void 785 TALER_full_payto_hash (const struct TALER_FullPayto fpayto, 786 struct TALER_FullPaytoHashP *h_fpayto) 787 { 788 struct GNUNET_HashCode sha512; 789 790 GNUNET_CRYPTO_hash (fpayto.full_payto, 791 strlen (fpayto.full_payto) + 1, 792 &sha512); 793 GNUNET_static_assert (sizeof (sha512) > sizeof (*h_fpayto)); 794 /* truncate */ 795 GNUNET_memcpy (h_fpayto, 796 &sha512, 797 sizeof (*h_fpayto)); 798 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 799 "Full hash of full payto `%s' is %16s\n", 800 fpayto.full_payto, 801 GNUNET_h2s_full (&sha512)); 802 } 803 804 805 struct TALER_NormalizedPayto 806 TALER_reserve_make_payto (const char *exchange_url, 807 const struct TALER_ReservePublicKeyP *reserve_pub) 808 { 809 struct TALER_NormalizedPayto npto = { 810 .normalized_payto = NULL 811 }; 812 char pub_str[sizeof (*reserve_pub) * 2]; 813 char *end; 814 bool is_http; 815 char *reserve_url; 816 817 end = GNUNET_STRINGS_data_to_string ( 818 reserve_pub, 819 sizeof (*reserve_pub), 820 pub_str, 821 sizeof (pub_str)); 822 *end = '\0'; 823 if (0 == strncmp (exchange_url, 824 "http://", 825 strlen ("http://"))) 826 { 827 is_http = true; 828 exchange_url = &exchange_url[strlen ("http://")]; 829 } 830 else if (0 == strncmp (exchange_url, 831 "https://", 832 strlen ("https://"))) 833 { 834 is_http = false; 835 exchange_url = &exchange_url[strlen ("https://")]; 836 } 837 else 838 { 839 GNUNET_break (0); 840 return npto; 841 } 842 /* exchange_url includes trailing '/' */ 843 GNUNET_asprintf (&reserve_url, 844 "payto://%s/%s%s", 845 is_http ? "taler-reserve-http" : "taler-reserve", 846 exchange_url, 847 pub_str); 848 npto.normalized_payto = reserve_url; 849 return npto; 850 } 851 852 853 /* end of payto.c */