5 * (c) 2018 Straylight/Edgeware
8 /*----- Licensing notice --------------------------------------------------*
10 * This file is part of the Trivial IP Encryption (TrIPE) Android app.
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.
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
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/>.
26 package uk.org.distorted.tripe; package object keys {
28 /*----- Imports -----------------------------------------------------------*/
30 import java.io.{Closeable, File, FileOutputStream, FileReader, IOException};
32 import scala.collection.mutable.HashMap;
34 /*----- Useful regular expressions ----------------------------------------*/
36 val RX_COMMENT = """(?x) ^ \s* (?: \# .* )? $""".r;
37 val RX_KEYVAL = """(?x) ^ \s*
42 val RX_DOLLARSUBST = """(?x) \$ \{ ([-\w]+) \}""".r;
44 /*----- Things that go wrong ----------------------------------------------*/
46 class ConfigSyntaxError(val file: String, val lno: Int, val msg: String)
48 override def getMessage(): String = s"$file:$lno: $msg";
51 class ConfigDefaultFailed(val file: String, val dfltkey: String,
52 val badkey: String, val badval: String)
54 override def getMessage(): String =
55 s"$file: can't default `$dfltkey' because " +
56 s"`$badval' is not a recognized value for `$badkey'";
59 class DefaultFailed(val key: String) extends Exception;
61 /*----- Parsing a configuration -------------------------------------------*/
63 type Config = scala.collection.Map[String, String];
65 val DEFAULTS: Seq[(String, Config => String)] =
66 Seq("repos-base" -> { _ => "tripe-keys.tar.gz" },
67 "sig-base" -> { _ => "tripe-keys.sig-<SEQ>" },
68 "repos-url" -> { conf => conf("base-url") + conf("repos-base") },
69 "sig-url" -> { conf => conf("base-url") + conf("sig-base") },
70 "kx" -> { _ => "dh" },
71 "kx-genalg" -> { conf => conf("kx") match {
72 case alg@("dh" | "ec" | "x25519" | "x448") => alg
73 case _ => throw new DefaultFailed("kx")
75 "kx-expire" -> { _ => "now + 1 year" },
76 "kx-warn-days" -> { _ => "28" },
77 "bulk" -> { _ => "iiv" },
78 "cipher" -> { conf => conf("bulk") match {
79 case "naclbox" => "salsa20"
80 case _ => "rijndael-cbc"
82 "hash" -> { _ => "sha256" },
83 "mgf" -> { conf => conf("hash") + "-mgf" },
84 "mac" -> { conf => conf("bulk") match {
85 case "naclbox" => "poly1305/128"
89 case -1 => throw new DefaultFailed("hash")
90 case hsz => s"${h}-hmac/${4*hsz}"
93 "sig" -> { conf => conf("kx") match {
96 case "x25519" => "ed25519"
97 case "x448" => "ed448"
98 case _ => throw new DefaultFailed("kx")
100 "sig-fresh" -> { _ => "always" },
101 "fingerprint-hash" -> { _("hash") });
103 def readConfig(path: String): Config = {
104 var m = HashMap[String, String]();
105 withCleaner { clean =>
106 var in = new FileReader(path); clean { in.close(); }
108 for (line <- lines(in)) {
110 case RX_COMMENT() => ();
111 case RX_KEYVAL(key, value) => m += key -> value;
113 throw new ConfigSyntaxError(path, lno, "failed to parse line");
119 for ((key, dflt) <- DEFAULTS) {
120 if (!(m contains key)) {
121 try { m += key -> dflt(m); }
123 case e: DefaultFailed =>
124 throw new ConfigDefaultFailed(path, key, e.key, m(e.key));
131 /*----- Managing a key repository -----------------------------------------*/
137 * insert config file via URL or something
139 * -> pending (pending/tripe-keys.conf)
141 * verify master key fingerprint (against barcode?)
143 * -> confirmed (live/tripe-keys.conf; no live/repos)
153 * -> updating (live/...; new/...)
155 * rename old repository aside
157 * -> committing (old/...; new/...)
159 * rename verified repository
167 object State extends Enumeration {
168 val Empty, Pending, Confirmed, Updating, Committing, Live = Value;
173 class Repository(val root: File) extends Closeable {
174 import Repository.State.{Value => State, _};
176 val livedir = new File(root, "live");
177 val livereposdir = new File(livedir, "repos");
178 val newdir = new File(root, "new");
179 val olddir = new File(root, "old");
180 val pendingdir = new File(root, "pending");
181 val tmpdir = new File(root, "tmp");
184 if (!root.isDirectory && !root.mkdir()) ???;
185 val chan = new FileOutputStream(new File(root, "lk")).getChannel;
186 chan.tryLock() match {
188 throw new IOException(s"repository `${root.getPath}' locked")
195 lock.channel.close();
199 if (livedir.isDirectory) {
200 if (!livereposdir.isDirectory) Confirmed
201 else if (newdir.isDirectory && olddir.isDirectory) Committing
204 if (newdir.isDirectory) Updating
205 else if (pendingdir.isDirectory) Pending
209 def commitState(): State = state match {
210 case Updating => rmTree(newdir); state
212 if (!newdir.renameTo(livedir) && !olddir.renameTo(livedir))
213 throw new IOException("failed to commit update");
221 /*----- That's all, folks -------------------------------------------------*/