1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
package org.meshbay.client.bridge
import org.json.JSONArray
import org.json.JSONObject
import org.meshbay.client.keys.DeviceKey
import org.meshbay.client.keys.Keyring
import org.meshbay.client.keys.SecretStore
import org.meshbay.client.keys.Secrets
/**
* The device key, the account's bundle key and its identity on every node —
* held here, never in the page (§8.2, §14.1 #20). The page is answered with
* public keys, signatures and agreements; it names what it signs by kind and
* fields, never by bytes (Transcripts). Arguments are checked as main.js does:
* ids are ids, keys are keys.
*
* `confirm` is a dialog this process draws, worded from the interface's own
* catalogues: what widens what leaves this device is never answered by the
* page.
*/
class KeyChannels(
private val secrets: Secrets,
private val confirm: (String) -> Boolean,
private val declined: () -> String,
) {
private val device = DeviceKey(secrets)
val keyring = Keyring(
load = {
val raw = secrets.read().optString(SecretStore.KEYRING_SLOT, "")
if (raw.isEmpty()) null else try { JSONObject(raw) } catch (e: Exception) { null }
},
save = { state -> secrets.update { it.put(SecretStore.KEYRING_SLOT, state.toString()) } },
)
// Only where the OS protects what is stored: an identity kept here and lost
// at the next start would leave a node pinning a key nobody holds, so
// without key storage the page keeps its keys the way a browser does.
private fun available() = secrets.backend() != "unavailable"
private fun needKeys() { if (!available()) throw Refused("No OS key storage") }
fun handles(channel: String) = channel.startsWith("keys:") || channel.startsWith("device:") ||
channel == "secrets:backend"
fun call(channel: String, a: JSONArray): Any? = when (channel) {
"secrets:backend" -> secrets.backend()
"device:ensure" -> device.ensure()
"device:public" -> device.publicKey()
"device:sign" -> device.sign(a.optString(0, ""))
"device:forget" -> device.forget()
"keys:available" -> available()
"keys:derive-session" -> {
needKeys()
val o = a.optJSONObject(0) ?: JSONObject()
keyring.deriveSession(
password = o.optString("password", ""), username = o.optString("username", ""),
userId = uid(o.opt("userId")), pepperB64 = o.optString("pepperB64", ""),
pepperVersion = o.optInt("pepperVersion", 1).takeIf { it != 0 } ?: 1,
pendingChange = o.optBoolean("pending", false))
}
"keys:commit-pending" -> keyring.commitPending(uid(a.opt(0)))
"keys:drop-pending" -> keyring.dropPending(uid(a.opt(0)))
"keys:has-session" -> available() && keyring.hasSession(uid(a.opt(0)))
"keys:forget-session" -> keyring.forgetSession(uid(a.opt(0)))
"keys:identity" -> keyring.identity(uid(a.opt(0)), npk(a.opt(1)))?.let {
JSONObject().put("pkEdB64", it.pkEdB64).put("pkXB64", it.pkXB64).put("sealedWith", it.sealedWith ?: JSONObject.NULL)
}
"keys:open-bundle" -> pub(keyring.openBundle(uid(a.opt(0)), npk(a.opt(1)),
bundleEnc = a.optJSONObject(2)?.optString("bundleEnc", "") ?: ""))
"keys:mint" -> { needKeys(); pub(keyring.mint(uid(a.opt(0)), npk(a.opt(1)))) }
"keys:seal-bundle" -> keyring.sealBundle(uid(a.opt(0)), npk(a.opt(1)),
usePending = a.optJSONObject(2)?.optBoolean("pending", false) ?: false).let {
JSONObject().put("bundle", it.bundle).put("fingerprint", it.fingerprint)
}
"keys:seal-recovery" -> keyring.sealRecovery(uid(a.opt(0)), npk(a.opt(1)), a.optString(2, ""), a.optString(3, ""))
"keys:mark-sealed" -> keyring.markSealed(uid(a.opt(0)), npk(a.opt(1)), a.optString(2, ""))
"keys:fingerprint" -> keyring.currentFingerprint(uid(a.opt(0)))
// By kind and fields: the page never names the bytes.
"keys:sign" -> keyring.signAs(uid(a.opt(0)), npk(a.opt(1)), a.optString(2, ""), a.optJSONObject(3) ?: JSONObject())
"keys:shared" -> keyring.shared(uid(a.opt(0)), npk(a.opt(1)), a.optString(2, ""))
"keys:playlist-key" -> keyring.playlistKey(uid(a.opt(0)))
"keys:browser-access" -> keyring.browserAccess(uid(a.opt(0)))
// Turning it on leaves this account's identities on every node, sealed
// for a browser: the person decides that here, in a dialog the page
// cannot answer. Turning it off only narrows.
"keys:set-browser-access" -> {
val id = uid(a.opt(0))
val on = a.optBoolean(1, false)
if (on && !keyring.browserAccess(id) && !confirm("native.browser_access_confirm")) throw Refused(declined())
keyring.setBrowserAccess(id, on)
}
// An account created on this device starts without browser access.
// Only ever narrows, so the page may say it.
"keys:created-here" -> keyring.setBrowserAccess(uid(a.opt(0)), false)
else -> throw Refused("Refused: no such channel")
}
private fun pub(p: Keyring.Pub) = JSONObject().put("pkEdB64", p.pkEdB64).put("pkXB64", p.pkXB64)
companion object {
private val UID = Regex("^[0-9a-f-]{36}$", RegexOption.IGNORE_CASE)
private val NPK = Regex("^[A-Za-z0-9+/=]{1,100}$")
fun uid(v: Any?): String {
val s = if (v == null || v == JSONObject.NULL) "" else v.toString()
if (!UID.matches(s)) throw Refused("Refused: not an account id")
return s
}
fun npk(v: Any?): String {
val s = if (v == null || v == JSONObject.NULL) "" else v.toString()
if (!NPK.matches(s)) throw Refused("Refused: not a node's key")
return s
}
}
}
|