p2p-cljs / src / ird / p2p / identity.cljs
691 words
  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))))))

static mirror of HEAD ยท about ยท clone: git clone https://ird.cod-sursa.ro/p2p-cljs.git