shake it all up
[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
30import java.io.{Closeable, File, FileOutputStream, FileReader, IOException};
31
32import scala.collection.mutable.HashMap;
33
34/*----- Useful regular expressions ----------------------------------------*/
35
36val RX_COMMENT = """(?x) ^ \s* (?: \# .* )? $""".r;
37val RX_KEYVAL = """(?x) ^ \s*
38 ([-\w]+)
39 (?:\s+(?!=)|\s*=\s*)
40 (|\S|\S.*\S)
41 \s* $""".r;
42val RX_DOLLARSUBST = """(?x) \$ \{ ([-\w]+) \}""".r;
43
44/*----- Things that go wrong ----------------------------------------------*/
45
46class ConfigSyntaxError(val file: String, val lno: Int, val msg: String)
47 extends Exception {
48 override def getMessage(): String = s"$file:$lno: $msg";
49}
50
51class 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
59class DefaultFailed(val key: String) extends Exception;
60
61/*----- Parsing a configuration -------------------------------------------*/
62
63type Config = scala.collection.Map[String, String];
64
65val 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
103def 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
166object Repository {
167 object State extends Enumeration {
168 val Empty, Pending, Confirmed, Updating, Committing, Live = Value;
169 }
170
171}
172
173class 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}