chiark / gitweb /
wip
[tripe-android] / keys.scala
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
26 package uk.org.distorted.tripe; package object keys {
27
28 /*----- Imports -----------------------------------------------------------*/
29
30 import java.io.{Closeable, File, FileOutputStream, FileReader, IOException};
31
32 import scala.collection.mutable.HashMap;
33
34 /*----- Useful regular expressions ----------------------------------------*/
35
36 val RX_COMMENT = """(?x) ^ \s* (?: \# .* )? $""".r;
37 val RX_KEYVAL = """(?x) ^ \s*
38       ([-\w]+)
39       (?:\s+(?!=)|\s*=\s*)
40       (|\S|\S.*\S)
41       \s* $""".r;
42 val RX_DOLLARSUBST = """(?x) \$ \{ ([-\w]+) \}""".r;
43
44 /*----- Things that go wrong ----------------------------------------------*/
45
46 class ConfigSyntaxError(val file: String, val lno: Int, val msg: String)
47         extends Exception {
48   override def getMessage(): String = s"$file:$lno: $msg";
49 }
50
51 class ConfigDefaultFailed(val file: String, val dfltkey: String,
52                           val badkey: String, val badval: String)
53         extends Exception {
54   override def getMessage(): String =
55     s"$file: can't default `$dfltkey' because " +
56           s"`$badval' is not a recognized value for `$badkey'";
57 }
58
59 class DefaultFailed(val key: String) extends Exception;
60
61 /*----- Parsing a configuration -------------------------------------------*/
62
63 type Config = scala.collection.Map[String, String];
64
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")
74       } },
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"
81       } },
82       "hash" -> { _ => "sha256" },
83       "mgf" -> { conf => conf("hash") + "-mgf" },
84       "mac" -> { conf => conf("bulk") match {
85         case "naclbox" => "poly1305/128"
86         case _ =>
87           val h = conf("hash");
88           JNI.hashsz(h) match {
89             case -1 => throw new DefaultFailed("hash")
90             case hsz => s"${h}-hmac/${4*hsz}"
91           }
92       } },
93       "sig" -> { conf => conf("kx") match {
94         case "dh" => "dsa"
95         case "ec" => "ecdsa"
96         case "x25519" => "ed25519"
97         case "x448" => "ed448"
98         case _ => throw new DefaultFailed("kx")
99       } },
100       "sig-fresh" -> { _ => "always" },
101       "fingerprint-hash" -> { _("hash") });
102
103 def readConfig(path: String): Config = {
104   var m = HashMap[String, String]();
105   withCleaner { clean =>
106     var in = new FileReader(path); clean { in.close(); }
107     var lno = 1;
108     for (line <- lines(in)) {
109       line match {
110         case RX_COMMENT() => ();
111         case RX_KEYVAL(key, value) => m += key -> value;
112         case _ =>
113           throw new ConfigSyntaxError(path, lno, "failed to parse line");
114       }
115       lno += 1;
116     }
117   }
118
119   for ((key, dflt) <- DEFAULTS) {
120     if (!(m contains key)) {
121       try { m += key -> dflt(m); }
122       catch {
123         case e: DefaultFailed =>
124           throw new ConfigDefaultFailed(path, key, e.key, m(e.key));
125       }
126     }
127   }
128   m
129 }
130
131 /*----- Managing a key repository -----------------------------------------*/
132
133 /* Lifecycle notes
134  *
135  *   -> empty
136  *
137  * insert config file via URL or something
138  *
139  *   -> pending (pending/tripe-keys.conf)
140  *
141  * verify master key fingerprint (against barcode?)
142  *
143  *   -> confirmed (live/tripe-keys.conf; no live/repos)
144  *   -> live (live/...)
145  *
146  * download package
147  * extract contents
148  * verify signature
149  * build keyrings
150  * build peer config
151  * rename tmp -> new
152  *
153  *   -> updating (live/...; new/...)
154  *
155  * rename old repository aside
156  *
157  *   -> committing (old/...; new/...)
158  *
159  * rename verified repository
160  *
161  *   -> live (live/...)
162  *
163  * (delete old/)
164  */
165
166 object Repository {
167   object State extends Enumeration {
168     val Empty, Pending, Confirmed, Updating, Committing, Live = Value;
169   }
170
171 }
172
173 class Repository(val root: File) extends Closeable {
174   import Repository.State.{Value => State, _};
175
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");
182
183   val lock = {
184     if (!root.isDirectory && !root.mkdir()) ???;
185     val chan = new FileOutputStream(new File(root, "lk")).getChannel;
186     chan.tryLock() match {
187       case null =>
188         throw new IOException(s"repository `${root.getPath}' locked")
189       case lk => lk
190     }
191   }
192
193   def close() {
194     lock.release();
195     lock.channel.close();
196   }
197
198   def state: State =
199     if (livedir.isDirectory) {
200       if (!livereposdir.isDirectory) Confirmed
201       else if (newdir.isDirectory && olddir.isDirectory) Committing
202       else Live
203     } else {
204       if (newdir.isDirectory) Updating
205       else if (pendingdir.isDirectory) Pending
206       else Empty
207     }
208
209   def commitState(): State = state match {
210     case Updating => rmTree(newdir); state
211     case Committing =>
212       if (!newdir.renameTo(livedir) && !olddir.renameTo(livedir))
213         throw new IOException("failed to commit update");
214       state
215     case st => st;
216
217   def clean() {
218         
219 }
220
221 /*----- That's all, folks -------------------------------------------------*/
222
223 }