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
118
119
120
121
122
123
124
125
126
127
128
129
130
131 | (ns ird.p2p.identity
"Optional long-lived identities (PROTOCOL.md \"Identity\"): a 32-byte
Ed25519 seed (43 URL-safe base64 characters) that signs a binding of the
current session to the room, so peers recognise each other across
sessions by fingerprint. Identity never affects membership โ only the
room secret does.
Also invite links (the secret in the URL fragment, which URL loaders
never send to a server) and fingerprints: SHA-256 of the public key as
lowercase hex and as four emoji from the 64-entry table below."
(:require [clojure.string :as str]
[cljs.core.async :as async :refer [go <!]]
[ird.p2p.async :as a :refer [go-try <?]]
[ird.p2p.crypto :as crypto]
[ird.p2p.errors :as err]
[ird.p2p.impl.bytes :as bytes]
[ird.p2p.impl.webcrypto :as wc]))
;; ---- fingerprints -----------------------------------------------------------------
(def emoji-table
"The 64-entry table is part of the wire format; several entries carry a
variation selector (U+FE0F) and must stay exactly as they are."
["๐ข" "๐ฆ" "๐ผ" "๐ฆ" "๐ธ" "๐" "๐ฆ" "๐" "๐ฌ" "๐ฆ" "๐" "๐ฆ" "๐ต" "๐ฒ" "๐" "๐"
"๐ป" "๐" "โญ" "๐ฅ" "๐" "โก" "โ๏ธ" "๐" "๐" "๐" "๐" "๐" "๐ฅ" "๐ฝ" "๐ฅ" "๐ฉ"
"๐" "๐ฅจ" "๐ง" "๐ฟ" "โ" "๐ต" "๐ง" "๐" "๐ฒ" "๐ฏ" "๐ธ" "๐บ" "๐ฅ" "๐" "๐ฒ" "โต"
"๐" "๐ฟ" "๐๏ธ" "๐" "๐งฒ" "๐งญ" "โณ" "๐" "๐๏ธ" "๐" "โ๏ธ" "๐" "๐" "๐ก๏ธ" "โ๏ธ" "๐"])
(defn fingerprint
"{:hex <64 lowercase hex> :emoji <4 emoji>} of PUBLIC-KEY (URL-safe
base64, 32 bytes); nil when the key is malformed. The emoji index the
digest's first 24 bits in 6-bit groups."
[public-key-b64url]
(when-let [pub (bytes/b64url->b64 public-key-b64url)]
(when (= 32 (bytes/byte-length pub))
(let [hex (wc/sha256-hex pub)
bits (bytes/hex->int (subs hex 0 6))
at (fn [shift] (nth emoji-table (bit-and (bit-shift-right bits shift) 63)))]
{:hex hex
:emoji (str (at 18) (at 12) (at 6) (at 0))}))))
;; ---- identities ----------------------------------------------------------------------
(defn- check-seed [seed]
(when-not (and (bytes/valid-b64url? seed) (= 32 (bytes/b64url-byte-length seed)))
(throw (err/config-error "malformed identity seed: expected 32 bytes as URL-safe base64" {}))))
(defn from-seed
"The identity of SEED (URL-safe base64, 32 bytes): {:seed :public-key
:fingerprint :name} where :name is the optional announcement taken from
OPTS. Other keys in OPTS (such as the :hue/:glyph display opinions some
peers announce) are ignored, never announced and never an error: they are
unsigned, and this SDK leaves them to app payloads. Channel; a config
error for a malformed seed."
([seed] (from-seed seed nil))
([seed {:keys [name]}]
(go-try
(check-seed seed)
(let [pub (<! (wc/ed25519-public-key seed))]
(when-not pub (throw (err/config-error "malformed identity seed" {})))
(cond-> {:seed seed :public-key pub :fingerprint (fingerprint pub)}
(some? name) (assoc :name name))))))
(defn generate
"A fresh identity (random seed). Channel."
([] (generate nil))
([opts] (from-seed (bytes/random-b64url 32) opts)))
(defn binding-string
"The signed binding `sueta-id|v2|<appSalt>|<roomId>|<peerId>`. The tag is a
fixed literal shared with earlier implementations."
[app-salt room-id peer-id]
(str "sueta-id|v2|" app-salt "|" room-id "|" peer-id))
(defn assert-session
"Sign the binding of this session to the room: the assertion's `sig`,
standard base64. Channel."
[{:keys [seed]} app-salt room-id peer-id]
(wc/ed25519-sign seed (bytes/utf8->b64 (binding-string app-salt room-id peer-id))))
(defn- normalize-sig
"Accept the signature in standard or URL-safe base64."
[sig]
(when (string? sig)
(bytes/b64url->b64 (bytes/b64->b64url sig))))
(defn verify-assertion
"Verify a peer's assertion; the fingerprint of the key on success, nil
otherwise. Channel."
[app-salt room-id peer-id public-key-b64url signature]
(go
(when-let [sig (normalize-sig signature)]
(when (<! (wc/ed25519-verify public-key-b64url sig (bytes/utf8->b64 (binding-string app-salt room-id peer-id))))
(fingerprint public-key-b64url)))))
(defn sign-raw
"Raw Ed25519 signature of DATA-B64 (URL-safe base64 signature). Channel."
[{:keys [seed]} data-b64]
(go (some-> (<! (wc/ed25519-sign seed data-b64)) bytes/b64->b64url)))
(defn verify-raw
"True when SIGNATURE (URL-safe base64) signs DATA-B64 under the public key. Channel."
[public-key-b64url signature-b64url data-b64]
(go
(if-let [sig (bytes/b64url->b64 signature-b64url)]
(<! (wc/ed25519-verify public-key-b64url sig data-b64))
false)))
(defn identity-block
"The `id` block this identity announces in a hello, once signed for the
session. Channel of {:pub :sig :name?}; :name is omitted when nil."
[{:keys [public-key name] :as id} app-salt room-id peer-id]
(go
(cond-> {:pub public-key :sig (<! (assert-session id app-salt room-id peer-id))}
(some? name) (assoc :name name))))
;; ---- invite links -------------------------------------------------------------------------
(defn invite-url
"BASE with its fragment replaced by SECRET, e.g. `myapp://join#<secret>`."
[base secret]
(str (first (str/split base #"#" 2)) "#" secret))
(defn invite-secret
"The room secret in URL's fragment, if it is a valid room secret; nil otherwise."
[url]
(when (string? url)
(let [i (str/index-of url "#")]
(when i
(let [frag (subs url (inc i))]
(when (crypto/valid-secret? frag) frag))))))
|