rooms-kit / src / ardegazu / rooms / lib / access.cljs
 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
;; ported-from: src/lib/access.ts @ v1.0.0
;;
;; Hardened IPFS access controller: closes an authorship-forgery gap in
;; @orbitdb/core's IPFSAccessController.
;;
;; Upstream, entry verification is split in two: `Entry.verify` checks the
;; signature against `entry.key` (the device signing key), and `canAppend` checks
;; that the identity referenced by `entry.identity` is internally consistent and
;; on the write list. NOTHING binds the two — a room member could craft an entry
;; that references a VICTIM's identity record while being signed with the
;; attacker's own key, and every replica would accept it and attribute it to the
;; victim (fingerprint badge included).
;;
;; This wrapper delegates to the vanilla controller and adds the missing check:
;; the entry's signing key must BE the referenced identity's signing key. Address
;; determinism is preserved — the AC address hashed into the DB manifest is
;; computed by the inner IPFSAccessController from {type:"ipfs", write} only, so
;; wrapped and vanilla clients open the SAME database and interop freely (never
;; change `write`, that would fork it).
;;
;; Caveat: we always open by db NAME, which routes through this factory. An open
;; by raw /orbitdb/... address would rebuild the vanilla controller and silently
;; skip the check — don't add such a path.
(ns ardegazu.rooms.lib.access
  (:require ["@orbitdb/core" :as orbit]
            [ardegazu.rooms.js :as j]
            [shadow.cljs.modern :refer (js-await)]))

(defn signer-matches-identity
  "The one check upstream forgot: entry.key must be the identity's own key."
  [identities entry]
  (let [k (unchecked-get entry "key")
        ref (unchecked-get entry "identity")]
    (if (or (not (j/truthy? k)) (not (j/truthy? ref)))
      (js/Promise.resolve false)
      (-> (js/Promise.resolve nil)
          (.then (fn [_]
                   (js-await [ident (.getIdentity ^js identities ref)]
                     (and (j/truthy? ident)
                          (identical? (unchecked-get ident "publicKey") k)))))
          (.catch (fn [_] false))))))

(defn HardenedIPFSAccessController
  "Same call shape as IPFSAccessController({ write }); see the namespace docs."
  [options]
  (let [base (orbit/IPFSAccessController options)]
    (fn [params]
      (js-await [ac (base params)]
        ;; TS `{ ...ac, canAppend }` — own enumerable props copied, canAppend
        ;; replaced. `type`, `address` and `write` therefore pass through
        ;; untouched, which is what keeps the manifest address identical.
        (let [out (js/Object.assign (js-obj) ac)
              identities (unchecked-get params "identities")]
          (unchecked-set
           out "canAppend"
           (fn [entry]
             (js-await [ok (.canAppend ^js ac entry)]
               (if-not (j/truthy? ok)
                 false
                 (signer-matches-identity identities entry)))))
          out)))))

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/rooms-kit.git