concept-collection / commonview
commonview / src / p2p / identity.ts
96 lines · 2.7 KBBlameHistoryRaw
1import * as secp from '@noble/secp256k1'
3// The peer's identity is a secp256k1 / BIP340 (schnorr) keypair.
4// - The x-only public key (hex) IS the peer ID.
5// - The private key is persisted in localStorage so the identity survives reloads.
6// - The same key signs both nostr events (for relay discovery/signaling) and
7// every application-level message sent over WebRTC.
9const STORAGE_KEY = 'commonview:privkey'
11const toHex = (bytes: Uint8Array): string =>
12 bytes.reduce((s, b) => s + b.toString(16).padStart(2, '0'), '')
14const fromHex = (hex: string): Uint8Array => {
15 const out = new Uint8Array(hex.length / 2)
16 for (let i = 0; i < out.length; i++) {
17 out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
18 }
19 return out
22const loadOrCreateSecretKey = (): Uint8Array => {
23 const existing = localStorage.getItem(STORAGE_KEY)
24 if (existing && existing.length === 64) {
25 return fromHex(existing)
26 }
27 const {secretKey} = secp.schnorr.keygen()
28 localStorage.setItem(STORAGE_KEY, toHex(secretKey))
29 return secretKey
32const secretKey = loadOrCreateSecretKey()
33const publicKey = secp.schnorr.getPublicKey(secretKey)
35/** This peer's ID = its x-only public key, as hex. */
36export const selfId: string = toHex(publicKey)
38const sha256 = async (str: string): Promise<Uint8Array> =>
39 new Uint8Array(
40 await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str))
41 )
43/** Sign an arbitrary string payload; returns hex signature. */
44export const sign = async (payload: string): Promise<string> =>
45 toHex(await secp.schnorr.signAsync(await sha256(payload), secretKey))
47/** Verify a hex signature over a string payload against a peer's ID (pubkey hex). */
48export const verify = async (
49 payload: string,
50 sigHex: string,
51 pubkeyHex: string
52): Promise<boolean> => {
53 try {
54 return await secp.schnorr.verifyAsync(
55 fromHex(sigHex),
56 await sha256(payload),
57 fromHex(pubkeyHex)
58 )
59 } catch {
60 return false
61 }
64// ---- nostr event signing (schnorr over the nostr event id) ----
66export interface NostrEvent {
67 id: string
68 pubkey: string
69 created_at: number
70 kind: number
71 tags: string[][]
72 content: string
73 sig: string
76/** Build and sign a nostr event with this peer's key. */
77export const makeNostrEvent = async (
78 kind: number,
79 tags: string[][],
80 content: string
81): Promise<NostrEvent> => {
82 const created_at = Math.floor(Date.now() / 1000)
83 const serialized = JSON.stringify([
84 0,
85 selfId,
86 created_at,
87 kind,
88 tags,
89 content
90 ])
91 const id = toHex(await sha256(serialized))
92 const sig = toHex(await secp.schnorr.signAsync(fromHex(id), secretKey))
93 return {id, pubkey: selfId, created_at, kind, tags, content, sig}
96export {toHex, fromHex}