cautpartener / client / src / lib / access.ts
 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
/**
 * 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.
 */

import { IPFSAccessController } from "@orbitdb/core";

interface CanAppendEntry {
  key?: string;
  identity?: string;
}

interface AccessControllerInstance {
  type: string;
  address: string;
  write: string[];
  canAppend(entry: CanAppendEntry): Promise<boolean>;
}

interface FactoryParams {
  orbitdb: unknown;
  identities: {
    getIdentity(hash: string): Promise<{ publicKey?: string } | undefined>;
  };
  name?: string;
  address?: string;
}

type InnerFactory = (params: FactoryParams) => Promise<AccessControllerInstance>;

/** Same call shape as IPFSAccessController({ write }); see module docs. */
export function HardenedIPFSAccessController(options?: { write?: string[] }): InnerFactory {
  const base = IPFSAccessController(options) as InnerFactory;
  const factory: InnerFactory = async (params) => {
    const ac = await base(params);
    return {
      ...ac,
      canAppend: async (entry) =>
        (await ac.canAppend(entry)) && (await signerMatchesIdentity(params.identities, entry)),
    };
  };
  return factory;
}

/** The one check upstream forgot: entry.key must be the identity's own key. */
export async function signerMatchesIdentity(
  identities: FactoryParams["identities"],
  entry: CanAppendEntry,
): Promise<boolean> {
  if (!entry.key || !entry.identity) return false;
  try {
    const ident = await identities.getIdentity(entry.identity);
    return !!ident && ident.publicKey === entry.key;
  } catch {
    return false;
  }
}

static mirror of HEAD · about · clone: git clone https://cod-sursa.cautpartener.ro/cautpartener.git