1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License, Version 1.0 only 6 * (the "License"). You may not use this file except in compliance 7 * with the License. 8 * 9 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE 10 * or http://www.opensolaris.org/os/licensing. 11 * See the License for the specific language governing permissions 12 * and limitations under the License. 13 * 14 * When distributing Covered Code, include this CDDL HEADER in each 15 * file and include the License file at usr/src/OPENSOLARIS.LICENSE. 16 * If applicable, add the following below this CDDL HEADER, with the 17 * fields enclosed by brackets "[]" replaced with your own identifying 18 * information: Portions Copyright [yyyy] [name of copyright owner] 19 * 20 * CDDL HEADER END 21 */ 22 /* 23 * Copyright 2005 Sun Microsystems, Inc. All rights reserved. 24 * Use is subject to license terms. 25 */ 26 27 #pragma ident "%Z%%M% %I% %E% SMI" 28 29 #include <libintl.h> 30 #include <security/pam_appl.h> 31 #include <security/pam_modules.h> 32 #include <string.h> 33 #include <stdio.h> 34 #include <stdlib.h> 35 #include <sys/types.h> 36 #include <pwd.h> 37 #include <syslog.h> 38 #include <libintl.h> 39 #include <krb5.h> 40 #include <netdb.h> 41 #include <unistd.h> 42 #include <sys/stat.h> 43 #include <fcntl.h> 44 #include <errno.h> 45 #include <com_err.h> 46 47 #include "utils.h" 48 #include "krb5_repository.h" 49 50 #define PAMTXD "SUNW_OST_SYSOSPAM" 51 #define KRB5_DEFAULT_LIFE 60*60*10 /* 10 hours */ 52 53 extern void krb5_cleanup(pam_handle_t *, void *, int); 54 55 static int attempt_refresh_cred(krb5_module_data_t *, char *, int); 56 static int attempt_delete_initcred(krb5_module_data_t *); 57 static krb5_error_code krb5_renew_tgt(krb5_module_data_t *, krb5_principal, 58 krb5_principal, int); 59 static krb5_boolean creds_match(krb5_context, const krb5_creds *, 60 const krb5_creds *); 61 62 extern uint_t kwarn_add_warning(char *, int); 63 extern uint_t kwarn_del_warning(char *); 64 65 /* 66 * pam_sm_setcred 67 */ 68 int 69 pam_sm_setcred( 70 pam_handle_t *pamh, 71 int flags, 72 int argc, 73 const char **argv) 74 { 75 int i; 76 int err = 0; 77 int debug = 0; 78 krb5_module_data_t *kmd = NULL; 79 char *user; 80 int result; 81 krb5_repository_data_t *krb5_data = NULL; 82 pam_repository_t *rep_data = NULL; 83 84 for (i = 0; i < argc; i++) { 85 if (strcasecmp(argv[i], "debug") == 0) 86 debug = 1; 87 else if (strcasecmp(argv[i], "nowarn") == 0) 88 flags = flags | PAM_SILENT; 89 } 90 91 if (debug) 92 syslog(LOG_DEBUG, 93 "PAM-KRB5 (setcred): start: nowarn = %d, flags = 0x%x", 94 flags & PAM_SILENT ? 1 : 0, flags); 95 96 /* make sure flags are valid */ 97 if (flags && 98 !(flags & PAM_ESTABLISH_CRED) && 99 !(flags & PAM_REINITIALIZE_CRED) && 100 !(flags & PAM_REFRESH_CRED) && 101 !(flags & PAM_DELETE_CRED) && 102 !(flags & PAM_SILENT)) { 103 syslog(LOG_ERR, 104 dgettext(TEXT_DOMAIN, 105 "PAM-KRB5 (setcred): illegal flag %d"), flags); 106 err = PAM_SYSTEM_ERR; 107 goto out; 108 } 109 110 err = pam_get_item(pamh, PAM_USER, (void**) &user); 111 if (err != PAM_SUCCESS) 112 return (err); 113 114 if (user == NULL || !user[0]) 115 return (PAM_AUTH_ERR); 116 117 if (pam_get_data(pamh, KRB5_DATA, (const void**)&kmd) != PAM_SUCCESS) { 118 if (debug) { 119 syslog(LOG_DEBUG, 120 "PAM-KRB5 (setcred): kmd get failed, kmd=0x%p", 121 kmd); 122 } 123 124 /* 125 * User doesn't need to authenticate for PAM_REFRESH_CRED 126 * or for PAM_DELETE_CRED 127 */ 128 if (flags & (PAM_REFRESH_CRED|PAM_DELETE_CRED)) { 129 syslog(LOG_DEBUG, 130 "PAM-KRB5 (setcred): inst kmd structure"); 131 132 kmd = calloc(1, sizeof (krb5_module_data_t)); 133 134 if (kmd == NULL) { 135 result = PAM_BUF_ERR; 136 return (result); 137 } 138 139 if ((err = pam_set_data(pamh, KRB5_DATA, 140 kmd, &krb5_cleanup)) != PAM_SUCCESS) { 141 free(kmd); 142 return (PAM_SYSTEM_ERR); 143 } 144 } else { 145 err = PAM_CRED_UNAVAIL; 146 goto out; 147 } 148 149 } else { /* pam_get_data success */ 150 if (kmd == NULL) { 151 if (debug) { 152 syslog(LOG_DEBUG, 153 "PAM-KRB5 (setcred): kmd structure" 154 " gotten but is NULL for user %s", user); 155 } 156 err = PAM_CRED_UNAVAIL; 157 goto out; 158 } 159 160 if (debug) 161 syslog(LOG_DEBUG, 162 "PAM-KRB5 (setcred): kmd auth_status: %s", 163 pam_strerror(pamh, kmd->auth_status)); 164 165 /* 166 * pam_auth has set status to ignore, so we also return ignore 167 */ 168 if (kmd->auth_status == PAM_IGNORE) { 169 err = PAM_IGNORE; 170 goto out; 171 } 172 } 173 174 kmd->debug = debug; 175 176 177 /* 178 * User must have passed pam_authenticate() 179 * in order to use PAM_ESTABLISH_CRED or PAM_REINITIALIZE_CRED 180 */ 181 if ((flags & (PAM_ESTABLISH_CRED|PAM_REINITIALIZE_CRED)) && 182 (kmd->auth_status != PAM_SUCCESS)) { 183 if (kmd->debug) 184 syslog(LOG_DEBUG, 185 "PAM-KRB5 (setcred): unable to " 186 "setcreds, not authenticated!"); 187 return (PAM_CRED_UNAVAIL); 188 } 189 190 /* 191 * We cannot assume that kmd->kcontext being non-NULL 192 * means it is valid. Other pam_krb5 mods may have 193 * freed it but not reset it to NULL. 194 * Log a message when debugging to track down memory 195 * leaks. 196 */ 197 if (kmd->kcontext != NULL && kmd->debug) 198 syslog(LOG_DEBUG, 199 "PAM-KRB5 (setcred): kcontext != NULL, " 200 "possible memory leak."); 201 202 /* 203 * If auth was short-circuited we will not have anything to 204 * renew, so just return here. 205 */ 206 err = pam_get_item(pamh, PAM_REPOSITORY, (void **)&rep_data); 207 if (rep_data != NULL) { 208 if (strcmp(rep_data->type, KRB5_REPOSITORY_NAME) != 0) { 209 if (debug) 210 syslog(LOG_DEBUG, "PAM-KRB5 (setcred): wrong" 211 "repository found (%s), returning " 212 "PAM_IGNORE", rep_data->type); 213 return (PAM_IGNORE); 214 } 215 if (rep_data->scope_len == sizeof (krb5_repository_data_t)) { 216 krb5_data = (krb5_repository_data_t *)rep_data->scope; 217 218 if (krb5_data->flags == 219 SUNW_PAM_KRB5_ALREADY_AUTHENTICATED && 220 krb5_data->principal != NULL && 221 strlen(krb5_data->principal)) { 222 if (debug) 223 syslog(LOG_DEBUG, 224 "PAM-KRB5 (setcred): " 225 "Principal %s already " 226 "authenticated, " 227 "cannot setcred", 228 krb5_data->principal); 229 return (PAM_SUCCESS); 230 } 231 } 232 } 233 234 if (flags & PAM_REINITIALIZE_CRED) 235 err = attempt_refresh_cred(kmd, user, PAM_REINITIALIZE_CRED); 236 else if (flags & PAM_REFRESH_CRED) 237 err = attempt_refresh_cred(kmd, user, PAM_REFRESH_CRED); 238 else if (flags & PAM_DELETE_CRED) 239 err = attempt_delete_initcred(kmd); 240 else { 241 /* 242 * Default case: PAM_ESTABLISH_CRED 243 */ 244 err = attempt_refresh_cred(kmd, user, PAM_ESTABLISH_CRED); 245 } 246 247 if (err) 248 syslog(LOG_ERR, 249 "PAM-KRB5 (setcred): pam_setcred failed " 250 "for %s (%s).", user, pam_strerror(pamh, err)); 251 252 out: 253 if (kmd && kmd->kcontext) { 254 /* 255 * free 'kcontext' field if it is allocated, 256 * kcontext is local to the operation being performed 257 * not considered global to the entire pam module. 258 */ 259 krb5_free_context(kmd->kcontext); 260 kmd->kcontext = NULL; 261 } 262 263 /* 264 * 'kmd' is not freed here, it is handled in krb5_cleanup 265 */ 266 267 268 if (debug) 269 syslog(LOG_DEBUG, 270 "PAM-KRB5 (setcred): end: %s", 271 pam_strerror(pamh, err)); 272 return (err); 273 } 274 275 static int 276 attempt_refresh_cred( 277 krb5_module_data_t *kmd, 278 char *user, 279 int flag) 280 { 281 krb5_principal me; 282 krb5_principal server; 283 krb5_error_code code; 284 char kuser[2*MAXHOSTNAMELEN]; 285 krb5_data tgtname = { 286 0, 287 KRB5_TGS_NAME_SIZE, 288 KRB5_TGS_NAME 289 }; 290 291 /* User must have passed pam_authenticate() */ 292 if (kmd->auth_status != PAM_SUCCESS) { 293 if (kmd->debug) 294 syslog(LOG_DEBUG, 295 "PAM-KRB5 (setcred): unable to " 296 "setcreds, not authenticated!"); 297 return (PAM_CRED_UNAVAIL); 298 } 299 300 /* Create a new context here. */ 301 if (krb5_init_context(&kmd->kcontext) != 0) { 302 if (kmd->debug) 303 syslog(LOG_DEBUG, 304 "PAM-KRB5 (setcred): unable to " 305 "initialize krb5 context"); 306 return (PAM_SYSTEM_ERR); 307 } 308 309 if (krb5_cc_default(kmd->kcontext, &kmd->ccache) != 0) { 310 return (PAM_CRED_ERR); 311 } 312 313 if ((code = get_kmd_kuser(kmd->kcontext, (const char *)user, kuser, 314 2*MAXHOSTNAMELEN)) != 0) { 315 return (code); 316 } 317 318 if (krb5_parse_name(kmd->kcontext, kuser, &me) != 0) { 319 return (PAM_CRED_ERR); 320 } 321 322 if (code = krb5_build_principal_ext(kmd->kcontext, &server, 323 krb5_princ_realm(kmd->kcontext, me)->length, 324 krb5_princ_realm(kmd->kcontext, me)->data, 325 tgtname.length, tgtname.data, 326 krb5_princ_realm(kmd->kcontext, me)->length, 327 krb5_princ_realm(kmd->kcontext, me)->data, 0)) { 328 code = PAM_CRED_ERR; 329 goto out; 330 } 331 332 code = krb5_renew_tgt(kmd, me, server, flag); 333 334 out: 335 if (server) 336 krb5_free_principal(kmd->kcontext, server); 337 if (me) 338 krb5_free_principal(kmd->kcontext, me); 339 340 if (code) { 341 return (PAM_CRED_ERR); 342 } else { 343 return (PAM_SUCCESS); 344 } 345 } 346 347 /* 348 * This code will update the credential matching "server" in the user's 349 * credential cache. The flag may be set to one of: 350 * PAM_ESTABLISH_CRED - Create a new cred cache if one doesnt exist, 351 * else refresh the existing one. 352 * PAM_REINITIALIZE_CRED - destroy current cred cache and create a new one 353 * PAM_REFRESH_CRED - update the existing cred cache (default action) 354 */ 355 static krb5_error_code 356 krb5_renew_tgt( 357 krb5_module_data_t *kmd, 358 krb5_principal me, 359 krb5_principal server, 360 int flag) 361 { 362 krb5_error_code retval; 363 krb5_creds creds; 364 krb5_creds *credsp = &creds; 365 char *client_name = NULL; 366 typedef struct _cred_node { 367 krb5_creds *creds; 368 struct _cred_node *next; 369 } cred_node; 370 cred_node *cred_list_head = NULL; 371 cred_node *fetched = NULL; 372 373 #define my_creds (kmd->initcreds) 374 375 if ((flag != PAM_REFRESH_CRED) && 376 (flag != PAM_REINITIALIZE_CRED) && 377 (flag != PAM_ESTABLISH_CRED)) 378 return (PAM_SYSTEM_ERR); 379 380 /* this is needed only for the ktkt_warnd */ 381 if (krb5_unparse_name(kmd->kcontext, me, &client_name) != 0) { 382 krb5_free_principal(kmd->kcontext, me); 383 return (PAM_CRED_ERR); 384 } 385 386 (void) memset((char *)credsp, 0, sizeof (krb5_creds)); 387 if ((retval = krb5_copy_principal(kmd->kcontext, 388 server, &credsp->server))) { 389 if (kmd->debug) 390 syslog(LOG_DEBUG, 391 "PAM-KRB5 (setcred): krb5_copy_principal " 392 "failed: %s", 393 error_message((errcode_t)retval)); 394 goto cleanup_creds; 395 } 396 397 /* obtain ticket & session key */ 398 retval = krb5_cc_get_principal(kmd->kcontext, 399 kmd->ccache, &credsp->client); 400 if (retval && (kmd->debug)) 401 syslog(LOG_DEBUG, 402 dgettext(TEXT_DOMAIN, 403 "PAM-KRB5 (setcred): User not in cred " 404 "cache (%s)"), error_message((errcode_t)retval)); 405 406 if ((retval == KRB5_FCC_NOFILE) && 407 (flag & (PAM_ESTABLISH_CRED|PAM_REINITIALIZE_CRED))) { 408 /* 409 * Create a fresh ccache, and store the credentials 410 * we got from pam_authenticate() 411 */ 412 if ((retval = krb5_cc_initialize(kmd->kcontext, 413 kmd->ccache, me)) != 0) { 414 syslog(LOG_DEBUG, 415 "PAM-KRB5 (setcred): krb5_cc_initialize " 416 "failed: %s", 417 error_message((errcode_t)retval)); 418 goto cleanup_creds; 419 } else if ((retval = krb5_cc_store_cred(kmd->kcontext, 420 kmd->ccache, &my_creds)) != 0) { 421 syslog(LOG_DEBUG, 422 "PAM-KRB5 (setcred): krb5_cc_store_cred " 423 "failed: %s", 424 error_message((errcode_t)retval)); 425 goto cleanup_creds; 426 } 427 } else if (retval) { 428 /* 429 * We failed to get the user's credentials. 430 * This might be due to permission error on the cache, 431 * or maybe we are looking in the wrong cache file! 432 */ 433 syslog(LOG_ERR, 434 dgettext(TEXT_DOMAIN, 435 "PAM-KRB5 (setcred): Cannot find creds" 436 " for %s (%s)"), 437 client_name ? client_name : "(unknown)", 438 error_message((errcode_t)retval)); 439 440 } else if (flag & PAM_REINITIALIZE_CRED) { 441 /* 442 * This destroys the credential cache, and stores a new 443 * krbtgt with updated startime, endtime and renewable 444 * lifetime. 445 */ 446 creds.times.starttime = my_creds.times.starttime; 447 creds.times.endtime = my_creds.times.endtime; 448 creds.times.renew_till = my_creds.times.renew_till; 449 if ((retval = krb5_get_credentials_renew(kmd->kcontext, 0, 450 kmd->ccache, &creds, &credsp))) { 451 if (kmd->debug) 452 syslog(LOG_DEBUG, 453 "PAM-KRB5 (setcred): krb5_get_credentials", 454 "_renew(reinitialize) failed: %s", 455 error_message((errcode_t)retval)); 456 /* perhaps the tgt lifetime has expired */ 457 if ((retval = krb5_cc_initialize(kmd->kcontext, 458 kmd->ccache, me)) != 0) { 459 goto cleanup_creds; 460 } else if ((retval = krb5_cc_store_cred(kmd->kcontext, 461 kmd->ccache, &my_creds)) != 0) { 462 goto cleanup_creds; 463 } 464 } 465 } else { 466 /* 467 * Creds already exist, update them if possible. 468 * We got here either with the ESTABLISH or REFRESH flag. 469 * 470 * The credential cache does exist, and we are going to 471 * read in each cred, looking for our own. When we find 472 * a matching credential, we will update it, and store it. 473 * Any nonmatching credentials are stored as is. 474 * 475 * Rules: 476 * TGT must exist in cache to get to this point. 477 * if flag == ESTABLISH 478 * refresh it if possible, else overwrite 479 * with new TGT, other tickets in cache remain 480 * unchanged. 481 * else if flag == REFRESH 482 * refresh it if possible, else return error. 483 * - Will not work if "R" flag is not set in 484 * original cred, we dont want to 2nd guess the 485 * intention of the person who created the 486 * existing TGT. 487 * 488 */ 489 krb5_cc_cursor cursor; 490 krb5_creds nextcred; 491 boolean_t found = 0; 492 493 if ((retval = krb5_cc_start_seq_get(kmd->kcontext, 494 kmd->ccache, &cursor)) != 0) 495 goto cleanup_creds; 496 497 while ((krb5_cc_next_cred(kmd->kcontext, kmd->ccache, 498 &cursor, &nextcred) == 0)) { 499 /* if two creds match, we just update the first */ 500 if ((!found) && (creds_match(kmd->kcontext, 501 &nextcred, &creds))) { 502 /* 503 * Mark it as found, don't store it 504 * in the list or else it will be 505 * stored twice later. 506 */ 507 found = 1; 508 } else { 509 /* 510 * Add a new node to the list 511 * of creds that must be replaced 512 * in the cache later. 513 */ 514 cred_node *newnode = (cred_node *)malloc( 515 sizeof (cred_node)); 516 if (newnode == NULL) { 517 retval = ENOMEM; 518 goto cleanup_creds; 519 } 520 newnode->creds = NULL; 521 newnode->next = NULL; 522 523 if (cred_list_head == NULL) { 524 cred_list_head = newnode; 525 fetched = cred_list_head; 526 } else { 527 fetched->next = newnode; 528 fetched = fetched->next; 529 } 530 retval = krb5_copy_creds(kmd->kcontext, 531 &nextcred, &fetched->creds); 532 if (retval) 533 goto cleanup_creds; 534 } 535 } 536 537 if ((retval = krb5_cc_end_seq_get(kmd->kcontext, 538 kmd->ccache, &cursor)) != 0) 539 goto cleanup_creds; 540 541 /* 542 * If we found a matching cred, renew it. 543 * This destroys the credential cache, if and only 544 * if it passes. 545 */ 546 if (found && 547 (retval = krb5_get_credentials_renew(kmd->kcontext, 548 0, kmd->ccache, &creds, &credsp))) { 549 if (kmd->debug) 550 syslog(LOG_DEBUG, 551 "PAM-KRB5 (setcred): krb5_get_credentials" 552 "_renew(update) failed: %s", 553 error_message((errcode_t)retval)); 554 /* 555 * If we only wanted to refresh the creds but failed 556 * due to expiration, lack of "R" flag, or other 557 * problems, return an error. If we were trying to 558 * establish new creds, add them to the cache. 559 */ 560 if ((retval = krb5_cc_initialize(kmd->kcontext, 561 kmd->ccache, me)) != 0) { 562 goto cleanup_creds; 563 } else if ((retval = krb5_cc_store_cred(kmd->kcontext, 564 kmd->ccache, &my_creds)) != 0) { 565 goto cleanup_creds; 566 } 567 } 568 /* 569 * If no matching creds were found, we must 570 * initialize the cache before we can store stuff 571 * in it. 572 */ 573 if (!found) { 574 if ((retval = krb5_cc_initialize(kmd->kcontext, 575 kmd->ccache, me)) != 0) { 576 goto cleanup_creds; 577 } 578 } 579 580 /* now store all the other tickets */ 581 fetched = cred_list_head; 582 while (fetched != NULL) { 583 retval = krb5_cc_store_cred(kmd->kcontext, 584 kmd->ccache, fetched->creds); 585 fetched = fetched->next; 586 if (retval) { 587 if (kmd->debug) 588 syslog(LOG_DEBUG, 589 "PAM-KRB5(setcred): krb5_cc_store_cred() " 590 "failed: %s", 591 error_message((errcode_t)retval)); 592 goto cleanup_creds; 593 } 594 } 595 } 596 597 cleanup_creds: 598 /* Cleanup the list of creds read from the cache if necessary */ 599 fetched = cred_list_head; 600 while (fetched != NULL) { 601 cred_node *old = fetched; 602 /* Free the contents and the cred structure itself */ 603 krb5_free_creds(kmd->kcontext, fetched->creds); 604 fetched = fetched->next; 605 free(old); 606 } 607 608 if ((retval == 0) && (client_name != NULL)) { 609 /* 610 * Credential update was successful! 611 * 612 * We now chown the ccache to the appropriate uid/gid 613 * combination, if its a FILE based ccache. 614 */ 615 if (strstr(kmd->env, "FILE:")) { 616 uid_t uuid; 617 gid_t ugid; 618 char *username = NULL, *tmpname = NULL; 619 char *filepath = NULL; 620 621 username = strdup(client_name); 622 if ((tmpname = strchr(username, '@'))) 623 *tmpname = '\0'; 624 625 if (get_pw_uid(username, &uuid) == 0 || 626 get_pw_gid(username, &ugid) == 0) { 627 syslog(LOG_ERR, "PAM-KRB5 (setcred): Unable to " 628 "find matching uid/gid pair for user `%s'", 629 username); 630 return (PAM_SYSTEM_ERR); 631 } 632 if (!(filepath = strchr(kmd->env, ':')) || 633 !(filepath+1)) { 634 syslog(LOG_ERR, 635 "PAM-KRB5 (setcred): Invalid pathname " 636 "for credential cache of user `%s'", 637 username); 638 return (PAM_SYSTEM_ERR); 639 } 640 if (chown(filepath+1, uuid, ugid)) { 641 if (kmd->debug) 642 syslog(LOG_DEBUG, 643 "PAM-KRB5 (setcred): chown to user " 644 "`%s' failed for FILE=%s", 645 username, filepath); 646 } 647 648 free(username); 649 } 650 651 if (creds.times.endtime != 0) { 652 kwarn_del_warning(client_name); 653 if (kwarn_add_warning(client_name, 654 creds.times.endtime) != 0) { 655 syslog(LOG_NOTICE, dgettext(TEXT_DOMAIN, 656 "PAM-KRB5 (auth): kwarn_add_warning" 657 " failed: ktkt_warnd(1M) down?")); 658 } 659 } 660 } 661 if (client_name != NULL) 662 free(client_name); 663 664 krb5_free_cred_contents(kmd->kcontext, &creds); 665 666 return (retval); 667 } 668 669 static krb5_boolean 670 creds_match(krb5_context ctx, const krb5_creds *mcreds, 671 const krb5_creds *creds) 672 { 673 char *s1, *s2, *c1, *c2; 674 krb5_unparse_name(ctx, mcreds->client, &c1); 675 krb5_unparse_name(ctx, mcreds->server, &s1); 676 krb5_unparse_name(ctx, creds->client, &c2); 677 krb5_unparse_name(ctx, creds->server, &s2); 678 679 return (krb5_principal_compare(ctx, mcreds->client, creds->client) && 680 krb5_principal_compare(ctx, mcreds->server, creds->server)); 681 } 682 683 /* 684 * Delete the user's credentials for this session 685 */ 686 static int 687 attempt_delete_initcred(krb5_module_data_t *kmd) 688 { 689 if (kmd == NULL) 690 return (0); 691 692 if (kmd->debug) { 693 syslog(LOG_DEBUG, 694 "PAM-KRB5 (setcred): deleting user's " 695 "credentials (initcreds)"); 696 } 697 krb5_free_cred_contents(kmd->kcontext, &kmd->initcreds); 698 (void) memset((char *)&kmd->initcreds, 0, sizeof (krb5_creds)); 699 kmd->auth_status = PAM_AUTHINFO_UNAVAIL; 700 return (0); 701 } 702