/ proof-of-concept / hitandrun-commonview
Sign in
proof-of-concept / hitandrun-commonview
hitandrun-commonview / src / p2p / identity.ts
100 lines · 2.9 KBCodeBlameHistory
40dabfdInitial commit: shared-view hit-and-run samplerJeremy Magland 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 = 'hitandrun-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/** SHA-256 of raw bytes, as hex (used to authenticate binary blobs). */
44export const sha256HexBytes = async (bytes: ArrayBuffer): Promise<string> =>
45 toHex(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)))
47/** Sign an arbitrary string payload; returns hex signature. */
48export const sign = async (payload: string): Promise<string> =>
49 toHex(await secp.schnorr.signAsync(await sha256(payload), secretKey))
51/** Verify a hex signature over a string payload against a peer's ID (pubkey hex). */
52export const verify = async (
53 payload: string,
54 sigHex: string,
55 pubkeyHex: string
56): Promise<boolean> => {
57 try {
58 return await secp.schnorr.verifyAsync(
59 fromHex(sigHex),
60 await sha256(payload),
61 fromHex(pubkeyHex)
62 )
63 } catch {
64 return false
65 }
68// ---- nostr event signing (schnorr over the nostr event id) ----
70export interface NostrEvent {
71 id: string
72 pubkey: string
73 created_at: number
74 kind: number
75 tags: string[][]
76 content: string
77 sig: string
80/** Build and sign a nostr event with this peer's key. */
81export const makeNostrEvent = async (
82 kind: number,
83 tags: string[][],
84 content: string
85): Promise<NostrEvent> => {
86 const created_at = Math.floor(Date.now() / 1000)
87 const serialized = JSON.stringify([
88 0,
89 selfId,
90 created_at,
91 kind,
92 tags,
93 content
94 ])
95 const id = toHex(await sha256(serialized))
96 const sig = toHex(await secp.schnorr.signAsync(fromHex(id), secretKey))
97 return {id, pubkey: selfId, created_at, kind, tags, content, sig}
100export {toHex, fromHex}
↑↓ move↵openescclose