keys.scala, etc.: Make merging public keys have a progress bar.
[tripe-android] / keys.scala
CommitLineData
8eabb4ff
MW
1/* -*-scala-*-
2 *
3 * Key distribution
4 *
5 * (c) 2018 Straylight/Edgeware
6 */
7
8/*----- Licensing notice --------------------------------------------------*
9 *
10 * This file is part of the Trivial IP Encryption (TrIPE) Android app.
11 *
12 * TrIPE is free software: you can redistribute it and/or modify it under
13 * the terms of the GNU General Public License as published by the Free
14 * Software Foundation; either version 3 of the License, or (at your
15 * option) any later version.
16 *
17 * TrIPE is distributed in the hope that it will be useful, but WITHOUT
18 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
20 * for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with TrIPE. If not, see <https://www.gnu.org/licenses/>.
24 */
25
26package uk.org.distorted.tripe; package object keys {
27
28/*----- Imports -----------------------------------------------------------*/
29
b1ec59e3 30import scala.collection.mutable.{ArrayBuffer, HashMap};
8eabb4ff 31
a5ec891a
MW
32import java.io.{Closeable, File, IOException};
33import java.lang.{Long => JLong};
c8292b34 34import java.net.{URL, URLConnection};
a5ec891a
MW
35import java.text.SimpleDateFormat;
36import java.util.Date;
c8292b34
MW
37import java.util.zip.GZIPInputStream;
38
39import sys.{SystemError, hashsz, runCommand};
40import sys.Errno.EEXIST;
41import sys.FileImplicits._;
a5ec891a 42import sys.FileInfo.{DIR, REG};
c8292b34 43
b1ec59e3
MW
44import progress.{Eyecandy, SimpleModel, DataModel, DetailedModel};
45import Implicits.truish;
04a5abae 46
8eabb4ff
MW
47/*----- Useful regular expressions ----------------------------------------*/
48
04a5abae
MW
49private final val RX_COMMENT = """(?x) ^ \s* (?: \# .* )? $""".r;
50private final val RX_KEYVAL = """(?x) ^ \s*
8eabb4ff
MW
51 ([-\w]+)
52 (?:\s+(?!=)|\s*=\s*)
53 (|\S|\S.*\S)
54 \s* $""".r;
04a5abae 55private final val RX_DOLLARSUBST = """(?x) \$ \{ ([-\w]+) \}""".r;
8eabb4ff 56
a5ec891a
MW
57private final val RX_PUBKEY = """(?x) ^ peer- (.*) \.pub $""".r;
58
59private final val RX_KEYINFO = """(?x) ^ ([^:]*) : \s* (\S.*) $""".r
60private final val RX_KEYATTR = """(?x) ^ \s*
61 ([^\s=] | [^\s=][^=]*[^\s=])
62 \s* = \s*
63 (\S.*) $""".r;
64
8eabb4ff
MW
65/*----- Things that go wrong ----------------------------------------------*/
66
67class ConfigSyntaxError(val file: String, val lno: Int, val msg: String)
68 extends Exception {
69 override def getMessage(): String = s"$file:$lno: $msg";
70}
71
72class ConfigDefaultFailed(val file: String, val dfltkey: String,
73 val badkey: String, val badval: String)
74 extends Exception {
75 override def getMessage(): String =
76 s"$file: can't default `$dfltkey' because " +
77 s"`$badval' is not a recognized value for `$badkey'";
78}
79
80class DefaultFailed(val key: String) extends Exception;
81
82/*----- Parsing a configuration -------------------------------------------*/
83
84type Config = scala.collection.Map[String, String];
85
c8292b34 86private val DEFAULTS: Seq[(String, Config => String)] =
8eabb4ff
MW
87 Seq("repos-base" -> { _ => "tripe-keys.tar.gz" },
88 "sig-base" -> { _ => "tripe-keys.sig-<SEQ>" },
89 "repos-url" -> { conf => conf("base-url") + conf("repos-base") },
90 "sig-url" -> { conf => conf("base-url") + conf("sig-base") },
91 "kx" -> { _ => "dh" },
92 "kx-genalg" -> { conf => conf("kx") match {
93 case alg@("dh" | "ec" | "x25519" | "x448") => alg
94 case _ => throw new DefaultFailed("kx")
95 } },
96 "kx-expire" -> { _ => "now + 1 year" },
97 "kx-warn-days" -> { _ => "28" },
98 "bulk" -> { _ => "iiv" },
99 "cipher" -> { conf => conf("bulk") match {
100 case "naclbox" => "salsa20"
101 case _ => "rijndael-cbc"
102 } },
103 "hash" -> { _ => "sha256" },
104 "mgf" -> { conf => conf("hash") + "-mgf" },
105 "mac" -> { conf => conf("bulk") match {
106 case "naclbox" => "poly1305/128"
107 case _ =>
108 val h = conf("hash");
c8292b34 109 hashsz(h) match {
8eabb4ff
MW
110 case -1 => throw new DefaultFailed("hash")
111 case hsz => s"${h}-hmac/${4*hsz}"
112 }
113 } },
114 "sig" -> { conf => conf("kx") match {
115 case "dh" => "dsa"
116 case "ec" => "ecdsa"
117 case "x25519" => "ed25519"
118 case "x448" => "ed448"
119 case _ => throw new DefaultFailed("kx")
120 } },
121 "sig-fresh" -> { _ => "always" },
122 "fingerprint-hash" -> { _("hash") });
123
3bb2303d 124private def parseConfig(file: File): HashMap[String, String] = {
8eabb4ff 125
04a5abae 126 /* Build the new configuration in a temporary place. */
ad64fbfa 127 val m = HashMap[String, String]();
04a5abae
MW
128
129 /* Read the config file into our map. */
130 file.withReader { in =>
131 var lno = 1;
132 for (line <- lines(in)) {
133 line match {
134 case RX_COMMENT() => ok;
3bb2303d 135 case RX_KEYVAL(key, value) => m(key) = value;
04a5abae
MW
136 case _ =>
137 throw new ConfigSyntaxError(file.getPath, lno,
138 "failed to parse line");
139 }
140 lno += 1;
c8292b34 141 }
04a5abae
MW
142 }
143
a5ec891a
MW
144 /* Done. */
145 m
146}
147
148private def readConfig(file: File): Config = {
149 var m = parseConfig(file);
150
04a5abae
MW
151 /* Fill in defaults where things have been missed out. */
152 for ((key, dflt) <- DEFAULTS) {
153 if (!(m contains key)) {
3bb2303d 154 try { m(key) = dflt(m); }
04a5abae
MW
155 catch {
156 case e: DefaultFailed =>
157 throw new ConfigDefaultFailed(file.getPath, key,
158 e.key, m(e.key));
159 }
c8292b34 160 }
04a5abae
MW
161 }
162
163 /* And we're done. */
164 m
165}
166
167/*----- Managing a key repository -----------------------------------------*/
168
169def downloadToFile(file: File, url: URL,
170 maxlen: Long = Long.MaxValue,
171 ic: Eyecandy) {
172 ic.job(new SimpleModel(s"connecting to `$url'", -1)) { jr =>
173 fetchURL(url, new URLFetchCallbacks {
174 val out = file.openForOutput();
175 private def toobig() {
176 throw new KeyConfigException(
177 s"remote file `$url' is suspiciously large");
178 }
179 var totlen: Long = 0;
180 override def preflight(conn: URLConnection) {
181 totlen = conn.getContentLength;
182 if (totlen > maxlen) toobig();
183 jr.change(new SimpleModel(s"downloading `$url'", totlen)
184 with DataModel,
185 0);
186 }
187 override def done(win: Boolean) { out.close(); }
188 def write(buf: Array[Byte], n: Int, len: Long) {
189 if (len + n > maxlen) toobig();
190 out.write(buf, 0, n);
191 jr.step(len + n);
192 }
193 })
194 }
8eabb4ff
MW
195}
196
8eabb4ff
MW
197/* Lifecycle notes
198 *
199 * -> empty
200 *
201 * insert config file via URL or something
202 *
203 * -> pending (pending/tripe-keys.conf)
204 *
205 * verify master key fingerprint (against barcode?)
206 *
207 * -> confirmed (live/tripe-keys.conf; no live/repos)
208 * -> live (live/...)
209 *
210 * download package
211 * extract contents
212 * verify signature
213 * build keyrings
214 * build peer config
215 * rename tmp -> new
216 *
217 * -> updating (live/...; new/...)
218 *
219 * rename old repository aside
220 *
221 * -> committing (old/...; new/...)
222 *
223 * rename verified repository
224 *
225 * -> live (live/...)
226 *
227 * (delete old/)
228 */
229
04a5abae
MW
230class RepositoryStateException(val state: Repository.State.Value,
231 msg: String)
232 extends Exception(msg);
233
234class KeyConfigException(msg: String) extends Exception(msg);
235
236private def launderFingerprint(fp: String): String =
237 fp filter { _.isLetterOrDigit };
238
239private def fingerprintsEqual(a: String, b: String) =
240 launderFingerprint(a) == launderFingerprint(b);
241
242private def keyFingerprint(kr: File, tag: String, hash: String): String = {
243 val (out, _) = runCommand("key", "-k", kr.getPath, "fingerprint",
244 "-a", hash, "-f", "-secret", tag);
245 nextToken(out) match {
246 case Some((fp, _)) => fp
247 case _ =>
a5ec891a 248 throw new IOException("unexpected output from `key fingerprint'");
04a5abae
MW
249 }
250}
251
a5ec891a
MW
252private def checkIdent(id: String) {
253 if (id exists { ch => ch == ':' || ch == '.' || ch.isWhitespace })
254 throw new IllegalArgumentException(s"bad key tag `$id'");
255}
256
8eabb4ff
MW
257object Repository {
258 object State extends Enumeration {
259 val Empty, Pending, Confirmed, Updating, Committing, Live = Value;
260 }
8eabb4ff
MW
261}
262
04a5abae
MW
263def checkConfigSanity(file: File, ic: Eyecandy) {
264 ic.operation("checking new configuration") { _ =>
c8292b34 265
04a5abae
MW
266 /* Make sure we can read and understand the file. */
267 val conf = readConfig(file);
268
269 /* Make sure there are entries which we can use to update. This won't
270 * guarantee that we can reliably update, but it will help.
271 */
272 conf("repos-url"); conf("sig-url");
273 conf("fingerprint-hash"); conf("sig-fresh");
274 conf("master-sequence"); conf("hk-master");
275 }
276}
c8292b34 277
a5ec891a 278private val keydatefmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
b1ec59e3 279
a5ec891a
MW
280class PrivateKey private[keys](repo: Repository, dir: File) {
281 private[this] lazy val keyring = dir/"keyring";
282 private[this] lazy val meta = parseConfig(dir/"meta");
283 lazy val tag = meta("tag");
284 lazy val time = datefmt synchronized { datefmt.parse(meta("time")); };
285 lazy val fingerprint = keyFingerprint(keyring, tag,
286 repo.config("fingerprint-hash"));
287
288 def remove() { dir.rmTree(); }
289
290 private[this] lazy val (info, _attr) = {
291 val m = Map.newBuilder[String, String];
292 val a = Map.newBuilder[String, String];
293 val (out, _) = runCommand("key", "-k", keyring.getPath,
294 "list", "-vv", tag);
295 val lines = out.lines;
296 while (lines.hasNext) lines.next match {
297 case "attributes:" =>
298 while (lines.hasNext) lines.next match {
299 case RX_KEYATTR(k, v) => a += k -> v;
300 case line => throw new IOException(
301 s"unexpected output from `key list': $line");
302 }
303 case RX_KEYINFO(k, v) =>
304 m += k -> v;
305 case line => throw new IOException(
306 s"unexpected output from `key list': $line");
307 }
308 (m.result, a.result)
309 }
310
311 lazy val expires = info("expiry") match {
312 case "forever" => None
313 case d => Some(keydatefmt synchronized { keydatefmt.parse(d) })
314 }
315 lazy val ty = info("type");
316 lazy val comment = info("comment");
317 lazy val keyid = {
318
319 /* Ugh. Using `Int' throws an exception on words whose top bit is set
320 * because Java doesn't have proper unsigned integers. There's
321 * `parseUnsignedInt' in Java 1.8, but that limits our Android targets.
322 * And Scala has put its own `Long' object in the way of Java's so we
b1ec59e3 323 * need this circumlocution.
a5ec891a
MW
324 */
325 (JLong.parseLong(info("keyid"), 16)&0xffffffff).toInt;
326 }
327 lazy val attr = _attr;
328}
329
8eabb4ff
MW
330class Repository(val root: File) extends Closeable {
331 import Repository.State.{Value => State, _};
332
c8292b34 333 /* Important directories and files. */
a5ec891a
MW
334 private[this] val configdir = root/"config";
335 private[this] val livedir = configdir/"live";
04a5abae 336 private[this] val livereposdir = livedir/"repos";
a5ec891a
MW
337 private[this] val newdir = configdir/"new";
338 private[this] val olddir = configdir/"old";
339 private[this] val pendingdir = configdir/"pending";
04a5abae 340 private[this] val tmpdir = root/"tmp";
a5ec891a 341 private[this] val keysdir = root/"keys";
c8292b34
MW
342
343 /* Take out a lock in case of other instances. */
a5ec891a 344 private[this] var open = false;
c8292b34 345 private[this] val lock = {
a5ec891a
MW
346 root.mkdirNew_!();
347 open = true;
04a5abae 348 (root/"lk").lock_!()
c8292b34 349 }
a5ec891a
MW
350 def close() { lock.close(); open = false; }
351 private[this] def checkLocked()
352 { if (!open) throw new IllegalStateException("repository is unlocked"); }
c8292b34
MW
353
354 /* Maintain a cache of some repository state. */
355 private var _state: State = null;
356 private var _config: Config = null;
357 private def invalidate() {
358 _state = null;
359 _config = null;
360 }
361
362 def state: State = {
363 /* Determine the current repository state. */
364
365 if (_state == null)
366 _state = if (livedir.isdir_!) {
367 if (!livereposdir.isdir_!) Confirmed
368 else if (newdir.isdir_!) Updating
369 else Live
370 } else {
371 if (newdir.isdir_!) Committing
372 else if (pendingdir.isdir_!) Pending
373 else Empty
374 }
375
376 _state
377 }
378
379 def checkState(wanted: State*) {
380 /* Ensure we're in a particular state. */
a5ec891a 381 checkLocked();
c8292b34
MW
382 val st = state;
383 if (wanted.forall(_ != st)) {
384 throw new RepositoryStateException(st, s"Repository is $st, not " +
385 oxford("or",
386 wanted.map(_.toString)));
387 }
388 }
389
a5ec891a 390 def cleanup(ic: Eyecandy) {
c8292b34
MW
391
392 /* If we're part-way through an update then back out or press forward. */
393 state match {
394
395 case Updating =>
396 /* We have a new tree allegedly ready, but the current one is still
397 * in place. It seems safer to zap the new one here, but we could go
398 * either way.
399 */
400
a5ec891a
MW
401 ic.operation("rolling back failed update")
402 { _ => newdir.rmTree(); }
c8292b34
MW
403 invalidate(); // should move back to `Live' or `Confirmed'
404
405 case Committing =>
406 /* We have a new tree ready, and an old one moved aside. We're going
407 * to have to move one of them. Let's try committing the new tree.
408 */
409
a5ec891a
MW
410 ic.operation("committing interrupted update")
411 { _ => newdir.rename_!(livedir); }
412 invalidate(); // should move on to `Live'
c8292b34
MW
413
414 case _ =>
415 /* Other states are stable. */
416 ok;
8eabb4ff 417 }
c8292b34
MW
418
419 /* Now work through the things in our area of the filesystem and zap the
420 * ones which don't belong. In particular, this will always erase
421 * `tmpdir'.
422 */
a5ec891a
MW
423 ic.operation("cleaning up configuration area") { or =>
424 val st = state;
425 root foreachFile { f => f.getName match {
426 case "lk" | "keys" => ok;
427 case "config" => configdir foreachFile { f => (f.getName, st) match {
428 case ("live", Live | Confirmed) => ok;
429 case ("pending", Pending) => ok;
430 case (_, Updating | Committing) =>
431 unreachable(s"unexpectedly still in `$st' state");
432 case _ => or.step(s"delete `$f'"); f.rmTree();
433 } }
434 case _ => or.step(s"delete `$f'"); f.rmTree();
435 } }
c8292b34 436 }
a5ec891a 437 }
c8292b34 438
04a5abae 439 def destroy(ic: Eyecandy) {
c8292b34 440 /* Clear out the entire repository. Everything. It's all gone. */
04a5abae 441 ic.operation("clearing configuration")
a5ec891a 442 { _ => root foreachFile { f => if (f.getName != "lk") f.rmTree(); } }
c8292b34
MW
443 }
444
445 def clearTmp() {
446 /* Arrange to have an empty `tmpdir'. */
447 tmpdir.rmTree();
448 tmpdir.mkdir_!();
449 }
450
451 def config: Config = {
452 /* Return the repository configuration. */
453
a5ec891a 454 checkLocked();
c8292b34
MW
455 if (_config == null) {
456
457 /* Firstly, decide where to find the configuration file. */
a5ec891a 458 checkState(Pending, Confirmed, Live);
c8292b34
MW
459 val dir = state match {
460 case Live | Confirmed => livedir
461 case Pending => pendingdir
a5ec891a 462 case _ => ???
c8292b34 463 }
c8292b34 464
04a5abae
MW
465 /* And then read the configuration. */
466 _config = readConfig(dir/"tripe-keys.conf");
c8292b34
MW
467 }
468
469 _config
8eabb4ff
MW
470 }
471
04a5abae 472 def fetchConfig(url: URL, ic: Eyecandy) {
c8292b34
MW
473 /* Fetch an initial configuration file from a given URL. */
474
475 checkState(Empty);
476 clearTmp();
04a5abae
MW
477
478 val conffile = tmpdir/"tripe-keys.conf";
479 downloadToFile(conffile, url, 16*1024, ic);
480 checkConfigSanity(conffile, ic);
a5ec891a 481 configdir.mkdirNew_!();
04a5abae
MW
482 ic.operation("committing configuration")
483 { _ => tmpdir.rename_!(pendingdir); }
c8292b34 484 invalidate(); // should move to `Pending'
a5ec891a 485 cleanup(ic);
8eabb4ff
MW
486 }
487
04a5abae 488 def confirm(ic: Eyecandy) {
c8292b34
MW
489 /* The user has approved the master key fingerprint in the `Pending'
490 * configuration. Advance to `Confirmed'.
491 */
492
493 checkState(Pending);
04a5abae
MW
494 ic.operation("confirming configuration")
495 { _ => pendingdir.rename_!(livedir); }
c8292b34
MW
496 invalidate(); // should move to `Confirmed'
497 }
498
04a5abae 499 def update(ic: Eyecandy) {
c8292b34
MW
500 /* Update the repository from the master.
501 *
502 * Fetch a (possibly new) archive; unpack it; verify the master key
503 * against the known fingerprint; and check the signature on the bundle.
504 */
505
a5ec891a 506 cleanup(ic);
c8292b34
MW
507 checkState(Confirmed, Live);
508 val conf = config;
509 clearTmp();
510
511 /* First thing is to download the tarball and signature. */
04a5abae
MW
512 val tarfile = tmpdir/"tripe-keys.tar.gz";
513 downloadToFile(tarfile, new URL(conf("repos-url")), 256*1024, ic);
514 val sigfile = tmpdir/"tripe-keys.sig";
c8292b34
MW
515 val seq = conf("master-sequence");
516 downloadToFile(sigfile,
517 new URL(conf("sig-url").replaceAllLiterally("<SEQ>",
04a5abae
MW
518 seq)),
519 4*1024, ic);
c8292b34
MW
520
521 /* Unpack the tarball. Carefully. */
04a5abae
MW
522 val unpkdir = tmpdir/"unpk";
523 ic.operation("unpacking archive") { or =>
524 unpkdir.mkdir_!();
525 withCleaner { clean =>
526 val tar = new TarFile(new GZIPInputStream(tarfile.open()));
527 clean { tar.close(); }
528 for (e <- tar) {
529
530 /* Check the filename to make sure it's not evil. */
a5ec891a
MW
531 if (e.name(0) == '/' || e.name.split('/').exists { _ == ".." }) {
532 throw new KeyConfigException(
533 s"invalid path `${e.name}' in tarball");
534 }
04a5abae
MW
535
536 /* Report on progress. */
537 or.step(s"entry `${e.name}'");
538
539 /* Find out where this file points. */
540 val f = unpkdir/e.name;
541
542 /* Unpack it. */
a5ec891a
MW
543 e.typ match {
544 case DIR =>
545 /* A directory. Create it if it doesn't exist already. */
546
547 f.mkdirNew_!();
04a5abae 548
a5ec891a
MW
549 case REG =>
550 /* A regular file. Write stuff to it. */
04a5abae 551
a5ec891a
MW
552 e.withStream { in =>
553 f.withOutput { out =>
554 for ((b, n) <- blocks(in)) out.write(b, 0, n);
555 }
04a5abae 556 }
c8292b34 557
a5ec891a
MW
558 case ty =>
559 /* Something else. Be paranoid and reject it. */
560
561 throw new KeyConfigException(
562 s"entry `${e.name}' has unexpected object type $ty");
04a5abae 563 }
c8292b34
MW
564 }
565 }
8eabb4ff
MW
566 }
567
c8292b34 568 /* There ought to be a file in here called `repos/master.pub'. */
04a5abae
MW
569 val reposdir = unpkdir/"repos";
570 val masterfile = reposdir/"master.pub";
571
c8292b34
MW
572 if (!reposdir.isdir_!)
573 throw new KeyConfigException("missing `repos/' directory");
c8292b34
MW
574 if (!masterfile.isreg_!)
575 throw new KeyConfigException("missing `repos/master.pub' file");
04a5abae 576 val mastertag = s"master-$seq";
8eabb4ff 577
c8292b34 578 /* Fetch the master key's fingerprint. */
04a5abae
MW
579 ic.operation("checking master key fingerprint") { _ =>
580 val foundfp = keyFingerprint(masterfile, mastertag,
581 conf("fingerprint-hash"));
582 val wantfp = conf("hk-master");
583 if (!fingerprintsEqual(wantfp, foundfp)) {
584 throw new KeyConfigException(
585 s"master key #$seq has wrong fingerprint: " +
586 s"expected $wantfp but found $foundfp");
587 }
588 }
589
590 /* Check the archive signature. */
591 ic.operation("verifying archive signature") { or =>
592 runCommand("catsign", "-k", masterfile.getPath, "verify", "-aqC",
593 "-k", mastertag, "-t", conf("sig-fresh"),
594 sigfile.getPath, tarfile.getPath);
595 }
596
597 /* Confirm that the configuration in the new archive is sane. */
598 checkConfigSanity(unpkdir/"tripe-keys.conf", ic);
599
b1ec59e3
MW
600 /* Build the public keyring. */
601 ic.job(new SimpleModel("counting public keys", -1)) { jr =>
602
603 /* Delete the accumulated keyring. */
a5ec891a
MW
604 val pubkeys = unpkdir/"keyring.pub";
605 pubkeys.remove_!();
b1ec59e3
MW
606
607 /* Figure out which files we need to hack. */
608 var kv = ArrayBuffer[File]();
609 reposdir.foreachFile { file => file.getName match {
610 case RX_PUBKEY(peer) if file.isreg_! => kv += file;
a5ec891a
MW
611 case _ => ok;
612 } }
b1ec59e3
MW
613 kv = kv.sorted;
614 val m = new DetailedModel("collecting public keys", kv.length);
615 var i: Long = 0;
616
617 /* Work through the key files. */
618 for (k <- kv) {
619 m.detail = k.getName;
620 if (!i) jr.change(m, i);
621 else jr.step(i);
622 runCommand("key", "-k", pubkeys.getPath, "merge", k.getPath);
623 i += 1;
624 }
625
626 /* Clean up finally. */
a5ec891a
MW
627 (unpkdir/"keyring.pub.old").remove_!();
628 }
629
04a5abae
MW
630 /* Now we just have to juggle the files about. */
631 ic.operation("committing new configuration") { _ =>
632 unpkdir.rename_!(newdir);
633 livedir.rename_!(olddir);
634 newdir.rename_!(livedir);
635 }
636
a5ec891a 637 /* All done. */
04a5abae 638 invalidate(); // should move to `Live'
a5ec891a 639 cleanup(ic);
c8292b34 640 }
a5ec891a
MW
641
642 def generateKey(tag: String, label: String, ic: Eyecandy) {
643 checkIdent(tag);
644 if (label.exists { _ == '/' })
645 throw new IllegalArgumentException(s"invalid label string `$label'");
646 if ((keysdir/label).isdir_!)
647 throw new IllegalArgumentException(s"key `$label' already exists");
648
649 cleanup(ic);
650 checkState(Live);
651 val conf = config;
652 clearTmp();
653
654 val now = datefmt synchronized { datefmt.format(new Date) };
655 val kr = tmpdir/"keyring";
656 val pub = tmpdir/s"peer-$tag.pub";
657 val param = livereposdir/"param";
658
659 keysdir.mkdirNew_!();
660
661 ic.operation("fetching key-generation parameters") { _ =>
662 runCommand("key", "-k", kr.getPath, "merge", param.getPath);
663 }
664 ic.operation("generating new key") { _ =>
665 runCommand("key", "-k", kr.getPath, "add",
666 "-a", conf("kx-genalg"), "-p", "param",
667 "-e", conf("kx-expire"), "-t", tag, "tripe");
668 }
669 ic.operation("extracting public key") { _ =>
670 runCommand("key", "-k", kr.getPath, "extract",
671 "-f", "-secret", pub.getPath, tag);
672 }
673 ic.operation("writing metadata") { _ =>
674 tmpdir/"meta" withWriter { w =>
675 w.write(s"tag = $tag\n");
676 w.write(s"time = $now\n");
677 }
678 }
679 ic.operation("installing new key") { _ =>
680 tmpdir.rename_!(keysdir/label);
681 }
682 }
683
684 def key(label: String): PrivateKey = new PrivateKey(this, keysdir/label);
685 def keyLabels: Seq[String] = (keysdir.files_! map { _.getName }).toStream;
686 def keys: Seq[PrivateKey] = keyLabels map { k => key(k) };
8eabb4ff
MW
687}
688
689/*----- That's all, folks -------------------------------------------------*/
690
691}