| @@ -1,14 +1,14 @@ | | | @@ -1,14 +1,14 @@ |
1 | /* $NetBSD: rndctl.c,v 1.30 2015/04/13 22:18:50 riastradh Exp $ */ | | 1 | /* $NetBSD: rndctl.c,v 1.31 2019/12/06 14:43:18 riastradh Exp $ */ |
2 | | | 2 | |
3 | /*- | | 3 | /*- |
4 | * Copyright (c) 1997 Michael Graff. | | 4 | * Copyright (c) 1997 Michael Graff. |
5 | * All rights reserved. | | 5 | * All rights reserved. |
6 | * | | 6 | * |
7 | * Redistribution and use in source and binary forms, with or without | | 7 | * Redistribution and use in source and binary forms, with or without |
8 | * modification, are permitted provided that the following conditions | | 8 | * modification, are permitted provided that the following conditions |
9 | * are met: | | 9 | * are met: |
10 | * 1. Redistributions of source code must retain the above copyright | | 10 | * 1. Redistributions of source code must retain the above copyright |
11 | * notice, this list of conditions and the following disclaimer. | | 11 | * notice, this list of conditions and the following disclaimer. |
12 | * 2. Redistributions in binary form must reproduce the above copyright | | 12 | * 2. Redistributions in binary form must reproduce the above copyright |
13 | * notice, this list of conditions and the following disclaimer in the | | 13 | * notice, this list of conditions and the following disclaimer in the |
14 | * documentation and/or other materials provided with the distribution. | | 14 | * documentation and/or other materials provided with the distribution. |
| @@ -23,34 +23,35 @@ | | | @@ -23,34 +23,35 @@ |
23 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, | | 23 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
24 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | | 24 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED | | 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
26 | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | | 26 | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
27 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY | | 27 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY |
28 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | | 28 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF |
29 | * SUCH DAMAGE. | | 29 | * SUCH DAMAGE. |
30 | */ | | 30 | */ |
31 | #include <sys/cdefs.h> | | 31 | #include <sys/cdefs.h> |
32 | #include <sys/types.h> | | 32 | #include <sys/types.h> |
33 | #include <sha1.h> | | 33 | #include <sha1.h> |
34 | | | 34 | |
35 | #ifndef lint | | 35 | #ifndef lint |
36 | __RCSID("$NetBSD: rndctl.c,v 1.30 2015/04/13 22:18:50 riastradh Exp $"); | | 36 | __RCSID("$NetBSD: rndctl.c,v 1.31 2019/12/06 14:43:18 riastradh Exp $"); |
37 | #endif | | 37 | #endif |
38 | | | 38 | |
39 | | | 39 | |
40 | #include <sys/types.h> | | 40 | #include <sys/types.h> |
41 | #include <sys/ioctl.h> | | 41 | #include <sys/ioctl.h> |
42 | #include <sys/param.h> | | 42 | #include <sys/param.h> |
43 | #include <sys/rndio.h> | | 43 | #include <sys/rndio.h> |
| | | 44 | #include <sys/sha3.h> |
44 | | | 45 | |
45 | #include <stdio.h> | | 46 | #include <stdio.h> |
46 | #include <stdlib.h> | | 47 | #include <stdlib.h> |
47 | #include <unistd.h> | | 48 | #include <unistd.h> |
48 | #include <fcntl.h> | | 49 | #include <fcntl.h> |
49 | #include <errno.h> | | 50 | #include <errno.h> |
50 | #include <err.h> | | 51 | #include <err.h> |
51 | #include <paths.h> | | 52 | #include <paths.h> |
52 | #include <string.h> | | 53 | #include <string.h> |
53 | | | 54 | |
54 | typedef struct { | | 55 | typedef struct { |
55 | const char *a_name; | | 56 | const char *a_name; |
56 | u_int32_t a_type; | | 57 | u_int32_t a_type; |
| @@ -117,131 +118,247 @@ find_name(u_int32_t type) | | | @@ -117,131 +118,247 @@ find_name(u_int32_t type) |
117 | a = source_types; | | 118 | a = source_types; |
118 | | | 119 | |
119 | while (a->a_name != NULL) { | | 120 | while (a->a_name != NULL) { |
120 | if (type == a->a_type) | | 121 | if (type == a->a_type) |
121 | return (a->a_name); | | 122 | return (a->a_name); |
122 | a++; | | 123 | a++; |
123 | } | | 124 | } |
124 | | | 125 | |
125 | warnx("device type %u unknown", type); | | 126 | warnx("device type %u unknown", type); |
126 | return ("???"); | | 127 | return ("???"); |
127 | } | | 128 | } |
128 | | | 129 | |
129 | static void | | 130 | static void |
130 | do_save(const char *const filename) | | 131 | do_save(const char *filename, const void *extra, size_t nextra, |
| | | 132 | uint32_t extraentropy) |
131 | { | | 133 | { |
132 | int est1, est2; | | 134 | char tmp[PATH_MAX]; |
133 | rndpoolstat_t rp; | | 135 | uint32_t systementropy; |
| | | 136 | uint8_t buf[32]; |
| | | 137 | SHAKE128_CTX shake128; |
134 | rndsave_t rs; | | 138 | rndsave_t rs; |
135 | SHA1_CTX s; | | 139 | SHA1_CTX s; |
136 | | | 140 | ssize_t nread, nwrit; |
137 | int fd; | | 141 | int fd; |
138 | | | 142 | |
139 | fd = open(_PATH_URANDOM, O_RDONLY, 0644); | | 143 | /* Paranoia: Avoid stack memory disclosure. */ |
140 | if (fd < 0) { | | 144 | memset(&rs, 0, sizeof rs); |
141 | err(1, "device open"); | | | |
142 | } | | | |
143 | | | | |
144 | if (ioctl(fd, RNDGETPOOLSTAT, &rp) < 0) { | | | |
145 | err(1, "ioctl(RNDGETPOOLSTAT)"); | | | |
146 | } | | | |
147 | | | 145 | |
148 | est1 = rp.curentropy; | | 146 | /* Format the temporary file name. */ |
| | | 147 | if (snprintf(tmp, sizeof tmp, "%s.tmp", filename) >= PATH_MAX) |
| | | 148 | errx(1, "path too long"); |
149 | | | 149 | |
150 | if (read(fd, rs.data, sizeof(rs.data)) != sizeof(rs.data)) { | | 150 | /* Open /dev/urandom. */ |
151 | err(1, "entropy read"); | | 151 | if ((fd = open(_PATH_URANDOM, O_RDONLY)) == -1) |
152 | } | | 152 | err(1, "device open"); |
153 | | | 153 | |
154 | if (ioctl(fd, RNDGETPOOLSTAT, &rp) < 0) { | | 154 | /* Find how much entropy is in the pool. */ |
155 | err(1, "ioctl(RNDGETPOOLSTAT)"); | | 155 | if (ioctl(fd, RNDGETENTCNT, &systementropy) == -1) |
156 | } | | 156 | err(1, "ioctl(RNDGETENTCNT)"); |
| | | 157 | |
| | | 158 | /* Read some data from /dev/urandom. */ |
| | | 159 | if ((size_t)(nread = read(fd, buf, sizeof buf)) != sizeof buf) { |
| | | 160 | if (nread == -1) |
| | | 161 | err(1, "read"); |
| | | 162 | else |
| | | 163 | errx(1, "truncated read"); |
| | | 164 | } |
| | | 165 | |
| | | 166 | /* Close /dev/urandom; we're done with it. */ |
| | | 167 | if (close(fd) == -1) |
| | | 168 | warn("close"); |
| | | 169 | fd = -1; /* paranoia */ |
157 | | | 170 | |
158 | est2 = rp.curentropy; | | 171 | /* |
| | | 172 | * Hash what we read together with the extra input to generate |
| | | 173 | * the seed data. |
| | | 174 | */ |
| | | 175 | SHAKE128_Init(&shake128); |
| | | 176 | SHAKE128_Update(&shake128, buf, sizeof buf); |
| | | 177 | SHAKE128_Update(&shake128, extra, nextra); |
| | | 178 | SHAKE128_Final(rs.data, sizeof(rs.data), &shake128); |
| | | 179 | explicit_memset(&shake128, 0, sizeof shake128); /* paranoia */ |
159 | | | 180 | |
160 | if (est1 - est2 < 0) { | | 181 | /* |
161 | rs.entropy = 0; | | 182 | * Report an upper bound on the min-entropy of the seed data. |
162 | } else { | | 183 | * We take the larger of the system entropy and the extra |
163 | rs.entropy = est1 - est2; | | 184 | * entropy -- the system state and the extra input may or may |
164 | } | | 185 | * not be independent, so we can't add them -- and clamp to the |
| | | 186 | * size of the data. |
| | | 187 | */ |
| | | 188 | systementropy = MIN(systementropy, |
| | | 189 | MIN(sizeof(buf), UINT32_MAX/NBBY)*NBBY); |
| | | 190 | extraentropy = MIN(extraentropy, MIN(nextra, UINT32_MAX/NBBY)*NBBY); |
| | | 191 | rs.entropy = MIN(MAX(systementropy, extraentropy), |
| | | 192 | MIN(sizeof(rs.data), UINT32_MAX/NBBY)*NBBY); |
165 | | | 193 | |
| | | 194 | /* |
| | | 195 | * Compute the checksum on the 32-bit entropy count, in host |
| | | 196 | * byte order (XXX this means it is not portable across |
| | | 197 | * different-endian platforms!), followed by the seed data. |
| | | 198 | */ |
166 | SHA1Init(&s); | | 199 | SHA1Init(&s); |
167 | SHA1Update(&s, (uint8_t *)&rs.entropy, sizeof(rs.entropy)); | | 200 | SHA1Update(&s, (const uint8_t *)&rs.entropy, sizeof(rs.entropy)); |
168 | SHA1Update(&s, rs.data, sizeof(rs.data)); | | 201 | SHA1Update(&s, rs.data, sizeof(rs.data)); |
169 | SHA1Final(rs.digest, &s); | | 202 | SHA1Final(rs.digest, &s); |
| | | 203 | explicit_memset(&s, 0, sizeof s); /* paranoia */ |
170 | | | 204 | |
171 | close(fd); | | 205 | /* |
172 | unlink(filename); | | 206 | * Write it to a temporary file and sync it before we commit. |
173 | fd = open(filename, O_CREAT|O_EXCL|O_WRONLY, 0600); | | 207 | * This way either the old seed or the new seed is completely |
174 | if (fd < 0) { | | 208 | * written in the expected location on disk even if the system |
175 | err(1, "output open"); | | 209 | * crashes as long as the file system doesn't get corrupted too |
176 | } | | 210 | * badly. |
177 | | | 211 | * |
178 | if (write(fd, &rs, sizeof(rs)) != sizeof(rs)) { | | 212 | * If interrupted after this point and the temporary file is |
179 | unlink(filename); | | 213 | * disclosed, no big deal -- either the pool was predictable to |
180 | fsync_range(fd, FDATASYNC|FDISKSYNC, (off_t)0, (off_t)0); | | 214 | * begin with in which case we're hosed either way, or we've |
181 | err(1, "write"); | | 215 | * just revealed some output which is not a problem. |
182 | } | | 216 | */ |
183 | fsync_range(fd, FDATASYNC|FDISKSYNC, (off_t)0, (off_t)0); | | 217 | if ((fd = open(tmp, O_CREAT|O_TRUNC|O_WRONLY, 0600)) == -1) |
184 | close(fd); | | 218 | err(1, "open seed file to save"); |
| | | 219 | if ((size_t)(nwrit = write(fd, &rs, sizeof rs)) != sizeof rs) { |
| | | 220 | int error = errno; |
| | | 221 | if (unlink(tmp) == -1) |
| | | 222 | warn("unlink"); |
| | | 223 | if (nwrit == -1) |
| | | 224 | errc(1, error, "write"); |
| | | 225 | else |
| | | 226 | errx(1, "truncated write"); |
| | | 227 | } |
| | | 228 | explicit_memset(&rs, 0, sizeof rs); /* paranoia */ |
| | | 229 | if (fsync_range(fd, FDATASYNC|FDISKSYNC, 0, 0) == -1) { |
| | | 230 | int error = errno; |
| | | 231 | if (unlink(tmp) == -1) |
| | | 232 | warn("unlink"); |
| | | 233 | errc(1, error, "fsync_range"); |
| | | 234 | } |
| | | 235 | if (close(fd) == -1) |
| | | 236 | warn("close"); |
| | | 237 | |
| | | 238 | /* Rename it over the original file to commit. */ |
| | | 239 | if (rename(tmp, filename) == -1) |
| | | 240 | err(1, "rename"); |
185 | } | | 241 | } |
186 | | | 242 | |
187 | static void | | 243 | static void |
188 | do_load(const char *const filename) | | 244 | do_load(const char *filename) |
189 | { | | 245 | { |
190 | int fd; | | 246 | char tmp[PATH_MAX]; |
191 | rndsave_t rs, rszero; | | 247 | int fd_seed, fd_random; |
| | | 248 | rndsave_t rs; |
192 | rnddata_t rd; | | 249 | rnddata_t rd; |
| | | 250 | ssize_t nread, nwrit; |
193 | SHA1_CTX s; | | 251 | SHA1_CTX s; |
194 | uint8_t digest[SHA1_DIGEST_LENGTH]; | | 252 | uint8_t digest[SHA1_DIGEST_LENGTH]; |
195 | | | 253 | |
196 | fd = open(filename, O_RDWR, 0600); | | 254 | /* |
197 | if (fd < 0) { | | 255 | * The order of operations is important here: |
198 | err(1, "input open"); | | 256 | * |
199 | } | | 257 | * 1. Load the old seed. |
200 | | | 258 | * 2. Feed the old seed into the kernel. |
201 | unlink(filename); | | 259 | * 3. Generate and write a new seed. |
| | | 260 | * 4. Erase the old seed. |
| | | 261 | * |
| | | 262 | * This follows the procedure in |
| | | 263 | * |
| | | 264 | * Niels Ferguson, Bruce Schneier, and Tadayoshi Kohno, |
| | | 265 | * _Cryptography Engineering_, Wiley, 2010, Sec. 9.6.2 |
| | | 266 | * `Update Seed File'. |
| | | 267 | * |
| | | 268 | * There is a race condition: If another process generates a |
| | | 269 | * key from /dev/urandom after step (2) but before step (3), |
| | | 270 | * and if the machine crashes before step (3), an adversary who |
| | | 271 | * can read the disk after the crash can probably guess the |
| | | 272 | * complete state of the entropy pool and thereby predict the |
| | | 273 | * key. |
| | | 274 | * |
| | | 275 | * There's not much we can do here without some kind of |
| | | 276 | * systemwide lock on /dev/urandom and without introducing an |
| | | 277 | * opportunity for a crash to wipe out the entropy altogether. |
| | | 278 | * To avoid this race, you should ensure that any key |
| | | 279 | * generation happens _after_ `rndctl -L' has completed. |
| | | 280 | */ |
202 | | | 281 | |
203 | if (read(fd, &rs, sizeof(rs)) != sizeof(rs)) { | | 282 | /* Format the temporary file name. */ |
204 | err(1, "read"); | | 283 | if (snprintf(tmp, sizeof tmp, "%s.tmp", filename) >= PATH_MAX) |
| | | 284 | errx(1, "path too long"); |
| | | 285 | |
| | | 286 | /* 1. Load the old seed. */ |
| | | 287 | if ((fd_seed = open(filename, O_RDWR)) == -1) |
| | | 288 | err(1, "open seed file to load"); |
| | | 289 | if ((size_t)(nread = read(fd_seed, &rs, sizeof rs)) != sizeof rs) { |
| | | 290 | if (nread == -1) |
| | | 291 | err(1, "read seed"); |
| | | 292 | else |
| | | 293 | errx(1, "seed too short"); |
205 | } | | 294 | } |
206 | | | 295 | |
207 | memset(&rszero, 0, sizeof(rszero)); | | 296 | /* Verify its checksum. */ |
208 | if (pwrite(fd, &rszero, sizeof(rszero), (off_t)0) != sizeof(rszero)) | | | |
209 | err(1, "overwrite"); | | | |
210 | fsync_range(fd, FDATASYNC|FDISKSYNC, (off_t)0, (off_t)0); | | | |
211 | close(fd); | | | |
212 | | | | |
213 | SHA1Init(&s); | | 297 | SHA1Init(&s); |
214 | SHA1Update(&s, (uint8_t *)&rs.entropy, sizeof(rs.entropy)); | | 298 | SHA1Update(&s, (const uint8_t *)&rs.entropy, sizeof(rs.entropy)); |
215 | SHA1Update(&s, rs.data, sizeof(rs.data)); | | 299 | SHA1Update(&s, rs.data, sizeof(rs.data)); |
216 | SHA1Final(digest, &s); | | 300 | SHA1Final(digest, &s); |
217 | | | 301 | if (!consttime_memequal(digest, rs.digest, sizeof(digest))) { |
218 | if (memcmp(digest, rs.digest, sizeof(digest))) { | | 302 | /* |
219 | errx(1, "bad digest"); | | 303 | * If the checksum doesn't match, doesn't hurt to feed |
| | | 304 | * the seed in anyway, but act as though it has zero |
| | | 305 | * entropy in case it was corrupted with predictable |
| | | 306 | * garbage. |
| | | 307 | */ |
| | | 308 | warnx("bad checksum"); |
| | | 309 | rs.entropy = 0; |
220 | } | | 310 | } |
221 | | | 311 | |
| | | 312 | /* Format the ioctl request. */ |
222 | rd.len = MIN(sizeof(rd.data), sizeof(rs.data)); | | 313 | rd.len = MIN(sizeof(rd.data), sizeof(rs.data)); |
223 | rd.entropy = rs.entropy; | | 314 | rd.entropy = rs.entropy; |
224 | memcpy(rd.data, rs.data, MIN(sizeof(rd.data), sizeof(rs.data))); | | 315 | memcpy(rd.data, rs.data, rd.len); |
| | | 316 | explicit_memset(&rs, 0, sizeof rs); /* paranoia */ |
225 | | | 317 | |
226 | fd = open(_PATH_URANDOM, O_RDWR, 0644); | | 318 | /* 2. Feed the old seed into the kernel. */ |
227 | if (fd < 0) { | | 319 | if ((fd_random = open(_PATH_URANDOM, O_WRONLY)) == -1) |
228 | err(1, "device open"); | | 320 | err(1, "open /dev/urandom"); |
229 | } | | 321 | if (ioctl(fd_random, RNDADDDATA, &rd) == -1) |
| | | 322 | err(1, "RNDADDDATA"); |
| | | 323 | if (close(fd_random) == -1) |
| | | 324 | warn("close /dev/urandom"); |
| | | 325 | fd_random = -1; /* paranoia */ |
230 | | | 326 | |
231 | if (ioctl(fd, RNDADDDATA, &rd) < 0) { | | 327 | /* |
232 | err(1, "ioctl"); | | 328 | * 3. Generate and write a new seed. Note that we hash the old |
| | | 329 | * seed together with whatever /dev/urandom returns in do_save. |
| | | 330 | * Why? After RNDADDDATA, the input may not be distributed |
| | | 331 | * immediately to /dev/urandom. |
| | | 332 | */ |
| | | 333 | do_save(filename, rd.data, rd.len, rd.entropy); |
| | | 334 | explicit_memset(&rd, 0, sizeof rd); /* paranoia */ |
| | | 335 | |
| | | 336 | /* |
| | | 337 | * 4. Erase the old seed. Only effective if we're on a |
| | | 338 | * fixed-address file system like ffs -- doesn't help to erase |
| | | 339 | * the data on lfs, but doesn't hurt either. No need to unlink |
| | | 340 | * because do_save will have already overwritten it. |
| | | 341 | */ |
| | | 342 | memset(&rs, 0, sizeof rs); |
| | | 343 | if ((size_t)(nwrit = pwrite(fd_seed, &rs, sizeof rs, 0)) != |
| | | 344 | sizeof rs) { |
| | | 345 | if (nwrit == -1) |
| | | 346 | err(1, "overwrite old seed"); |
| | | 347 | else |
| | | 348 | errx(1, "truncated overwrite"); |
233 | } | | 349 | } |
234 | close(fd); | | 350 | if (fsync_range(fd_seed, FDATASYNC|FDISKSYNC, 0, 0) == -1) |
| | | 351 | err(1, "fsync_range"); |
235 | } | | 352 | } |
236 | | | 353 | |
237 | static void | | 354 | static void |
238 | do_ioctl(rndctl_t *rctl) | | 355 | do_ioctl(rndctl_t *rctl) |
239 | { | | 356 | { |
240 | int fd; | | 357 | int fd; |
241 | int res; | | 358 | int res; |
242 | | | 359 | |
243 | fd = open(_PATH_URANDOM, O_RDONLY, 0644); | | 360 | fd = open(_PATH_URANDOM, O_RDONLY, 0644); |
244 | if (fd < 0) | | 361 | if (fd < 0) |
245 | err(1, "open"); | | 362 | err(1, "open"); |
246 | | | 363 | |
247 | res = ioctl(fd, RNDCTL, rctl); | | 364 | res = ioctl(fd, RNDCTL, rctl); |
| @@ -387,26 +504,29 @@ do_stats(void) | | | @@ -387,26 +504,29 @@ do_stats(void) |
387 | | | 504 | |
388 | close(fd); | | 505 | close(fd); |
389 | } | | 506 | } |
390 | | | 507 | |
391 | int | | 508 | int |
392 | main(int argc, char **argv) | | 509 | main(int argc, char **argv) |
393 | { | | 510 | { |
394 | rndctl_t rctl; | | 511 | rndctl_t rctl; |
395 | int ch, cmd, lflag, mflag, sflag; | | 512 | int ch, cmd, lflag, mflag, sflag; |
396 | u_int32_t type; | | 513 | u_int32_t type; |
397 | char name[16]; | | 514 | char name[16]; |
398 | const char *filename = NULL; | | 515 | const char *filename = NULL; |
399 | | | 516 | |
| | | 517 | if (SHA3_Selftest() != 0) |
| | | 518 | errx(1, "SHA-3 self-test failed"); |
| | | 519 | |
400 | rctl.mask = 0; | | 520 | rctl.mask = 0; |
401 | rctl.flags = 0; | | 521 | rctl.flags = 0; |
402 | | | 522 | |
403 | cmd = 0; | | 523 | cmd = 0; |
404 | lflag = 0; | | 524 | lflag = 0; |
405 | mflag = 0; | | 525 | mflag = 0; |
406 | sflag = 0; | | 526 | sflag = 0; |
407 | type = 0xff; | | 527 | type = 0xff; |
408 | | | 528 | |
409 | while ((ch = getopt(argc, argv, "CES:L:celt:d:sv")) != -1) { | | 529 | while ((ch = getopt(argc, argv, "CES:L:celt:d:sv")) != -1) { |
410 | switch (ch) { | | 530 | switch (ch) { |
411 | case 'C': | | 531 | case 'C': |
412 | rctl.flags |= RND_FLAG_NO_COLLECT; | | 532 | rctl.flags |= RND_FLAG_NO_COLLECT; |
| @@ -472,27 +592,27 @@ main(int argc, char **argv) | | | @@ -472,27 +592,27 @@ main(int argc, char **argv) |
472 | argc -= optind; | | 592 | argc -= optind; |
473 | argv += optind; | | 593 | argv += optind; |
474 | | | 594 | |
475 | /* | | 595 | /* |
476 | * No leftover non-option arguments. | | 596 | * No leftover non-option arguments. |
477 | */ | | 597 | */ |
478 | if (argc > 0) | | 598 | if (argc > 0) |
479 | usage(); | | 599 | usage(); |
480 | | | 600 | |
481 | /* | | 601 | /* |
482 | * Save. | | 602 | * Save. |
483 | */ | | 603 | */ |
484 | if (cmd == 'S') { | | 604 | if (cmd == 'S') { |
485 | do_save(filename); | | 605 | do_save(filename, NULL, 0, 0); |
486 | exit(0); | | 606 | exit(0); |
487 | } | | 607 | } |
488 | | | 608 | |
489 | /* | | 609 | /* |
490 | * Load. | | 610 | * Load. |
491 | */ | | 611 | */ |
492 | if (cmd == 'L') { | | 612 | if (cmd == 'L') { |
493 | do_load(filename); | | 613 | do_load(filename); |
494 | exit(0); | | 614 | exit(0); |
495 | } | | 615 | } |
496 | | | 616 | |
497 | /* | | 617 | /* |
498 | * Cannot list and modify at the same time. | | 618 | * Cannot list and modify at the same time. |