1/* 2 * Copyright (c) 2005, PADL Software Pty Ltd. 3 * All rights reserved. 4 * 5 * Portions Copyright (c) 2009 - 2011 Apple Inc. All rights reserved. 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * 1. Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 14 * 2. Redistributions in binary form must reproduce the above copyright 15 * notice, this list of conditions and the following disclaimer in the 16 * documentation and/or other materials provided with the distribution. 17 * 18 * 3. Neither the name of PADL Software nor the names of its contributors 19 * may be used to endorse or promote products derived from this software 20 * without specific prior written permission. 21 * 22 * THIS SOFTWARE IS PROVIDED BY PADL SOFTWARE AND CONTRIBUTORS ``AS IS'' AND 23 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 25 * ARE DISCLAIMED. IN NO EVENT SHALL PADL SOFTWARE OR CONTRIBUTORS BE LIABLE 26 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 28 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 29 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 31 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 32 * SUCH DAMAGE. 33 */ 34 35#include "kcm_locl.h" 36 37#ifdef HAVE_NOTIFY_H 38#include <mach/mach.h> 39#include <mach/mach_time.h> 40#endif 41 42#include <uuid/uuid.h> 43#include <bsm/libbsm.h> 44#include <vproc.h> 45#include <vproc_priv.h> 46 47static void kcm_release_ccache_locked(krb5_context, kcm_ccache); 48 49 50 51HEIMDAL_MUTEX ccache_mutex = HEIMDAL_MUTEX_INITIALIZER; 52TAILQ_HEAD(ccache_head, kcm_ccache_data); 53static struct ccache_head ccache_head = TAILQ_HEAD_INITIALIZER(ccache_head); 54static uint32_t ccache_nextid = 0; 55 56#ifdef HAVE_NOTIFY_H 57 58static uint64_t 59relative_nano_time(void) 60{ 61 static uint64_t factor; 62 uint64_t now; 63 64 now = mach_absolute_time(); 65 66 if (factor == 0) { 67 mach_timebase_info_data_t base; 68 (void)mach_timebase_info(&base); 69 factor = base.numer / base.denom; 70 } 71 72 return now * factor; 73} 74 75#endif 76 77/* 78 * Deliver a notification every NOTIFY_TIME_LIMIT 79 */ 80 81static void 82notify_changed_caches(void) 83{ 84#ifdef HAVE_NOTIFY_H 85 static uint64_t last_change; 86 static int notify_pending; 87 88#define NOTIFY_TIME_LIMIT (NSEC_PER_SEC / 2) 89 90 dispatch_async(dispatch_get_main_queue(), ^{ 91 uint64_t now, diff; 92 93 if (notify_pending) 94 return; 95 96 now = relative_nano_time(); 97 if (now < last_change) 98 diff = NOTIFY_TIME_LIMIT; 99 else 100 diff = now - last_change; 101 102 if (diff >= NOTIFY_TIME_LIMIT) { 103 notify_post(KRB5_KCM_NOTIFY_CACHE_CHANGED); 104 last_change = now; 105 } else { 106 notify_pending = 1; 107 /* wait up to deliver the event */ 108 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NOTIFY_TIME_LIMIT - diff), dispatch_get_main_queue(), ^{ 109 notify_pending = 0; 110 last_change = relative_nano_time(); 111 notify_post(KRB5_KCM_NOTIFY_CACHE_CHANGED); 112 }); 113 } 114 }); 115#endif 116} 117 118char * 119kcm_ccache_nextid(pid_t pid, uid_t uid) 120{ 121 kcm_ccache c; 122 char *name = NULL; 123 unsigned n; 124 125 HEIMDAL_MUTEX_lock(&ccache_mutex); 126 while (name == NULL) { 127 n = ++ccache_nextid; 128 asprintf(&name, "%ld:%u", (long)uid, n); 129 130 /* find dups */ 131 TAILQ_FOREACH(c, &ccache_head, members) { 132 if (strcmp(c->name, name) == 0) { 133 free(name); 134 name = NULL; 135 break; 136 } 137 } 138 } 139 HEIMDAL_MUTEX_unlock(&ccache_mutex); 140 141 return name; 142} 143 144krb5_error_code 145kcm_ccache_resolve_by_name(krb5_context context, 146 const char *name, 147 kcm_ccache *ccache) 148{ 149 kcm_ccache p; 150 krb5_error_code ret; 151 152 *ccache = NULL; 153 154 ret = KRB5_FCC_NOFILE; 155 156 HEIMDAL_MUTEX_lock(&ccache_mutex); 157 158 TAILQ_FOREACH(p, &ccache_head, members) { 159 if (strcmp(p->name, name) == 0) { 160 ret = 0; 161 break; 162 } 163 } 164 165 if (ret == 0) { 166 kcm_retain_ccache(context, p); 167 *ccache = p; 168 } 169 170 HEIMDAL_MUTEX_unlock(&ccache_mutex); 171 172 return ret; 173} 174 175krb5_error_code 176kcm_ccache_resolve_by_uuid(krb5_context context, 177 kcmuuid_t uuid, 178 kcm_ccache *ccache) 179{ 180 kcm_ccache p; 181 krb5_error_code ret; 182 183 *ccache = NULL; 184 185 ret = KRB5_FCC_NOFILE; 186 187 HEIMDAL_MUTEX_lock(&ccache_mutex); 188 189 TAILQ_FOREACH(p, &ccache_head, members) { 190 if (memcmp(p->uuid, uuid, sizeof(kcmuuid_t)) == 0) { 191 ret = 0; 192 break; 193 } 194 } 195 196 if (ret == 0) { 197 kcm_retain_ccache(context, p); 198 *ccache = p; 199 } 200 201 HEIMDAL_MUTEX_unlock(&ccache_mutex); 202 203 return ret; 204} 205 206krb5_error_code 207kcm_ccache_get_uuids(krb5_context context, 208 kcm_client *client, 209 kcm_operation opcode, 210 krb5_storage *sp) 211{ 212 kcm_ccache p; 213 214 HEIMDAL_MUTEX_lock(&ccache_mutex); 215 216 TAILQ_FOREACH(p, &ccache_head, members) { 217 krb5_error_code ret; 218 ret = kcm_access(context, client, opcode, p); 219 if (ret) 220 continue; 221 krb5_storage_write(sp, p->uuid, sizeof(p->uuid)); 222 } 223 224 HEIMDAL_MUTEX_unlock(&ccache_mutex); 225 226 return 0; 227} 228 229 230krb5_error_code 231kcm_debug_ccache(krb5_context context) 232{ 233 kcm_ccache p; 234 235 TAILQ_FOREACH(p, &ccache_head, members) { 236 char *cpn = NULL, *spn = NULL; 237 int ncreds = 0; 238 struct kcm_creds *k; 239 240 KCM_ASSERT_VALID(p); 241 242 for (k = p->creds; k != NULL; k = k->next) 243 ncreds++; 244 245 if (p->client != NULL) 246 krb5_unparse_name(context, p->client, &cpn); 247 if (p->server != NULL) 248 krb5_unparse_name(context, p->server, &spn); 249 250 kcm_log(7, "cache name %s refcnt %d flags %04x" 251 "uid %d client %s server %s ncreds %d", 252 p->name, p->refcnt, p->flags, p->uid, 253 (cpn == NULL) ? "<none>" : cpn, 254 (spn == NULL) ? "<none>" : spn, 255 ncreds); 256 257 if (cpn != NULL) 258 free(cpn); 259 if (spn != NULL) 260 free(spn); 261 } 262 263 return 0; 264} 265 266static void 267kcm_free_ccache_data_internal(krb5_context context, 268 kcm_ccache cache) 269{ 270 KCM_ASSERT_VALID(cache); 271 272 if (cache->name != NULL) { 273 free(cache->name); 274 cache->name = NULL; 275 } 276 277 if (cache->flags & KCM_FLAGS_USE_KEYTAB) { 278 krb5_kt_close(context, cache->keytab); 279 cache->keytab = NULL; 280 } else if (cache->flags & KCM_FLAGS_USE_PASSWORD) { 281 memset(cache->password, 0, strlen(cache->password)); 282 free(cache->password); 283 } 284 285 cache->flags = 0; 286 cache->uid = -1; 287 cache->session = -1; 288 289 kcm_zero_ccache_data_internal(context, cache); 290 291 cache->tkt_life = 0; 292 cache->renew_life = 0; 293 294 cache->refcnt = 0; 295} 296 297 298krb5_error_code 299kcm_ccache_destroy(krb5_context context, const char *name) 300{ 301 kcm_ccache p; 302 303 HEIMDAL_MUTEX_lock(&ccache_mutex); 304 TAILQ_FOREACH(p, &ccache_head, members) { 305 if (strcmp(p->name, name) == 0) { 306 HEIMDAL_MUTEX_lock(&p->mutex); 307 TAILQ_REMOVE(&ccache_head, p, members); 308 break; 309 } 310 } 311 HEIMDAL_MUTEX_unlock(&ccache_mutex); 312 if (p == NULL) 313 return KRB5_FCC_NOFILE; 314 315 notify_changed_caches(); 316 317 /* XXX blocking */ 318 heim_ipc_event_cancel(p->renew_event); 319 heim_ipc_event_free(p->renew_event); 320 p->renew_event = NULL; 321 322 heim_ipc_event_cancel(p->expire_event); 323 heim_ipc_event_free(p->expire_event); 324 p->expire_event = NULL; 325 326 kcm_release_ccache_locked(context, p); 327 328 return 0; 329} 330 331#define KCM_EVENT_QUEUE_INTERVAL 60 332 333void 334kcm_update_renew_time(kcm_ccache ccache) 335{ 336 time_t renewtime = time(NULL) + 3600 * 2; 337 time_t expire = ccache->expire; 338 339 /* if the ticket is about to expire in less then QUEUE_INTERVAL, 340 * don't bother */ 341 if (time(NULL) + KCM_EVENT_QUEUE_INTERVAL > expire) 342 return; 343 344 if (renewtime > expire - KCM_EVENT_QUEUE_INTERVAL) 345 renewtime = expire - KCM_EVENT_QUEUE_INTERVAL; 346 347 kcm_log(1, "%s: will try to renew credentals in %d seconds", 348 ccache->name, (int)(renewtime - time(NULL))); 349 350 heim_ipc_event_set_time(ccache->renew_event, renewtime); 351 ccache->renew_time = renewtime; 352} 353 354void 355kcm_update_expire_time(kcm_ccache cache, time_t t) 356{ 357 if (t == 0) { 358 t = time(NULL); 359 } else if (t < time(NULL)) { 360 cache->next_refresh_time = 0; 361 return; 362 } 363 cache->next_refresh_time = t; 364 heim_ipc_event_set_time(cache->expire_event, t); 365} 366 367static void 368renew_func(heim_event_t event, void *ptr) 369{ 370 kcm_ccache cache = ptr; 371 krb5_error_code ret; 372 time_t expire; 373 374 kcm_log(0, "cache: %s renewing", cache->name); 375 376 HEIMDAL_MUTEX_lock(&cache->mutex); 377 378 if (cache->flags & KCM_MASK_KEY_PRESENT) { 379 ret = kcm_ccache_acquire(kcm_context, cache, &expire); 380 krb5_warn(kcm_context, ret, "cache: %s acquire complete", cache->name); 381 } else { 382 ret = kcm_ccache_refresh(kcm_context, cache, &expire); 383 krb5_warn(kcm_context, ret, "cache: %s renew complete", cache->name); 384 } 385 386 switch (ret) { 387 case KRB5KRB_AP_ERR_BAD_INTEGRITY: 388 case KRB5KRB_AP_ERR_MODIFIED: 389 case KRB5KDC_ERR_PREAUTH_FAILED: 390 /* bad password, drop it like dead */ 391 kcm_log(0, "cache: %s got bad password, stop renewing", 392 cache->name); 393 kcm_ccache_update_acquire_status(kcm_context, cache, KCM_STATUS_ACQUIRE_STOPPED, ret); 394 cache->flags &= ~KCM_MASK_KEY_PRESENT; 395 break; 396 case 0: 397 kcm_data_changed = 1; 398 cache->expire = expire; 399 kcm_ccache_update_acquire_status(kcm_context, cache, KCM_STATUS_ACQUIRE_SUCCESS, 0); 400 401 notify_changed_caches(); 402 403 break; 404 default: { 405 const char *msg = krb5_get_error_message(kcm_context, ret); 406 kcm_log(0, "failed to renew: %s: %d", msg, ret); 407 krb5_free_error_message(kcm_context, msg); 408 kcm_ccache_update_acquire_status(kcm_context, cache, KCM_STATUS_ACQUIRE_FAILED, ret); 409 break; 410 } 411 } 412 kcm_update_renew_time(cache); 413 HEIMDAL_MUTEX_unlock(&cache->mutex); 414} 415 416static void 417expire_func(heim_event_t event, void *ptr) 418{ 419 kcm_ccache cache = ptr; 420 krb5_error_code ret; 421 422 kcm_log(0, "cache: %s expired", cache->name); 423 424 HEIMDAL_MUTEX_lock(&cache->mutex); 425 426 heim_ipc_event_cancel(cache->renew_event); 427 428 cache->next_refresh_time = 0; 429 430 if (cache->flags & KCM_MASK_KEY_PRESENT){ 431 time_t expire; 432 433 ret = kcm_ccache_acquire(kcm_context, cache, &expire); 434 435 switch (ret) { 436 case KRB5KRB_AP_ERR_BAD_INTEGRITY: 437 case KRB5KRB_AP_ERR_MODIFIED: 438 case KRB5KDC_ERR_PREAUTH_FAILED: 439 /* bad password, drop it like dead */ 440 kcm_log(0, "cache: %s got bad password, stop renewing", 441 cache->name); 442 kcm_ccache_update_acquire_status(kcm_context, cache, KCM_STATUS_ACQUIRE_STOPPED, ret); 443 cache->flags &= ~KCM_MASK_KEY_PRESENT; 444 break; 445 case 0: 446 kcm_data_changed = 1; 447 kcm_log(0, "cache: %s got new tickets (expire in %d seconds)", 448 cache->name, (int)(expire - time(NULL))); 449 450 cache->expire = expire; 451 kcm_ccache_update_acquire_status(kcm_context, cache, KCM_STATUS_ACQUIRE_SUCCESS, 0); 452 notify_changed_caches(); 453 break; 454 default: 455 kcm_data_changed = 1; 456 kcm_update_expire_time(cache, time(NULL) + 300); 457 kcm_ccache_update_acquire_status(kcm_context, cache, KCM_STATUS_ACQUIRE_FAILED, ret); 458 notify_changed_caches(); 459 break; 460 } 461 } else { 462 kcm_log(0, "cache: %s expired", cache->name); 463 notify_changed_caches(); 464 } 465 HEIMDAL_MUTEX_unlock(&cache->mutex); 466} 467 468static void 469release_cache(void *ctx) 470{ 471 kcm_release_ccache(kcm_context, (kcm_ccache)ctx); 472} 473 474static krb5_error_code 475kcm_ccache_alloc(krb5_context context, 476 const char *name, 477 kcm_ccache *cache) 478{ 479 kcm_ccache p = NULL; 480 krb5_error_code ret; 481 482 /* First, check for duplicates */ 483 HEIMDAL_MUTEX_lock(&ccache_mutex); 484 485 TAILQ_FOREACH(p, &ccache_head, members) { 486 if (strcmp(p->name, name) == 0) { 487 ret = KRB5_CC_WRITE; 488 goto out; 489 } 490 } 491 492 /* 493 * Create an enpty cache for us. 494 */ 495 p = calloc(1, sizeof(*p)); 496 if (p == NULL) { 497 ret = KRB5_CC_NOMEM; 498 goto out; 499 } 500 HEIMDAL_MUTEX_init(&p->mutex); 501 502 CCRandomCopyBytes(kCCRandomDefault, p->uuid, sizeof(p->uuid)); 503 504 p->name = strdup(name); 505 if (p->name == NULL) { 506 ret = KRB5_CC_NOMEM; 507 goto out; 508 } 509 510 p->refcnt = 3; /* on members, and both events */ 511 p->holdcount = 1; 512 p->flags = 0; 513 p->uid = -1; 514 p->client = NULL; 515 p->server = NULL; 516 p->creds = NULL; 517 p->keytab = NULL; 518 p->password = NULL; 519 p->tkt_life = 0; 520 p->renew_life = 0; 521 522 p->renew_event = heim_ipc_event_create_f(renew_func, p); 523 p->expire_event = heim_ipc_event_create_f(expire_func, p); 524 525 heim_ipc_event_set_final_f(p->renew_event, release_cache); 526 heim_ipc_event_set_final_f(p->expire_event, release_cache); 527 528 TAILQ_INSERT_HEAD(&ccache_head, p, members); 529 530 *cache = p; 531 532 HEIMDAL_MUTEX_unlock(&ccache_mutex); 533 return 0; 534 535out: 536 HEIMDAL_MUTEX_unlock(&ccache_mutex); 537 if (p != NULL) { 538 HEIMDAL_MUTEX_destroy(&p->mutex); 539 free(p); 540 } 541 *cache = NULL; 542 return ret; 543} 544 545#define KRB5_CONF_NAME "krb5_ccache_conf_data" 546#define KRB5_REALM_NAME "X-CACHECONF:" 547 548krb5_error_code 549kcm_ccache_update_acquire_status(krb5_context context, 550 kcm_ccache ccache, 551 int status, 552 krb5_error_code ret) 553{ 554 krb5_creds cred; 555 uint8_t st[12]; 556 uint32_t u32; 557 558 if ((ccache->flags & KCM_MASK_KEY_PRESENT) == 0) 559 return 0; 560 if (ccache->client == NULL) 561 return 0; 562 563 switch (status) { 564 case KCM_STATUS_ACQUIRE_START: 565 kcm_update_expire_time(ccache, 0); 566 break; 567 568 case KCM_STATUS_ACQUIRE_STOPPED: 569 ccache->next_refresh_time = 0; 570 break; 571 572 case KCM_STATUS_ACQUIRE_FAILED: 573 kcm_update_expire_time(ccache, time(NULL) + 300); 574 break; 575 576 case KCM_STATUS_ACQUIRE_SUCCESS: { 577 time_t next_refresh, now = time(NULL); 578 579 if (ccache->expire > now) { 580 next_refresh = ccache->expire; 581 /* try to acquire credential just before */ 582 if (ccache->expire - now > 300) 583 next_refresh -= 300; 584 kcm_update_expire_time(ccache, next_refresh + 300); 585 } else { 586 ccache->next_refresh_time = 0; 587 } 588 break; 589 } 590 default: 591 heim_assert(0, "invalid status"); 592 break; 593 } 594 595 memcpy(st, "krb5", 4); 596 u32 = htonl(status); memcpy(&st[4], &u32, sizeof(u32)); 597 u32 = htonl(ret); memcpy(&st[8], &u32, sizeof(u32)); 598 599 memset(&cred, 0, sizeof(cred)); 600 601 cred.client = ccache->client; 602 ret = krb5_make_principal(context, &cred.server, 603 KRB5_REALM_NAME, KRB5_CONF_NAME, 604 KCM_STATUS_KEY, NULL); 605 if (ret) 606 return ret; 607 608 cred.ticket.data = st; 609 cred.ticket.length = sizeof(st); 610 cred.times.authtime = time(NULL); 611 cred.times.endtime = cred.times.authtime + 3600 * 24 * 30; 612 613 ret = kcm_ccache_store_cred_internal(context, ccache, &cred, NULL, 1); 614 615 krb5_free_principal(context, cred.server); 616 617 return ret; 618} 619 620krb5_error_code 621kcm_ccache_enqueue_default(krb5_context context, 622 kcm_ccache cache, 623 krb5_creds *newcred) 624{ 625 if (newcred == NULL) { 626 627 } else if (cache->flags & KCM_MASK_KEY_PRESENT) { 628 cache->expire = newcred->times.endtime; 629 kcm_update_renew_time(cache); 630 } else if (newcred->flags.b.renewable) { 631 cache->expire = newcred->times.endtime; 632 kcm_update_renew_time(cache); 633 kcm_update_expire_time(cache, newcred->times.endtime); 634 } else if (newcred->times.endtime > time(NULL)) { 635 cache->expire = newcred->times.endtime; 636 kcm_update_expire_time(cache, newcred->times.endtime); 637 } 638 639 notify_changed_caches(); 640 641 return 0; 642} 643 644 645krb5_error_code 646kcm_ccache_remove_creds_internal(krb5_context context, 647 kcm_ccache ccache) 648{ 649 struct kcm_creds *k; 650 651 k = ccache->creds; 652 while (k != NULL) { 653 struct kcm_creds *old; 654 655 krb5_free_cred_contents(context, &k->cred); 656 old = k; 657 k = k->next; 658 free(old); 659 } 660 ccache->creds = NULL; 661 662 notify_changed_caches(); 663 664 return 0; 665} 666 667krb5_error_code 668kcm_ccache_remove_creds(krb5_context context, 669 kcm_ccache ccache) 670{ 671 krb5_error_code ret; 672 673 KCM_ASSERT_VALID(ccache); 674 675 HEIMDAL_MUTEX_lock(&ccache->mutex); 676 ret = kcm_ccache_remove_creds_internal(context, ccache); 677 HEIMDAL_MUTEX_unlock(&ccache->mutex); 678 679 return ret; 680} 681 682krb5_error_code 683kcm_zero_ccache_data_internal(krb5_context context, 684 kcm_ccache cache) 685{ 686 if (cache->client != NULL) { 687 krb5_free_principal(context, cache->client); 688 cache->client = NULL; 689 } 690 691 if (cache->server != NULL) { 692 krb5_free_principal(context, cache->server); 693 cache->server = NULL; 694 } 695 696 kcm_ccache_remove_creds_internal(context, cache); 697 698 return 0; 699} 700 701krb5_error_code 702kcm_zero_ccache_data(krb5_context context, 703 kcm_ccache cache) 704{ 705 krb5_error_code ret; 706 707 KCM_ASSERT_VALID(cache); 708 709 HEIMDAL_MUTEX_lock(&cache->mutex); 710 ret = kcm_zero_ccache_data_internal(context, cache); 711 HEIMDAL_MUTEX_unlock(&cache->mutex); 712 713 return ret; 714} 715 716krb5_error_code 717kcm_retain_ccache(krb5_context context, 718 kcm_ccache ccache) 719{ 720 KCM_ASSERT_VALID(ccache); 721 722 HEIMDAL_MUTEX_lock(&ccache->mutex); 723 ccache->refcnt++; 724 HEIMDAL_MUTEX_unlock(&ccache->mutex); 725 726 return 0; 727} 728 729static void 730kcm_release_ccache_locked(krb5_context context, kcm_ccache p) 731{ 732 if (p->refcnt == 1) { 733 kcm_free_ccache_data_internal(context, p); 734 HEIMDAL_MUTEX_unlock(&p->mutex); 735 HEIMDAL_MUTEX_destroy(&p->mutex); 736 free(p); 737 } else { 738 p->refcnt--; 739 HEIMDAL_MUTEX_unlock(&p->mutex); 740 } 741} 742 743 744krb5_error_code 745kcm_release_ccache(krb5_context context, kcm_ccache c) 746{ 747 KCM_ASSERT_VALID(c); 748 749 HEIMDAL_MUTEX_lock(&c->mutex); 750 kcm_release_ccache_locked(context, c); 751 752 return 0; 753} 754 755krb5_error_code 756kcm_ccache_new(krb5_context context, 757 const char *name, 758 kcm_ccache *ccache) 759{ 760 krb5_error_code ret; 761 762 ret = kcm_ccache_alloc(context, name, ccache); 763 if (ret == 0) { 764 /* 765 * one reference is held by the linked list, 766 * one by the caller 767 */ 768 kcm_retain_ccache(context, *ccache); 769 } 770 771 return ret; 772} 773 774krb5_error_code 775kcm_ccache_store_cred(krb5_context context, 776 kcm_ccache ccache, 777 krb5_creds *creds, 778 int copy) 779{ 780 krb5_error_code ret; 781 782 KCM_ASSERT_VALID(ccache); 783 784 HEIMDAL_MUTEX_lock(&ccache->mutex); 785 ret = kcm_ccache_store_cred_internal(context, ccache, creds, NULL, copy); 786 HEIMDAL_MUTEX_unlock(&ccache->mutex); 787 788 return ret; 789} 790 791struct kcm_creds * 792kcm_ccache_find_cred_uuid(krb5_context context, 793 kcm_ccache ccache, 794 kcmuuid_t uuid) 795{ 796 struct kcm_creds *c; 797 798 for (c = ccache->creds; c != NULL; c = c->next) 799 if (memcmp(c->uuid, uuid, sizeof(c->uuid)) == 0) 800 return c; 801 802 return NULL; 803} 804 805 806 807krb5_error_code 808kcm_ccache_store_cred_internal(krb5_context context, 809 kcm_ccache ccache, 810 krb5_creds *creds, 811 kcmuuid_t uuid, 812 int copy) 813{ 814 struct kcm_creds **c; 815 krb5_error_code ret; 816 817 /* 818 * Remove dup creds and find the end to add new credential. 819 */ 820 821 c = &ccache->creds; 822 while (*c != NULL) { 823 if (krb5_compare_creds(context, 0, creds, &(*c)->cred)) { 824 struct kcm_creds *dup_cred = *c; 825 *c = dup_cred->next; 826 krb5_free_cred_contents(context, &dup_cred->cred); 827 free(dup_cred); 828 } else { 829 c = &(*c)->next; 830 } 831 } 832 833 *c = (struct kcm_creds *)calloc(1, sizeof(**c)); 834 if (*c == NULL) 835 return KRB5_CC_NOMEM; 836 837 if (uuid) 838 memcpy((*c)->uuid, uuid, sizeof((*c)->uuid)); 839 else 840 CCRandomCopyBytes(kCCRandomDefault, (*c)->uuid, sizeof((*c)->uuid)); 841 842 if (copy) { 843 ret = krb5_copy_creds_contents(context, creds, &(*c)->cred); 844 if (ret) { 845 free(*c); 846 *c = NULL; 847 } 848 } else { 849 (*c)->cred = *creds; 850 ret = 0; 851 } 852 853 /* 854 * Only push notification when the krbtgt in the cache changes. 855 */ 856 if ((*c)->cred.server && krb5_principal_is_root_krbtgt(context, (*c)->cred.server)) 857 notify_changed_caches(); 858 859 return ret; 860} 861 862krb5_error_code 863kcm_ccache_remove_cred_internal(krb5_context context, 864 kcm_ccache ccache, 865 krb5_flags whichfields, 866 const krb5_creds *mcreds) 867{ 868 krb5_error_code ret; 869 struct kcm_creds **c; 870 871 ret = KRB5_CC_NOTFOUND; 872 873 for (c = &ccache->creds; *c != NULL; c = &(*c)->next) { 874 if (krb5_compare_creds(context, whichfields, mcreds, &(*c)->cred)) { 875 struct kcm_creds *cred = *c; 876 877 *c = cred->next; 878 krb5_free_cred_contents(context, &cred->cred); 879 free(cred); 880 ret = 0; 881 if (*c == NULL) 882 break; 883 } 884 } 885 886 notify_changed_caches(); 887 888 return ret; 889} 890 891krb5_error_code 892kcm_ccache_remove_cred(krb5_context context, 893 kcm_ccache ccache, 894 krb5_flags whichfields, 895 const krb5_creds *mcreds) 896{ 897 krb5_error_code ret; 898 899 KCM_ASSERT_VALID(ccache); 900 901 HEIMDAL_MUTEX_lock(&ccache->mutex); 902 ret = kcm_ccache_remove_cred_internal(context, ccache, whichfields, mcreds); 903 HEIMDAL_MUTEX_unlock(&ccache->mutex); 904 905 return ret; 906} 907 908krb5_error_code 909kcm_ccache_retrieve_cred_internal(krb5_context context, 910 kcm_ccache ccache, 911 krb5_flags whichfields, 912 const krb5_creds *mcreds, 913 krb5_creds **creds) 914{ 915 krb5_boolean match; 916 struct kcm_creds *c; 917 krb5_error_code ret; 918 919 memset(creds, 0, sizeof(*creds)); 920 921 ret = KRB5_CC_END; 922 923 match = FALSE; 924 for (c = ccache->creds; c != NULL; c = c->next) { 925 match = krb5_compare_creds(context, whichfields, mcreds, &c->cred); 926 if (match) 927 break; 928 } 929 930 if (match) { 931 ret = 0; 932 *creds = &c->cred; 933 } 934 935 return ret; 936} 937 938krb5_error_code 939kcm_ccache_retrieve_cred(krb5_context context, 940 kcm_ccache ccache, 941 krb5_flags whichfields, 942 const krb5_creds *mcreds, 943 krb5_creds **credp) 944{ 945 krb5_error_code ret; 946 947 KCM_ASSERT_VALID(ccache); 948 949 HEIMDAL_MUTEX_lock(&ccache->mutex); 950 ret = kcm_ccache_retrieve_cred_internal(context, ccache, 951 whichfields, mcreds, credp); 952 HEIMDAL_MUTEX_unlock(&ccache->mutex); 953 954 return ret; 955} 956 957char * 958kcm_ccache_first_name(kcm_client *client) 959{ 960 kcm_ccache p; 961 char *name = NULL; 962 963 HEIMDAL_MUTEX_lock(&ccache_mutex); 964 965 TAILQ_FOREACH(p, &ccache_head, members) { 966 if (kcm_is_same_session(client, p->uid, p->session)) 967 break; 968 } 969 if (p) 970 name = strdup(p->name); 971 HEIMDAL_MUTEX_unlock(&ccache_mutex); 972 return name; 973} 974 975void 976kcm_cache_remove_session(pid_t session) 977{ 978 kcm_ccache p, tempp; 979 980 HEIMDAL_MUTEX_lock(&ccache_mutex); 981 982 TAILQ_FOREACH_SAFE(p, &ccache_head, members, tempp) { 983 if (p->session == session) { 984 kcm_log(1, "remove credental %s because session %d went away", 985 p->name, (int)session); 986 987 TAILQ_REMOVE(&ccache_head, p, members); 988 989 dispatch_async(dispatch_get_main_queue(), ^{ 990 /* XXX blocking */ 991 HEIMDAL_MUTEX_lock(&p->mutex); 992 993 heim_ipc_event_cancel(p->renew_event); 994 heim_ipc_event_free(p->renew_event); 995 p->renew_event = NULL; 996 997 heim_ipc_event_cancel(p->expire_event); 998 heim_ipc_event_free(p->expire_event); 999 p->expire_event = NULL; 1000 1001 kcm_release_ccache_locked(kcm_context, p); 1002 }); 1003 } 1004 } 1005 HEIMDAL_MUTEX_unlock(&ccache_mutex); 1006} 1007 1008static bool 1009session_exists(pid_t asid) 1010{ 1011 auditinfo_addr_t aia; 1012 aia.ai_asid = asid; 1013 1014 if (audit_get_sinfo_addr(&aia, sizeof(aia)) == 0) 1015 return true; 1016 return false; 1017} 1018 1019 1020#define CHECK(s) do { if ((s)) { goto out; } } while(0) 1021 1022#define DUMP_F_SERVER 1 1023#define DUMP_F_PASSWORD 2 1024#define DUMP_F_KEYTAB 4 1025 1026 1027static krb5_error_code 1028parse_krb5_cache(krb5_context context, krb5_storage *sp) 1029{ 1030 krb5_error_code ret; 1031 kcm_ccache c = NULL; 1032 char *name = NULL; 1033 uint32_t u32; 1034 int32_t s32; 1035 uint8_t u8; 1036 time_t renew_time; 1037 1038 CHECK(ret = krb5_ret_stringz(sp, &name)); 1039 ret = kcm_ccache_new(context, name, &c); 1040 free(name); 1041 CHECK(ret); 1042 CHECK(ret = krb5_ret_uuid(sp, c->uuid)); 1043 CHECK(ret = krb5_ret_uint32(sp, &u32)); 1044 c->renew_time = renew_time = u32; 1045 CHECK(ret = krb5_ret_uint32(sp, &u32)); 1046 c->next_refresh_time = u32; 1047 CHECK(ret = krb5_ret_uint32(sp, &u32)); 1048 c->holdcount = u32; 1049 CHECK(ret = krb5_ret_uint32(sp, &u32)); 1050 c->flags = u32; 1051 CHECK(ret = krb5_ret_int32(sp, &s32)); 1052 c->uid = s32; 1053 CHECK(ret = krb5_ret_int32(sp, &s32)); 1054 c->session = s32; 1055 1056 CHECK(ret = krb5_ret_principal(sp, &c->client)); 1057 1058 CHECK(ret = krb5_ret_uint32(sp, &u32)); 1059 1060 if (u32 & DUMP_F_SERVER) 1061 CHECK(ret = krb5_ret_principal(sp, &c->server)); 1062 1063 if (u32 & DUMP_F_PASSWORD) 1064 CHECK(ret = krb5_ret_stringz(sp, &c->password)); 1065 1066 if (u32 & DUMP_F_KEYTAB) { 1067 char *keytab; 1068 CHECK(ret = krb5_ret_stringz(sp, &keytab)); 1069 CHECK(ret = krb5_kt_resolve(context, keytab, &c->keytab)); 1070 free(keytab); 1071 } 1072 1073 while (1) { 1074 krb5_creds cred; 1075 kcmuuid_t uuid; 1076 1077 CHECK(ret = krb5_ret_uint8(sp, &u8)); 1078 if (u8 == 0) 1079 break; 1080 1081 CHECK(ret = krb5_ret_uuid(sp, uuid)); 1082 CHECK(ret = krb5_ret_creds(sp, &cred)); 1083 ret = kcm_ccache_store_cred_internal(context, c, &cred, uuid, 1); 1084 1085 krb5_free_cred_contents(context, &cred); 1086 CHECK(ret); 1087 } 1088 1089 /* if we have a renew time and its not too far in the past, kick off rewtime again */ 1090 if (renew_time && renew_time > time(NULL) - 60) { 1091 kcm_log(1, "re-setting renew time to: %ds (original renew time)", (int)(renew_time - time(NULL))); 1092 heim_ipc_event_set_time(c->renew_event, renew_time); 1093 } 1094 if (c->next_refresh_time) 1095 kcm_update_expire_time(c, c->next_refresh_time); 1096 1097 out: 1098 /* in case of failure, abandon memory (and broken cache) */ 1099 if (ret && c) { 1100 TAILQ_REMOVE(&ccache_head, c, members); 1101 } 1102 1103 return ret; 1104} 1105 1106static void 1107update_wakeup(time_t *nextwakeup, time_t t) 1108{ 1109 /* 1110 * don't bother wakeing up if its within 30s, to sort of time anyway, 1111 * just let the credential expire 1112 */ 1113 if (t < time(NULL) - 30) 1114 return; 1115 if (*nextwakeup == 0 || *nextwakeup > t) 1116 *nextwakeup = t; 1117} 1118 1119static krb5_error_code 1120unparse_krb5_cache(krb5_context context, krb5_storage *sp, kcm_ccache c, time_t *nextwakeup) 1121{ 1122 struct kcm_creds *cred; 1123 krb5_error_code ret; 1124 uint32_t sflags; 1125 1126 if (c->renew_time) 1127 update_wakeup(nextwakeup, c->renew_time); 1128 1129 if ((c->flags & KCM_MASK_KEY_PRESENT) && c->next_refresh_time) 1130 update_wakeup(nextwakeup, c->next_refresh_time); 1131 1132 CHECK(ret = krb5_store_stringz(sp, c->name)); 1133 CHECK(ret = krb5_store_uuid(sp, c->uuid)); 1134 CHECK(ret = krb5_store_uint32(sp, (uint32_t)c->renew_time)); 1135 CHECK(ret = krb5_store_uint32(sp, (uint32_t)c->next_refresh_time)); 1136 CHECK(ret = krb5_store_uint32(sp, (uint32_t)c->holdcount)); 1137 CHECK(ret = krb5_store_uint32(sp, c->flags)); 1138 CHECK(ret = krb5_store_int32(sp, c->uid)); 1139 CHECK(ret = krb5_store_int32(sp, c->session)); 1140 1141 CHECK(ret = krb5_store_principal(sp, c->client)); 1142 1143 sflags = 0; 1144 if (c->server) sflags |= DUMP_F_SERVER; 1145 if (c->password) sflags |= DUMP_F_PASSWORD; 1146 if (c->keytab) sflags |= DUMP_F_KEYTAB; 1147 CHECK(ret = krb5_store_uint32(sp, sflags)); 1148 1149 if (c->server) 1150 CHECK(ret = krb5_store_principal(sp, c->server)); 1151 if (c->password) 1152 CHECK(ret = krb5_store_stringz(sp, c->password)); 1153 if (c->keytab) { 1154 char *str; 1155 CHECK(ret = krb5_kt_get_full_name(context, c->keytab, &str)); 1156 CHECK(ret = krb5_store_stringz(sp, str)); 1157 krb5_xfree(str); 1158 } 1159 1160 for (cred = c->creds; cred != NULL; cred = cred->next) { 1161 CHECK(ret = krb5_store_uint8(sp, 1)); 1162 CHECK(ret = krb5_store_uuid(sp, cred->uuid)); 1163 CHECK(ret = krb5_store_creds(sp, &cred->cred)); 1164 } 1165 CHECK(ret = krb5_store_uint8(sp, 0)); 1166 out: 1167 return ret; 1168} 1169 1170static krb5_error_code 1171parse_default_one(krb5_context context, krb5_storage *sp) 1172{ 1173 struct kcm_default_cache *c; 1174 krb5_error_code ret; 1175 int32_t s32; 1176 char *str; 1177 1178 c = calloc(1, sizeof(*c)); 1179 if (c == NULL) 1180 return ENOMEM; 1181 1182 CHECK(ret = krb5_ret_int32(sp, &s32)); 1183 c->uid = s32; 1184 CHECK(ret = krb5_ret_int32(sp, &s32)); 1185 c->session = s32; 1186 CHECK(ret = krb5_ret_stringz(sp, &str)); 1187 c->name = str; 1188 1189 c->next = default_caches; 1190 default_caches = c; 1191 1192 out: 1193 if (ret) 1194 kcm_log(10, "failed to parse default entry"); 1195 return ret; 1196} 1197 1198static krb5_error_code 1199unparse_default_one(krb5_storage *inner, struct kcm_default_cache *c) 1200{ 1201 krb5_error_code ret; 1202 CHECK(ret = krb5_store_int32(inner, c->uid)); 1203 CHECK(ret = krb5_store_int32(inner, c->session)); 1204 CHECK(ret = krb5_store_stringz(inner, c->name)); 1205 out: 1206 return ret; 1207} 1208 1209static krb5_error_code 1210unparse_default_all(krb5_context context, krb5_storage *sp) 1211{ 1212 struct kcm_default_cache *c; 1213 krb5_error_code r = 0; 1214 1215 for (c = default_caches; r == 0 && c != NULL; c = c->next) { 1216 r = kcm_unparse_wrap(sp, "default-cache", c->session, ^(krb5_storage *inner) { 1217 return unparse_default_one(inner, c); 1218 }); 1219 1220 } 1221 1222 return r; 1223} 1224 1225#define KCM_DUMP_VERSION 2 1226 1227static int 1228kcm_parse_cache_data(krb5_context context, krb5_data *data) 1229{ 1230 krb5_error_code ret; 1231 krb5_storage *sp; 1232 char *str; 1233 uint8_t u8; 1234 1235 sp = krb5_storage_from_readonly_mem(data->data, data->length); 1236 if (sp == NULL) 1237 return ENOMEM; 1238 1239 CHECK(ret = krb5_ret_stringz(sp, &str)); 1240 if (strcmp(str, "start-dump") != 0) { 1241 free(str); 1242 ret = EINVAL; 1243 goto out; 1244 } 1245 free(str); 1246 1247 1248 CHECK(ret = krb5_ret_uint8(sp, &u8)); 1249 if (u8 != KCM_DUMP_VERSION) { 1250 ret = EINVAL; 1251 goto out; 1252 } 1253 CHECK(ret = krb5_ret_uint32(sp, &ccache_nextid)); 1254 1255 while(ret == 0) { 1256 int32_t session; 1257 krb5_data idata; 1258 krb5_storage *inner; 1259 1260 CHECK(ret = krb5_ret_stringz(sp, &str)); 1261 1262 kcm_log(10, "dump: reading a %s entry", str); 1263 1264 if (strcmp(str, "end-dump") == 0) { 1265 free(str); 1266 break; 1267 } 1268 1269 CHECK(ret = krb5_ret_int32(sp, &session)); 1270 CHECK(ret = krb5_ret_data(sp, &idata)); 1271 1272 inner = krb5_storage_from_data(&idata); 1273 heim_assert(inner, "krb5_storage_from_data"); 1274 1275 if (strcmp(str, "ntlm-cache") == 0) { 1276 ret = kcm_parse_ntlm_challenge_one(context, inner); 1277 } else if (!session_exists(session)) { 1278 /* do nothing */ 1279 } else if (strcmp(str, "krb5-cache") == 0) { 1280 ret = parse_krb5_cache(context, inner); 1281 } else if (strcmp(str, "digest-cache") == 0) { 1282 ret = kcm_parse_digest_one(context, inner); 1283 } else if (strcmp(str, "default-cache") == 0) { 1284 ret = parse_default_one(context, inner); 1285 } else { 1286 kcm_log(10, "dump: unknown type: %s", str); 1287 ret = 0; 1288 } 1289 if (ret) 1290 kcm_log(10, "dump: failed to unparse a %s cache with: %d", str, ret); 1291 free(str); 1292 krb5_storage_free(inner); 1293 krb5_data_free(&idata); 1294 } 1295 out: 1296 krb5_storage_free(sp); 1297 if (ret) 1298 kcm_log(10, "dump: failed to read credential dump: %d", ret); 1299 return ret; 1300} 1301 1302krb5_error_code 1303kcm_unparse_wrap(krb5_storage *sp, char *name, int32_t session, int (^wrapped)(krb5_storage *inner)) 1304{ 1305 krb5_error_code ret; 1306 krb5_storage *inner = krb5_storage_emem(); 1307 krb5_data data; 1308 1309 CHECK(ret = wrapped(inner)); 1310 CHECK(ret = krb5_store_stringz(sp, name)); 1311 CHECK(ret = krb5_store_int32(sp, session)); 1312 CHECK(ret = krb5_storage_to_data(inner, &data)); 1313 ret = krb5_store_data(sp, data); 1314 krb5_data_free(&data); 1315 1316 out: 1317 if (ret) 1318 kcm_log(10, "dump: failed to add a %s", name); 1319 krb5_storage_free(inner); 1320 return ret; 1321} 1322 1323void 1324kcm_unparse_cache_data(krb5_context context, krb5_data *data) 1325{ 1326 __block time_t nextwakeup = 0; 1327 krb5_error_code ret; 1328 krb5_storage *sp; 1329 kcm_ccache c; 1330 1331 krb5_data_zero(data); 1332 1333 sp = krb5_storage_emem(); 1334 if (sp == NULL) 1335 return; 1336 1337 CHECK(ret = krb5_store_stringz(sp, "start-dump")); 1338 CHECK(ret = krb5_store_uint8(sp, KCM_DUMP_VERSION)); 1339 CHECK(ret = krb5_store_uint32(sp, ccache_nextid)); 1340 1341 HEIMDAL_MUTEX_lock(&ccache_mutex); 1342 TAILQ_FOREACH_REVERSE(c, &ccache_head, ccache_head, members) { 1343 ret = kcm_unparse_wrap(sp, "krb5-cache", c->session, ^(krb5_storage *inner) { 1344 return unparse_krb5_cache(context, inner, c, &nextwakeup); 1345 }); 1346 } 1347 HEIMDAL_MUTEX_unlock(&ccache_mutex); 1348 CHECK(ret); 1349 1350 /* add default cache */ 1351 CHECK(ret = unparse_default_all(context, sp)); 1352 1353 /* add NTLM/SCRAM */ 1354 CHECK(ret = kcm_unparse_digest_all(context, sp)); 1355 1356 /* ntlm challenges */ 1357 CHECK(ret = kcm_unparse_challenge_all(context, sp)); 1358 1359 CHECK(ret = krb5_store_stringz(sp, "end-dump")); 1360 1361 if (nextwakeup) { 1362 int64_t next = nextwakeup - time(NULL); 1363 if (next > 0) { 1364 vproc_swap_integer(NULL, VPROC_GSK_START_INTERVAL, &next, NULL); 1365 } 1366 kcm_log(1, "next wakup in: %d", (int)next); 1367 } 1368 1369 out: 1370 if (ret == 0) { 1371 ret = krb5_storage_to_data(sp, data); 1372 if (ret) 1373 kcm_log(1, "dump: failed to create credential data: %d", ret); 1374 } 1375 krb5_storage_free(sp); 1376} 1377 1378static int have_uuid_master = 0; 1379static krb5_uuid uuid_master; 1380static const char *dumpfile = "/var/db/kcm-dump.bin"; 1381static const char *keyfile = "/var/db/kcm-dump.uuid"; 1382 1383static krb5_error_code 1384kcm_load_key(krb5_context context) 1385{ 1386 krb5_error_code ret; 1387 krb5_data enc; 1388 size_t len; 1389 void *p = NULL; 1390 1391 if (have_uuid_master) 1392 return 0; 1393 1394 ret = rk_undumpdata(keyfile, &p, &len); 1395 if (ret != 0) 1396 goto nokey; 1397 1398 if (len != sizeof(uuid_master)) { 1399 free(p); 1400 goto nokey; 1401 } 1402 1403 memcpy(uuid_master, p, sizeof(uuid_master)); 1404 free(p); 1405 1406 /* check if key is stale */ 1407 ret = kcm_store_io(context, uuid_master, "", 0, &enc, true); 1408 if (ret) 1409 goto nokey; 1410 1411 krb5_data_free(&enc); 1412 have_uuid_master = 1; 1413 1414 return 0; 1415 nokey: 1416 1417 ret = kcm_create_key(uuid_master); 1418 if (ret) 1419 return ret; 1420 rk_dumpdata(keyfile, uuid_master, sizeof(uuid_master)); 1421 1422 have_uuid_master = 1; 1423 1424 return ret; 1425} 1426 1427int kcm_data_changed = 0; 1428 1429void 1430kcm_write_dump(krb5_context context) 1431{ 1432 uuid_string_t uuidstr; 1433 krb5_data data, enc; 1434 krb5_error_code ret; 1435 1436 ret = kcm_load_key(context); 1437 if (ret) { 1438 unlink(keyfile); 1439 unlink(dumpfile); 1440 return; 1441 } 1442 1443 uuid_unparse(uuid_master, uuidstr); 1444 kcm_log(10, "dump: [masterkey] %s", uuidstr); 1445 1446 kcm_unparse_cache_data(context, &data); 1447 if (data.length == 0) 1448 return; 1449 1450 if (!kcm_data_changed) 1451 return; 1452 1453 ret = kcm_store_io(context, uuid_master, data.data, data.length, &enc, true); 1454 krb5_data_free(&data); 1455 if (ret) { 1456 kcm_log(1, "dump: failed to encrypt credential data %d", ret); 1457 return; 1458 } 1459 1460 rk_dumpdata(dumpfile, enc.data, enc.length); 1461 1462 krb5_data_free(&enc); 1463} 1464 1465void 1466kcm_read_dump(krb5_context context) 1467{ 1468 uuid_string_t uuidstr; 1469 krb5_error_code ret; 1470 krb5_data data; 1471 size_t len; 1472 void *p; 1473 1474 ret = kcm_load_key(context); 1475 if (ret) 1476 return; 1477 1478 uuid_unparse(uuid_master, uuidstr); 1479 kcm_log(10, "load: [masterkey] %s", uuidstr); 1480 1481 ret = rk_undumpdata(dumpfile, &p, &len); 1482 if (ret != 0 || len == 0) 1483 return; 1484 1485 ret = kcm_store_io(kcm_context, uuid_master, p, len, &data, false); 1486 if (ret == 0) { 1487 ret = kcm_parse_cache_data(kcm_context, &data); 1488 if (ret) 1489 unlink(dumpfile); 1490 krb5_data_free(&data); 1491 have_uuid_master = 1; 1492 } else { 1493 unlink(dumpfile); 1494 } 1495 free(p); 1496} 1497 1498