(ns ird.p2p.db.identity
"The OrbitDB identity every holder of the secret shares. Write access to
the database is possession of the secret: the database's access list
names exactly one OrbitDB identity, and that identity is derived from the
secret, so whoever holds the secret can produce it and nobody else can.
An OrbitDB identity is two keys. The *identity key* gives the identity its
id (the lowercase hex of its public key) and signs the binding of the
second key; the *signing key* signs every entry. Both are Ed25519 keys
whose 32-byte seeds come from the key schedule (ird.p2p.db.crypto):
identity key seed = HKDF(\"db|<name>|identity\")
signing key seed = HKDF(\"db|<name>|identity|sign\")
so the same secret, app salt and name always yield the same identity,
on every peer, with nothing exchanged. The keys reach OrbitDB's keystore
as its raw Ed25519 form, seed ‖ public key (64 bytes), filed under
`keystore-label` (the identity key) and under the identity's id (the
signing key) — exactly where OrbitDB's `publickey` identity provider
looks for them. Everything here is strings: seeds and public keys as
URL-safe base64, raw keystore records as standard base64."
(:require [cljs.core.async :as async :refer [go <!]]
[ird.p2p.async :as a :refer [go-try <?]]
[ird.p2p.db.crypto :as crypto]
[ird.p2p.db.errors :as err]
[ird.p2p.impl.bytes :as bytes]
[ird.p2p.impl.webcrypto :as wc]))
(def keystore-label
"The keystore id under which the identity key is filed — the `id` this
library asks OrbitDB's identity provider for."
"ird-p2p-db")
(defn- hex-of [b64url] (bytes/b64->hex (bytes/b64url->b64 b64url)))
(defn from-secret
"The shared identity of the database: {:orbit-id <64 lowercase hex, the
identity key's public key> :signing-key <64 lowercase hex, the signing
key's public key> :id-seed :id-public-key :sign-seed :sign-public-key}
(seeds and keys as URL-safe base64, 32 bytes each). Channel; a config
error when the secret, salt or name is invalid."
[{:keys [secret app-salt name]}]
(go-try
(crypto/check-secret secret)
(when-not (string? app-salt)
(throw (err/config-error "app salt must be a string" {:app-salt app-salt})))
(crypto/check-name name)
(let [id-seed (<! (crypto/identity-seed secret app-salt name))
sign-seed (<! (crypto/signing-seed secret app-salt name))
id-pub (when id-seed (<! (wc/ed25519-public-key id-seed)))
sign-pub (when sign-seed (<! (wc/ed25519-public-key sign-seed)))]
(when-not (and id-pub sign-pub)
(throw (err/config-error "identity derivation failed" {})))
{:orbit-id (hex-of id-pub)
:signing-key (hex-of sign-pub)
:id-seed id-seed
:id-public-key id-pub
:sign-seed sign-seed
:sign-public-key sign-pub})))
(defn raw-key
"OrbitDB's raw Ed25519 private key form, seed ‖ public key (64 bytes), as
standard base64."
[seed-b64url public-key-b64url]
(bytes/concat-b64 (bytes/b64url->b64 seed-b64url) (bytes/b64url->b64 public-key-b64url)))
(defn keystore-records
"The two keystore records the impl files before asking OrbitDB for the
identity: [{:id <keystore id> :private-key <raw key, standard base64>} ...]
— the identity key under `keystore-label`, the signing key under the
identity's id."
[{:keys [orbit-id id-seed id-public-key sign-seed sign-public-key]}]
[{:id keystore-label :private-key (raw-key id-seed id-public-key)}
{:id orbit-id :private-key (raw-key sign-seed sign-public-key)}])