Commit | Line | Data |
---|---|---|
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 | ||
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 | } |