X-Git-Url: https://git.distorted.org.uk/~mdw/tripe-android/blobdiff_plain/7894831e9078211df0b460c4d3dd1bc51ca46804..8eabb4ff13562f3550499ee599297f7e97fa8754:/keys.scala?ds=sidebyside
diff --git a/keys.scala b/keys.scala
new file mode 100644
index 0000000..f075159
--- /dev/null
+++ b/keys.scala
@@ -0,0 +1,223 @@
+/* -*-scala-*-
+ *
+ * Key distribution
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Trivial IP Encryption (TrIPE) Android app.
+ *
+ * TrIPE is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * TrIPE is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with TrIPE. If not, see .
+ */
+
+package uk.org.distorted.tripe; package object keys {
+
+/*----- Imports -----------------------------------------------------------*/
+
+import java.io.{Closeable, File, FileOutputStream, FileReader, IOException};
+
+import scala.collection.mutable.HashMap;
+
+/*----- Useful regular expressions ----------------------------------------*/
+
+val RX_COMMENT = """(?x) ^ \s* (?: \# .* )? $""".r;
+val RX_KEYVAL = """(?x) ^ \s*
+ ([-\w]+)
+ (?:\s+(?!=)|\s*=\s*)
+ (|\S|\S.*\S)
+ \s* $""".r;
+val RX_DOLLARSUBST = """(?x) \$ \{ ([-\w]+) \}""".r;
+
+/*----- Things that go wrong ----------------------------------------------*/
+
+class ConfigSyntaxError(val file: String, val lno: Int, val msg: String)
+ extends Exception {
+ override def getMessage(): String = s"$file:$lno: $msg";
+}
+
+class ConfigDefaultFailed(val file: String, val dfltkey: String,
+ val badkey: String, val badval: String)
+ extends Exception {
+ override def getMessage(): String =
+ s"$file: can't default `$dfltkey' because " +
+ s"`$badval' is not a recognized value for `$badkey'";
+}
+
+class DefaultFailed(val key: String) extends Exception;
+
+/*----- Parsing a configuration -------------------------------------------*/
+
+type Config = scala.collection.Map[String, String];
+
+val DEFAULTS: Seq[(String, Config => String)] =
+ Seq("repos-base" -> { _ => "tripe-keys.tar.gz" },
+ "sig-base" -> { _ => "tripe-keys.sig-" },
+ "repos-url" -> { conf => conf("base-url") + conf("repos-base") },
+ "sig-url" -> { conf => conf("base-url") + conf("sig-base") },
+ "kx" -> { _ => "dh" },
+ "kx-genalg" -> { conf => conf("kx") match {
+ case alg@("dh" | "ec" | "x25519" | "x448") => alg
+ case _ => throw new DefaultFailed("kx")
+ } },
+ "kx-expire" -> { _ => "now + 1 year" },
+ "kx-warn-days" -> { _ => "28" },
+ "bulk" -> { _ => "iiv" },
+ "cipher" -> { conf => conf("bulk") match {
+ case "naclbox" => "salsa20"
+ case _ => "rijndael-cbc"
+ } },
+ "hash" -> { _ => "sha256" },
+ "mgf" -> { conf => conf("hash") + "-mgf" },
+ "mac" -> { conf => conf("bulk") match {
+ case "naclbox" => "poly1305/128"
+ case _ =>
+ val h = conf("hash");
+ JNI.hashsz(h) match {
+ case -1 => throw new DefaultFailed("hash")
+ case hsz => s"${h}-hmac/${4*hsz}"
+ }
+ } },
+ "sig" -> { conf => conf("kx") match {
+ case "dh" => "dsa"
+ case "ec" => "ecdsa"
+ case "x25519" => "ed25519"
+ case "x448" => "ed448"
+ case _ => throw new DefaultFailed("kx")
+ } },
+ "sig-fresh" -> { _ => "always" },
+ "fingerprint-hash" -> { _("hash") });
+
+def readConfig(path: String): Config = {
+ var m = HashMap[String, String]();
+ withCleaner { clean =>
+ var in = new FileReader(path); clean { in.close(); }
+ var lno = 1;
+ for (line <- lines(in)) {
+ line match {
+ case RX_COMMENT() => ();
+ case RX_KEYVAL(key, value) => m += key -> value;
+ case _ =>
+ throw new ConfigSyntaxError(path, lno, "failed to parse line");
+ }
+ lno += 1;
+ }
+ }
+
+ for ((key, dflt) <- DEFAULTS) {
+ if (!(m contains key)) {
+ try { m += key -> dflt(m); }
+ catch {
+ case e: DefaultFailed =>
+ throw new ConfigDefaultFailed(path, key, e.key, m(e.key));
+ }
+ }
+ }
+ m
+}
+
+/*----- Managing a key repository -----------------------------------------*/
+
+/* Lifecycle notes
+ *
+ * -> empty
+ *
+ * insert config file via URL or something
+ *
+ * -> pending (pending/tripe-keys.conf)
+ *
+ * verify master key fingerprint (against barcode?)
+ *
+ * -> confirmed (live/tripe-keys.conf; no live/repos)
+ * -> live (live/...)
+ *
+ * download package
+ * extract contents
+ * verify signature
+ * build keyrings
+ * build peer config
+ * rename tmp -> new
+ *
+ * -> updating (live/...; new/...)
+ *
+ * rename old repository aside
+ *
+ * -> committing (old/...; new/...)
+ *
+ * rename verified repository
+ *
+ * -> live (live/...)
+ *
+ * (delete old/)
+ */
+
+object Repository {
+ object State extends Enumeration {
+ val Empty, Pending, Confirmed, Updating, Committing, Live = Value;
+ }
+
+}
+
+class Repository(val root: File) extends Closeable {
+ import Repository.State.{Value => State, _};
+
+ val livedir = new File(root, "live");
+ val livereposdir = new File(livedir, "repos");
+ val newdir = new File(root, "new");
+ val olddir = new File(root, "old");
+ val pendingdir = new File(root, "pending");
+ val tmpdir = new File(root, "tmp");
+
+ val lock = {
+ if (!root.isDirectory && !root.mkdir()) ???;
+ val chan = new FileOutputStream(new File(root, "lk")).getChannel;
+ chan.tryLock() match {
+ case null =>
+ throw new IOException(s"repository `${root.getPath}' locked")
+ case lk => lk
+ }
+ }
+
+ def close() {
+ lock.release();
+ lock.channel.close();
+ }
+
+ def state: State =
+ if (livedir.isDirectory) {
+ if (!livereposdir.isDirectory) Confirmed
+ else if (newdir.isDirectory && olddir.isDirectory) Committing
+ else Live
+ } else {
+ if (newdir.isDirectory) Updating
+ else if (pendingdir.isDirectory) Pending
+ else Empty
+ }
+
+ def commitState(): State = state match {
+ case Updating => rmTree(newdir); state
+ case Committing =>
+ if (!newdir.renameTo(livedir) && !olddir.renameTo(livedir))
+ throw new IOException("failed to commit update");
+ state
+ case st => st;
+
+ def clean() {
+
+}
+
+/*----- That's all, folks -------------------------------------------------*/
+
+}