xref: /illumos-gate/usr/src/cmd/krb5/kadmin/dbutil/kdb5_util.c (revision 1b8adde7ba7d5e04395c141c5400dc2cffd7d809)
1 /*
2  * Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
3  * Use is subject to license terms.
4  */
5 
6 
7 /*
8  * WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
9  *
10  *	Openvision retains the copyright to derivative works of
11  *	this source code.  Do *NOT* create a derivative of this
12  *	source code before consulting with your legal department.
13  *	Do *NOT* integrate *ANY* of this source code into another
14  *	product before consulting with your legal department.
15  *
16  *	For further information, read the top-level Openvision
17  *	copyright which is contained in the top-level MIT Kerberos
18  *	copyright.
19  *
20  * WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
21  *
22  */
23 
24 
25 /*
26  * admin/edit/kdb5_edit.c
27  *
28  * (C) Copyright 1990,1991, 1996 by the Massachusetts Institute of Technology.
29  * All Rights Reserved.
30  *
31  * Export of this software from the United States of America may
32  *   require a specific license from the United States Government.
33  *   It is the responsibility of any person or organization contemplating
34  *   export to obtain such a license before exporting.
35  *
36  * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
37  * distribute this software and its documentation for any purpose and
38  * without fee is hereby granted, provided that the above copyright
39  * notice appear in all copies and that both that copyright notice and
40  * this permission notice appear in supporting documentation, and that
41  * the name of M.I.T. not be used in advertising or publicity pertaining
42  * to distribution of the software without specific, written prior
43  * permission.  Furthermore if you modify this software you must label
44  * your software as modified software and not distribute it in such a
45  * fashion that it might be confused with the original M.I.T. software.
46  * M.I.T. makes no representations about the suitability of
47  * this software for any purpose.  It is provided "as is" without express
48  * or implied warranty.
49  *
50  *
51  * Edit a KDC database.
52  */
53 
54 /*
55  * Copyright (C) 1998 by the FundsXpress, INC.
56  *
57  * All rights reserved.
58  *
59  * Export of this software from the United States of America may require
60  * a specific license from the United States Government.  It is the
61  * responsibility of any person or organization contemplating export to
62  * obtain such a license before exporting.
63  *
64  * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
65  * distribute this software and its documentation for any purpose and
66  * without fee is hereby granted, provided that the above copyright
67  * notice appear in all copies and that both that copyright notice and
68  * this permission notice appear in supporting documentation, and that
69  * the name of FundsXpress. not be used in advertising or publicity pertaining
70  * to distribution of the software without specific, written prior
71  * permission.  FundsXpress makes no representations about the suitability of
72  * this software for any purpose.  It is provided "as is" without express
73  * or implied warranty.
74  *
75  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
76  * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
77  * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
78  */
79 
80 /*
81  *  Yes, I know this is a hack, but we need admin.h without including the
82  *  rpc.h header. Additionally, our rpc.h header brings in
83  *  a des.h header which causes other problems.
84  */
85 #define	_RPC_RPC_H
86 
87 #include <stdio.h>
88 #include <k5-int.h>
89 #include <kadm5/admin.h>
90 #include <rpc/types.h>
91 #include <krb5/adm_proto.h>
92 #include <rpc/xdr.h>
93 #include <time.h>
94 #include <libintl.h>
95 #include <locale.h>
96 #include "kdb5_util.h"
97 
98 char	*Err_no_master_msg = "Master key not entered!\n";
99 char	*Err_no_database = "Database not currently opened!\n";
100 
101 /*
102  * XXX Ick, ick, ick.  These global variables shouldn't be global....
103  */
104 char *mkey_password = 0;
105 
106 /*
107  * I can't figure out any way for this not to be global, given how ss
108  * works.
109  */
110 
111 int exit_status = 0;
112 krb5_context util_context;
113 kadm5_config_params global_params;
114 
115 void usage()
116 {
117      fprintf(stderr, "%s: "
118 	   "kdb5_util [-x db_args]* [-r realm] [-d dbname] [-k mkeytype] [-M mkeyname]\n"
119 	     "\t        [-sf stashfilename] [-P password] [-m] cmd [cmd_options]\n"
120 	     "\tcreate	[-s]\n"
121 	     "\tdestroy	[-f]\n"
122 	     "\tstash	[-f keyfile]\n"
123 	     "\tdump	[-old] [-ov] [-b6] [-verbose] [filename	[princs...]]\n"
124 	     "\t	[-mkey_convert] [-new_mkey_file mkey_file]\n"
125 	     "\t	[-rev] [-recurse] [filename [princs...]]\n"
126 	     "\tload	[-old] [-ov] [-b6] [-verbose] [-update] filename\n"
127 	     "\tark	[-e etype_list] principal\n"
128 	     "\nwhere,\n\t[-x db_args]* - any number of database specific arguments.\n"
129 	     "\t\t\tLook at each database documentation for supported arguments\n",
130 		gettext("Usage"));
131      exit(1);
132 }
133 
134 krb5_keyblock master_key;
135 extern krb5_principal master_princ;
136 krb5_db_entry master_entry;
137 int	valid_master_key = 0;
138 
139 char *progname;
140 krb5_boolean manual_mkey = FALSE;
141 krb5_boolean dbactive = FALSE;
142 
143 static int open_db_and_mkey(void);
144 
145 static void add_random_key(int, char **);
146 
147 typedef void (*cmd_func)(int, char **);
148 
149 struct _cmd_table {
150      char *name;
151      cmd_func func;
152      int opendb;
153 } cmd_table[] = {
154      {"create", kdb5_create, 0},
155      {"destroy", kdb5_destroy, 1},
156      {"stash", kdb5_stash, 1},
157      {"dump", dump_db, 1},
158      {"load", load_db, 0},
159      {"ark", add_random_key, 1},
160      {NULL, NULL, 0},
161 };
162 
163 static struct _cmd_table *cmd_lookup(name)
164    char *name;
165 {
166      struct _cmd_table *cmd = cmd_table;
167      while (cmd->name) {
168 	  if (strcmp(cmd->name, name) == 0)
169 	       return cmd;
170 	  else
171 	       cmd++;
172      }
173 
174      return NULL;
175 }
176 
177 #define ARG_VAL (--argc > 0 ? (koptarg = *(++argv)) : (char *)(usage(), NULL))
178 
179 char **db5util_db_args = NULL;
180 int    db5util_db_args_size = 0;
181 
182 static void extended_com_err_fn (const char *myprog, errcode_t code,
183 				 const char *fmt, va_list args)
184 {
185     const char *emsg;
186     if (code) {
187 	emsg = krb5_get_error_message (util_context, code);
188 	fprintf (stderr, "%s: %s ", myprog, emsg);
189 	krb5_free_error_message (util_context, emsg);
190     } else {
191 	fprintf (stderr, "%s: ", myprog);
192     }
193     vfprintf (stderr, fmt, args);
194     fprintf (stderr, "\n");
195 }
196 
197 int add_db_arg(char *arg)
198 {
199     char **temp;
200     db5util_db_args_size++;
201     temp = realloc(db5util_db_args,
202 		   sizeof(char *) * (db5util_db_args_size + 1));
203     if (temp == NULL)
204 	return 0;
205     db5util_db_args = temp;
206     db5util_db_args[db5util_db_args_size-1] = arg;
207     db5util_db_args[db5util_db_args_size]   = NULL;
208     return 1;
209 }
210 
211 int main(argc, argv)
212     int argc;
213     char *argv[];
214 {
215     struct _cmd_table *cmd = NULL;
216     char *koptarg, **cmd_argv;
217     char *db_name_tmp = NULL;
218     int cmd_argc;
219     krb5_error_code retval;
220 
221 	(void) setlocale(LC_ALL, "");
222     set_com_err_hook(extended_com_err_fn);
223 
224 #if !defined(TEXT_DOMAIN)  /* Should be defined by cc -D */
225 #define	TEXT_DOMAIN	"SYS_TEST"	/* Use this only if it weren't */
226 #endif
227 
228 	(void) textdomain(TEXT_DOMAIN);
229 
230 	Err_no_master_msg = gettext("Master key not entered!\n");
231 	Err_no_database = gettext("Database not currently opened!\n");
232 
233 	/*
234 	 * Solaris Kerberos:
235 	 * Ensure that "progname" is set before calling com_err.
236 	 */
237 	progname = (strrchr(argv[0], '/') ?
238 		    strrchr(argv[0], '/') + 1 : argv[0]);
239 
240     retval = kadm5_init_krb5_context(&util_context);
241     if (retval) {
242 	    com_err (progname, retval,
243 		gettext("while initializing Kerberos code"));
244 	    exit(1);
245     }
246 
247     cmd_argv = (char **) malloc(sizeof(char *)*argc);
248     if (cmd_argv == NULL) {
249 		com_err(progname, ENOMEM,
250 		    gettext("while creating sub-command arguments"));
251 	 exit(1);
252     }
253     memset(cmd_argv, 0, sizeof(char *)*argc);
254     cmd_argc = 1;
255 
256     argv++; argc--;
257     while (*argv) {
258        if (strcmp(*argv, "-P") == 0 && ARG_VAL) {
259 	    mkey_password = koptarg;
260 	    manual_mkey = TRUE;
261        } else if (strcmp(*argv, "-d") == 0 && ARG_VAL) {
262 	    global_params.dbname = koptarg;
263 	    global_params.mask |= KADM5_CONFIG_DBNAME;
264 
265 	    db_name_tmp = malloc( strlen(global_params.dbname) + sizeof("dbname="));
266 	    if( db_name_tmp == NULL )
267 	    {
268 		com_err(progname, ENOMEM, "while parsing command arguments");
269 		exit(1);
270 	    }
271 
272 	    strcpy( db_name_tmp, "dbname=");
273 	    strcat( db_name_tmp, global_params.dbname );
274 
275 	    if (!add_db_arg(db_name_tmp)) {
276 		com_err(progname, ENOMEM, "while parsing command arguments\n");
277 		exit(1);
278 	    }
279 
280        } else if (strcmp(*argv, "-x") == 0 && ARG_VAL) {
281 	   if (!add_db_arg(koptarg)) {
282 		com_err(progname, ENOMEM, "while parsing command arguments\n");
283 		exit(1);
284 	   }
285 
286        } else if (strcmp(*argv, "-r") == 0 && ARG_VAL) {
287 	    global_params.realm = koptarg;
288 	    global_params.mask |= KADM5_CONFIG_REALM;
289 	    /* not sure this is really necessary */
290 	    if ((retval = krb5_set_default_realm(util_context,
291 						 global_params.realm))) {
292 				com_err(progname, retval,
293 					gettext("while setting default "
294 						"realm name"));
295 		 exit(1);
296 	    }
297        } else if (strcmp(*argv, "-k") == 0 && ARG_VAL) {
298 	    if (krb5_string_to_enctype(koptarg, &global_params.enctype))
299 		 com_err(argv[0], 0, gettext("%s is an invalid enctype"), koptarg);
300 	    else
301 		 global_params.mask |= KADM5_CONFIG_ENCTYPE;
302        } else if (strcmp(*argv, "-M") == 0 && ARG_VAL) {
303 	    global_params.mkey_name = koptarg;
304 	    global_params.mask |= KADM5_CONFIG_MKEY_NAME;
305        } else if (((strcmp(*argv, "-sf") == 0)
306 		/* SUNWresync121 - carry the old -f forward too */
307 		|| (strcmp(*argv, "-f") == 0)) && ARG_VAL) {
308 	    global_params.stash_file = koptarg;
309 	    global_params.mask |= KADM5_CONFIG_STASH_FILE;
310        } else if (strcmp(*argv, "-m") == 0) {
311 	    manual_mkey = TRUE;
312 	    global_params.mkey_from_kbd = 1;
313 	    global_params.mask |= KADM5_CONFIG_MKEY_FROM_KBD;
314        } else if (cmd_lookup(*argv) != NULL) {
315 	    if (cmd_argv[0] == NULL)
316 		 cmd_argv[0] = *argv;
317 	    else
318 		 usage();
319        } else {
320 	    cmd_argv[cmd_argc++] = *argv;
321        }
322        argv++; argc--;
323     }
324 
325     if (cmd_argv[0] == NULL)
326 	 usage();
327 
328     if( !util_context->default_realm )
329     {
330 	char *temp = NULL;
331 	retval = krb5_get_default_realm(util_context, &temp);
332 	if( retval )
333 	{
334 	    com_err (progname, retval, "while getting default realm");
335 	    exit(1);
336 	}
337 	util_context->default_realm = temp;
338     }
339 
340     retval = kadm5_get_config_params(util_context, 1,
341 				     &global_params, &global_params);
342     if (retval) {
343 	 com_err(argv[0], retval,
344 		    gettext("while retreiving configuration parameters"));
345 	 exit(1);
346     }
347 
348     /*
349      * Dump creates files which should not be world-readable.  It is
350      * easiest to do a single umask call here.
351      */
352     (void) umask(077);
353 
354     (void) memset(&master_key, 0, sizeof (krb5_keyblock));
355 
356     if ((global_params.enctype != ENCTYPE_UNKNOWN) &&
357 	(!krb5_c_valid_enctype(global_params.enctype))) {
358 	com_err(argv[0], KRB5_PROG_KEYTYPE_NOSUPP,
359 	    gettext("while setting up enctype %d"), global_params.enctype);
360     }
361 
362     cmd = cmd_lookup(cmd_argv[0]);
363     if (cmd->opendb && open_db_and_mkey())
364 	 return exit_status;
365 
366 	if (global_params.iprop_enabled == TRUE)
367 		ulog_set_role(util_context, IPROP_MASTER);
368 	else
369 		ulog_set_role(util_context, IPROP_NULL);
370 
371     (*cmd->func)(cmd_argc, cmd_argv);
372 
373     if( db_name_tmp )
374 	free( db_name_tmp );
375 
376     if( db5util_db_args )
377 	free(db5util_db_args);
378 
379     kadm5_free_config_params(util_context, &global_params);
380     krb5_free_context(util_context);
381     return exit_status;
382 }
383 
384 #if 0
385 /*
386  * This function is no longer used in kdb5_util (and it would no
387  * longer work, anyway).
388  */
389 void set_dbname(argc, argv)
390     int argc;
391     char *argv[];
392 {
393     krb5_error_code retval;
394 
395     if (argc < 3) {
396 		com_err(argv[0], 0, gettext("Too few arguments"));
397 		com_err(argv[0], 0, gettext("Usage: %s dbpathname realmname"),
398 			argv[0]);
399 	exit_status++;
400 	return;
401     }
402     if (dbactive) {
403 	if ((retval = krb5_db_fini(util_context)) && retval!= KRB5_KDB_DBNOTINITED) {
404 	    com_err(argv[0], retval, gettext("while closing previous database"));
405 	    exit_status++;
406 	    return;
407 	}
408 	if (valid_master_key) {
409 	    krb5_free_keyblock_contents(util_context, &master_key);
410 	    master_key.contents = NULL;
411 	    valid_master_key = 0;
412 	}
413 	krb5_free_principal(util_context, master_princ);
414 	dbactive = FALSE;
415     }
416 
417     (void) set_dbname_help(argv[0], argv[1]);
418     return;
419 }
420 #endif
421 
422 /*
423  * open_db_and_mkey: Opens the KDC and policy database, and sets the
424  * global master_* variables.  Sets dbactive to TRUE if the databases
425  * are opened, and valid_master_key to 1 if the global master
426  * variables are set properly.  Returns 0 on success, and 1 on
427  * failure, but it is not considered a failure if the master key
428  * cannot be fetched (the master key stash file may not exist when the
429  * program is run).
430  */
431 static int open_db_and_mkey()
432 {
433     krb5_error_code retval;
434     int nentries;
435     krb5_boolean more;
436     krb5_data scratch, pwd, seed;
437 
438     dbactive = FALSE;
439     valid_master_key = 0;
440 
441     if ((retval = krb5_db_open(util_context, db5util_db_args,
442 			       KRB5_KDB_OPEN_RW | KRB5_KDB_SRV_TYPE_ADMIN))) {
443 	com_err(progname, retval, "while initializing database");
444 	exit_status++;
445 	return(1);
446     }
447 
448    /* assemble & parse the master key name */
449 
450     if ((retval = krb5_db_setup_mkey_name(util_context,
451 					  global_params.mkey_name,
452 					  global_params.realm,
453 					  0, &master_princ))) {
454 		com_err(progname, retval,
455 		    gettext("while setting up master key name"));
456 	exit_status++;
457 	return(1);
458     }
459     nentries = 1;
460     if ((retval = krb5_db_get_principal(util_context, master_princ,
461 					&master_entry, &nentries, &more))) {
462 		com_err(progname, retval,
463 		    gettext("while retrieving master entry"));
464 	exit_status++;
465 	(void) krb5_db_fini(util_context);
466 	return(1);
467     } else if (more) {
468 	com_err(progname, KRB5KDC_ERR_PRINCIPAL_NOT_UNIQUE,
469 		    gettext("while retrieving master entry"));
470 	exit_status++;
471 	(void) krb5_db_fini(util_context);
472 	return(1);
473     } else if (!nentries) {
474 		com_err(progname, KRB5_KDB_NOENTRY,
475 		    gettext("while retrieving master entry"));
476 	exit_status++;
477 	(void) krb5_db_fini(util_context);
478 	return(1);
479     }
480 
481     krb5_db_free_principal(util_context, &master_entry, nentries);
482 
483     /* the databases are now open, and the master principal exists */
484     dbactive = TRUE;
485 
486     if (mkey_password) {
487 	pwd.data = mkey_password;
488 	pwd.length = strlen(mkey_password);
489 	retval = krb5_principal2salt(util_context, master_princ, &scratch);
490 	if (retval) {
491 		com_err(progname, retval,
492 		    gettext("while calculated master key salt"));
493 	    return(1);
494 	}
495 
496 	/* If no encryption type is set, use the default */
497 	if (global_params.enctype == ENCTYPE_UNKNOWN) {
498 	    global_params.enctype = DEFAULT_KDC_ENCTYPE;
499 	    if (!krb5_c_valid_enctype(global_params.enctype))
500 		com_err(progname, KRB5_PROG_KEYTYPE_NOSUPP,
501 			gettext("while setting up enctype %d"),
502 			global_params.enctype);
503 	}
504 
505 	retval = krb5_c_string_to_key(util_context, global_params.enctype,
506 				      &pwd, &scratch, &master_key);
507 	if (retval) {
508 	    com_err(progname, retval,
509 		gettext("while transforming master key from password"));
510 	    return(1);
511 	}
512 	free(scratch.data);
513 	mkey_password = 0;
514     } else if ((retval = krb5_db_fetch_mkey(util_context, master_princ,
515 					    global_params.enctype,
516 					    manual_mkey, FALSE,
517 					    global_params.stash_file,
518 					    0, &master_key))) {
519 	com_err(progname, retval,
520 	    gettext("while reading master key"));
521 	com_err(progname, 0,
522 	    gettext("Warning: proceeding without master key"));
523 	/*
524 	 * Solaris Kerberos: We don't want to count as an error if for instance
525 	 * the stash file is not present and we are trying to automate
526 	 * propagation, which really doesn't need a master key to do so.
527 	 */
528 	if (retval != KRB5_KDB_CANTREAD_STORED)
529 		exit_status++;
530 	return(0);
531     }
532     if ((retval = krb5_db_verify_master_key(util_context, master_princ,
533 		&master_key))) {
534 	com_err(progname, retval,
535 		gettext("while verifying master key"));
536 	exit_status++;
537 	krb5_free_keyblock_contents(util_context, &master_key);
538 	return(1);
539     }
540 
541     seed.length = master_key.length;
542     seed.data = (char *)master_key.contents;
543 
544     if ((retval = krb5_c_random_seed(util_context, &seed))) {
545 	com_err(progname, retval,
546 		gettext("while initializing random key generator"));
547 	exit_status++;
548 	krb5_free_keyblock_contents(util_context, &master_key);
549 	return(1);
550     }
551 
552     valid_master_key = 1;
553     dbactive = TRUE;
554     return 0;
555 }
556 
557 #ifdef HAVE_GETCWD
558 #undef getwd
559 #endif
560 
561 int
562 quit()
563 {
564     krb5_error_code retval;
565     static krb5_boolean finished = 0;
566 
567     if (finished)
568 	return 0;
569     retval = krb5_db_fini(util_context);
570     krb5_free_keyblock_contents(util_context, &master_key);
571     finished = TRUE;
572     krb5_free_context(util_context);
573     if (retval && retval != KRB5_KDB_DBNOTINITED) {
574 		com_err(progname, retval, gettext("while closing database"));
575 	exit_status++;
576 	return 1;
577     }
578     return 0;
579 }
580 
581 static void
582 add_random_key(argc, argv)
583     int argc;
584     char **argv;
585 {
586     krb5_error_code ret;
587     krb5_principal princ;
588     krb5_db_entry dbent;
589     int n;
590     krb5_boolean more;
591     krb5_timestamp now;
592 
593     krb5_key_salt_tuple *keysalts = NULL;
594     krb5_int32 num_keysalts = 0;
595 
596     int free_keysalts;
597     char *me = argv[0];
598     char *ks_str = NULL;
599     char *pr_str;
600 
601     if (argc < 2)
602 	usage();
603     for (argv++, argc--; *argv; argv++, argc--) {
604 	if (!strcmp(*argv, "-e")) {
605 	    argv++; argc--;
606 	    ks_str = *argv;
607 	    continue;
608 	} else
609 	    break;
610     }
611     if (argc < 1)
612 	usage();
613     pr_str = *argv;
614     ret = krb5_parse_name(util_context, pr_str, &princ);
615     if (ret) {
616 	com_err(me, ret, gettext("while parsing principal name %s"), pr_str);
617 	exit_status++;
618 	return;
619     }
620     n = 1;
621     ret = krb5_db_get_principal(util_context, princ, &dbent,
622 				&n, &more);
623     if (ret) {
624 	com_err(me, ret, gettext("while fetching principal %s"), pr_str);
625 	exit_status++;
626 	return;
627     }
628     if (n != 1) {
629 	fprintf(stderr, gettext("principal %s not found\n"), pr_str);
630 	exit_status++;
631 	return;
632     }
633     if (more) {
634 	fprintf(stderr, gettext("principal %s not unique\n"), pr_str);
635 	krb5_db_free_principal(util_context, &dbent, 1);
636 	exit_status++;
637 	return;
638     }
639     ret = krb5_string_to_keysalts(ks_str,
640 				  ", \t", ":.-", 0,
641 				  &keysalts,
642 				  &num_keysalts);
643     if (ret) {
644 	com_err(me, ret, gettext("while parsing keysalts %s"), ks_str);
645 	exit_status++;
646 	return;
647     }
648     if (!num_keysalts || keysalts == NULL) {
649 	num_keysalts = global_params.num_keysalts;
650 	keysalts = global_params.keysalts;
651 	free_keysalts = 0;
652     } else
653 	free_keysalts = 1;
654     ret = krb5_dbe_ark(util_context, &master_key,
655 		       keysalts, num_keysalts,
656 		       &dbent);
657     if (free_keysalts)
658 	free(keysalts);
659     if (ret) {
660 	com_err(me, ret, gettext("while randomizing principal %s"), pr_str);
661 	krb5_db_free_principal(util_context, &dbent, 1);
662 	exit_status++;
663 	return;
664     }
665     dbent.attributes &= ~KRB5_KDB_REQUIRES_PWCHANGE;
666     ret = krb5_timeofday(util_context, &now);
667     if (ret) {
668 	com_err(me, ret, gettext("while getting time"));
669 	krb5_db_free_principal(util_context, &dbent, 1);
670 	exit_status++;
671 	return;
672     }
673     ret = krb5_dbe_update_last_pwd_change(util_context, &dbent, now);
674     if (ret) {
675 	com_err(me, ret, gettext("while setting changetime"));
676 	krb5_db_free_principal(util_context, &dbent, 1);
677 	exit_status++;
678 	return;
679     }
680     ret = krb5_db_put_principal(util_context, &dbent, &n);
681     krb5_db_free_principal(util_context, &dbent, 1);
682     if (ret) {
683 	com_err(me, ret, gettext("while saving principal %s"), pr_str);
684 	exit_status++;
685 	return;
686     }
687     printf("%s changed\n", pr_str);
688 }
689