From 1908e1ee0dd8174f1b0f35c8d608c8e54354f1de Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 19 Dec 2023 12:20:31 -0300 Subject: [PATCH] revamp core api + option to use nostr-wasm instead of noble-curves. --- bun.lockb | Bin 129997 -> 130355 bytes core.test.ts | 293 +++++++++++++++++++++++++++++++++++++++++++ core.ts | 50 ++++++++ event.test.ts | 339 -------------------------------------------------- event.ts | 99 --------------- keys.test.ts | 19 --- keys.ts | 10 -- package.json | 3 +- pure.ts | 59 +++++++++ wasm.ts | 38 ++++++ 10 files changed, 442 insertions(+), 468 deletions(-) create mode 100644 core.test.ts create mode 100644 core.ts delete mode 100644 event.test.ts delete mode 100644 event.ts delete mode 100644 keys.test.ts delete mode 100644 keys.ts create mode 100644 pure.ts create mode 100644 wasm.ts diff --git a/bun.lockb b/bun.lockb index 4fdf93f191ef8cd59cb2a49d98353593d5acd41b..62dc90de35dd76079108148e09a1a1cd3bbef12b 100755 GIT binary patch delta 22644 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 DXZgGK delta 22625 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 jTCdB<*uH&?JYDBX{&maUi&cAN^Fx< diff --git a/core.test.ts b/core.test.ts new file mode 100644 index 0000000..2b9aba3 --- /dev/null +++ b/core.test.ts @@ -0,0 +1,293 @@ +import { describe, test, expect } from 'bun:test' + +import { + finalizeEvent, + serializeEvent, + getEventHash, + validateEvent, + verifyEvent, + verifiedSymbol, + getPublicKey, + generateSecretKey, +} from './pure.ts' +import { ShortTextNote } from './kinds.ts' +import { hexToBytes } from '@noble/hashes/utils' + +test('private key generation', () => { + expect(generateSecretKey()).toMatch(/[a-f0-9]{64}/) +}) + +test('public key generation', () => { + expect(getPublicKey(generateSecretKey())).toMatch(/[a-f0-9]{64}/) +}) + +test('public key from private key deterministic', () => { + let sk = generateSecretKey() + let pk = getPublicKey(sk) + + for (let i = 0; i < 5; i++) { + expect(getPublicKey(sk)).toEqual(pk) + } +}) + +describe('finishEvent', () => { + test('should create a signed event from a template', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const template = { + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + } + + const event = finalizeEvent(template, privateKey) + + expect(event.kind).toEqual(template.kind) + expect(event.tags).toEqual(template.tags) + expect(event.content).toEqual(template.content) + expect(event.created_at).toEqual(template.created_at) + expect(event.pubkey).toEqual(publicKey) + expect(typeof event.id).toEqual('string') + expect(typeof event.sig).toEqual('string') + }) +}) + +describe('serializeEvent', () => { + test('should serialize a valid event object', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const unsignedEvent = { + pubkey: publicKey, + created_at: 1617932115, + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + } + + const serializedEvent = serializeEvent(unsignedEvent) + + expect(serializedEvent).toEqual( + JSON.stringify([ + 0, + publicKey, + unsignedEvent.created_at, + unsignedEvent.kind, + unsignedEvent.tags, + unsignedEvent.content, + ]), + ) + }) + + test('should throw an error for an invalid event object', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const invalidEvent = { + kind: ShortTextNote, + tags: [], + created_at: 1617932115, + pubkey: publicKey, // missing content + } + + expect(() => { + // @ts-expect-error + serializeEvent(invalidEvent) + }).toThrow("can't serialize event with wrong or missing properties") + }) +}) + +describe('getEventHash', () => { + test('should return the correct event hash', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const unsignedEvent = { + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + pubkey: publicKey, + } + + const eventHash = getEventHash(unsignedEvent) + + expect(typeof eventHash).toEqual('string') + expect(eventHash.length).toEqual(64) + }) +}) + +describe('validateEvent', () => { + test('should return true for a valid event object', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const unsignedEvent = { + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + pubkey: publicKey, + } + + const isValid = validateEvent(unsignedEvent) + + expect(isValid).toEqual(true) + }) + + test('should return false for a non object event', () => { + const nonObjectEvent = '' + const isValid = validateEvent(nonObjectEvent) + expect(isValid).toEqual(false) + }) + + test('should return false for an event object with missing properties', () => { + const invalidEvent = { + kind: ShortTextNote, + tags: [], + created_at: 1617932115, // missing content and pubkey + } + + const isValid = validateEvent(invalidEvent) + + expect(isValid).toEqual(false) + }) + + test('should return false for an empty object', () => { + const emptyObj = {} + + const isValid = validateEvent(emptyObj) + + expect(isValid).toEqual(false) + }) + + test('should return false for an object with invalid properties', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const invalidEvent = { + kind: 1, + tags: [], + created_at: '1617932115', // should be a number + pubkey: publicKey, + } + + const isValid = validateEvent(invalidEvent) + + expect(isValid).toEqual(false) + }) + + test('should return false for an object with an invalid public key', () => { + const invalidEvent = { + kind: 1, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + pubkey: 'invalid_pubkey', + } + + const isValid = validateEvent(invalidEvent) + + expect(isValid).toEqual(false) + }) + + test('should return false for an object with invalid tags', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const publicKey = getPublicKey(privateKey) + + const invalidEvent = { + kind: 1, + tags: {}, // should be an array + content: 'Hello, world!', + created_at: 1617932115, + pubkey: publicKey, + } + + const isValid = validateEvent(invalidEvent) + + expect(isValid).toEqual(false) + }) +}) + +describe('verifySignature', () => { + test('should return true for a valid event signature', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const event = finalizeEvent( + { + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + }, + privateKey, + ) + + const isValid = verifyEvent(event) + expect(isValid).toEqual(true) + }) + + test('should return false for an invalid event signature', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + const { [verifiedSymbol]: _, ...event } = finalizeEvent( + { + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + }, + privateKey, + ) + + // tamper with the signature + event.sig = event.sig.replace(/^.{3}/g, '666') + + const isValid = verifyEvent(event) + expect(isValid).toEqual(false) + }) + + test('should return false when verifying an event with a different private key', () => { + const privateKey1 = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + + const privateKey2 = hexToBytes('5b4a34f4e4b23c63ad55a35e3f84a3b53d96dbf266edf521a8358f71d19cbf67') + const publicKey2 = getPublicKey(privateKey2) + + const { [verifiedSymbol]: _, ...event } = finalizeEvent( + { + kind: ShortTextNote, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + }, + privateKey1, + ) + + // verify with different private key + const isValid = verifyEvent({ + ...event, + pubkey: publicKey2, + }) + expect(isValid).toEqual(false) + }) + + test('should return false for an invalid event id', () => { + const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf') + + const { [verifiedSymbol]: _, ...event } = finalizeEvent( + { + kind: 1, + tags: [], + content: 'Hello, world!', + created_at: 1617932115, + }, + privateKey, + ) + + // tamper with the id + event.id = event.id.replace(/^.{3}/g, '666') + + const isValid = verifyEvent(event) + expect(isValid).toEqual(false) + }) +}) diff --git a/core.ts b/core.ts new file mode 100644 index 0000000..462e8ce --- /dev/null +++ b/core.ts @@ -0,0 +1,50 @@ +export interface Nostr { + generateSecretKey(): Uint8Array + getPublicKey(secretKey: Uint8Array): string + finalizeEvent(event: EventTemplate, secretKey: Uint8Array): VerifiedEvent + verifyEvent(event: Event): event is VerifiedEvent +} + +/** Designates a verified event signature. */ +export const verifiedSymbol = Symbol('verified') + +export interface Event { + kind: number + tags: string[][] + content: string + created_at: number + pubkey: string + id: string + sig: string + [verifiedSymbol]?: boolean +} + +export type EventTemplate = Pick +export type UnsignedEvent = Pick + +/** An event whose signature has been verified. */ +export interface VerifiedEvent extends Event { + [verifiedSymbol]: true +} + +const isRecord = (obj: unknown): obj is Record => obj instanceof Object + +export function validateEvent(event: T): event is T & UnsignedEvent { + if (!isRecord(event)) return false + if (typeof event.kind !== 'number') return false + if (typeof event.content !== 'string') return false + if (typeof event.created_at !== 'number') return false + if (typeof event.pubkey !== 'string') return false + if (!event.pubkey.match(/^[a-f0-9]{64}$/)) return false + + if (!Array.isArray(event.tags)) return false + for (let i = 0; i < event.tags.length; i++) { + let tag = event.tags[i] + if (!Array.isArray(tag)) return false + for (let j = 0; j < tag.length; j++) { + if (typeof tag[j] === 'object') return false + } + } + + return true +} diff --git a/event.test.ts b/event.test.ts deleted file mode 100644 index 42352c6..0000000 --- a/event.test.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { describe, test, expect } from 'bun:test' - -import { - finishEvent, - serializeEvent, - getEventHash, - validateEvent, - verifySignature, - getSignature, - verifiedSymbol, -} from './event.ts' -import { getPublicKey } from './keys.ts' -import { ShortTextNote } from './kinds.ts' - -describe('Event', () => { - describe('finishEvent', () => { - test('should create a signed event from a template', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const template = { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - } - - const event = finishEvent(template, privateKey) - - expect(event.kind).toEqual(template.kind) - expect(event.tags).toEqual(template.tags) - expect(event.content).toEqual(template.content) - expect(event.created_at).toEqual(template.created_at) - expect(event.pubkey).toEqual(publicKey) - expect(typeof event.id).toEqual('string') - expect(typeof event.sig).toEqual('string') - }) - }) - - describe('serializeEvent', () => { - test('should serialize a valid event object', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const unsignedEvent = { - pubkey: publicKey, - created_at: 1617932115, - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - } - - const serializedEvent = serializeEvent(unsignedEvent) - - expect(serializedEvent).toEqual( - JSON.stringify([ - 0, - publicKey, - unsignedEvent.created_at, - unsignedEvent.kind, - unsignedEvent.tags, - unsignedEvent.content, - ]), - ) - }) - - test('should throw an error for an invalid event object', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const invalidEvent = { - kind: ShortTextNote, - tags: [], - created_at: 1617932115, - pubkey: publicKey, // missing content - } - - expect(() => { - // @ts-expect-error - serializeEvent(invalidEvent) - }).toThrow("can't serialize event with wrong or missing properties") - }) - }) - - describe('getEventHash', () => { - test('should return the correct event hash', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const unsignedEvent = { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - pubkey: publicKey, - } - - const eventHash = getEventHash(unsignedEvent) - - expect(typeof eventHash).toEqual('string') - expect(eventHash.length).toEqual(64) - }) - }) - - describe('validateEvent', () => { - test('should return true for a valid event object', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const unsignedEvent = { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - pubkey: publicKey, - } - - const isValid = validateEvent(unsignedEvent) - - expect(isValid).toEqual(true) - }) - - test('should return false for a non object event', () => { - const nonObjectEvent = '' - const isValid = validateEvent(nonObjectEvent) - expect(isValid).toEqual(false) - }) - - test('should return false for an event object with missing properties', () => { - const invalidEvent = { - kind: ShortTextNote, - tags: [], - created_at: 1617932115, // missing content and pubkey - } - - const isValid = validateEvent(invalidEvent) - - expect(isValid).toEqual(false) - }) - - test('should return false for an empty object', () => { - const emptyObj = {} - - const isValid = validateEvent(emptyObj) - - expect(isValid).toEqual(false) - }) - - test('should return false for an object with invalid properties', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const invalidEvent = { - kind: 1, - tags: [], - created_at: '1617932115', // should be a number - pubkey: publicKey, - } - - const isValid = validateEvent(invalidEvent) - - expect(isValid).toEqual(false) - }) - - test('should return false for an object with an invalid public key', () => { - const invalidEvent = { - kind: 1, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - pubkey: 'invalid_pubkey', - } - - const isValid = validateEvent(invalidEvent) - - expect(isValid).toEqual(false) - }) - - test('should return false for an object with invalid tags', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const invalidEvent = { - kind: 1, - tags: {}, // should be an array - content: 'Hello, world!', - created_at: 1617932115, - pubkey: publicKey, - } - - const isValid = validateEvent(invalidEvent) - - expect(isValid).toEqual(false) - }) - }) - - describe('verifySignature', () => { - test('should return true for a valid event signature', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - - const event = finishEvent( - { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - }, - privateKey, - ) - - const isValid = verifySignature(event) - - expect(isValid).toEqual(true) - }) - - test('should return false for an invalid event signature', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - - const { [verifiedSymbol]: _, ...event } = finishEvent( - { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - }, - privateKey, - ) - - // tamper with the signature - event.sig = event.sig.replace(/^.{3}/g, '666') - - const isValid = verifySignature(event) - - expect(isValid).toEqual(false) - }) - - test('should return false when verifying an event with a different private key', () => { - const privateKey1 = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - - const privateKey2 = '5b4a34f4e4b23c63ad55a35e3f84a3b53d96dbf266edf521a8358f71d19cbf67' - const publicKey2 = getPublicKey(privateKey2) - - const { [verifiedSymbol]: _, ...event } = finishEvent( - { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - }, - privateKey1, - ) - - // verify with different private key - const isValid = verifySignature({ - ...event, - pubkey: publicKey2, - }) - - expect(isValid).toEqual(false) - }) - - test('should return false for an invalid event id', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - - const { [verifiedSymbol]: _, ...event } = finishEvent( - { - kind: 1, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - }, - privateKey, - ) - - // tamper with the id - event.id = event.id.replace(/^.{3}/g, '666') - - const isValid = verifySignature(event) - - expect(isValid).toEqual(false) - }) - }) - - describe('getSignature', () => { - test('should produce the correct signature for an event object', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const unsignedEvent = { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - pubkey: publicKey, - } - - const sig = getSignature(unsignedEvent, privateKey) - - // verify the signature - const isValid = verifySignature({ - ...unsignedEvent, - id: getEventHash(unsignedEvent), - sig, - }) - - expect(typeof sig).toEqual('string') - expect(sig.length).toEqual(128) - expect(isValid).toEqual(true) - }) - - test('should not sign an event with different private key', () => { - const privateKey = 'd217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf' - const publicKey = getPublicKey(privateKey) - - const wrongPrivateKey = 'a91e2a9d9e0f70f0877bea0dbf034e8f95d7392a27a7f07da0d14b9e9d456be7' - - const unsignedEvent = { - kind: ShortTextNote, - tags: [], - content: 'Hello, world!', - created_at: 1617932115, - pubkey: publicKey, - } - - const sig = getSignature(unsignedEvent, wrongPrivateKey) - - // verify the signature - // @ts-expect-error - const isValid = verifySignature({ - ...unsignedEvent, - sig, - }) - - expect(typeof sig).toEqual('string') - expect(sig.length).toEqual(128) - expect(isValid).toEqual(false) - }) - }) -}) diff --git a/event.ts b/event.ts deleted file mode 100644 index f31248a..0000000 --- a/event.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { schnorr } from '@noble/curves/secp256k1' -import { sha256 } from '@noble/hashes/sha256' -import { bytesToHex } from '@noble/hashes/utils' - -import { getPublicKey } from './keys.ts' -import { utf8Encoder } from './utils.ts' - -/** Designates a verified event signature. */ -export const verifiedSymbol = Symbol('verified') - -export interface Event { - kind: number - tags: string[][] - content: string - created_at: number - pubkey: string - id: string - sig: string - [verifiedSymbol]?: boolean -} - -export type EventTemplate = Pick -export type UnsignedEvent = Pick - -/** An event whose signature has been verified. */ -export interface VerifiedEvent extends Event { - [verifiedSymbol]: true -} - -export function finishEvent(t: EventTemplate, privateKey: string): VerifiedEvent { - const event = t as VerifiedEvent - event.pubkey = getPublicKey(privateKey) - event.id = getEventHash(event) - event.sig = getSignature(event, privateKey) - event[verifiedSymbol] = true - return event -} - -export function serializeEvent(evt: UnsignedEvent): string { - if (!validateEvent(evt)) throw new Error("can't serialize event with wrong or missing properties") - - return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]) -} - -export function getEventHash(event: UnsignedEvent): string { - let eventHash = sha256(utf8Encoder.encode(serializeEvent(event))) - return bytesToHex(eventHash) -} - -const isRecord = (obj: unknown): obj is Record => obj instanceof Object - -export function validateEvent(event: T): event is T & UnsignedEvent { - if (!isRecord(event)) return false - if (typeof event.kind !== 'number') return false - if (typeof event.content !== 'string') return false - if (typeof event.created_at !== 'number') return false - if (typeof event.pubkey !== 'string') return false - if (!event.pubkey.match(/^[a-f0-9]{64}$/)) return false - - if (!Array.isArray(event.tags)) return false - for (let i = 0; i < event.tags.length; i++) { - let tag = event.tags[i] - if (!Array.isArray(tag)) return false - for (let j = 0; j < tag.length; j++) { - if (typeof tag[j] === 'object') return false - } - } - - return true -} - -/** Verify the event's signature. This function mutates the event with a `verified` symbol, making it idempotent. */ -export function verifySignature(event: Event): boolean { - if (typeof event[verifiedSymbol] === 'boolean') return event[verifiedSymbol] - - const hash = getEventHash(event) - if (hash !== event.id) { - return (event[verifiedSymbol] = false) - } - - try { - return (event[verifiedSymbol] = schnorr.verify(event.sig, hash, event.pubkey)) - } catch (err) { - return (event[verifiedSymbol] = false) - } -} - -/** @deprecated Use `getSignature` instead. */ -export function signEvent(event: UnsignedEvent, key: string): string { - console.warn( - 'nostr-tools: `signEvent` is deprecated and will be removed or changed in the future. Please use `getSignature` instead.', - ) - return getSignature(event, key) -} - -/** Calculate the signature for an event. */ -export function getSignature(event: UnsignedEvent, key: string): string { - return bytesToHex(schnorr.sign(getEventHash(event), key)) -} diff --git a/keys.test.ts b/keys.test.ts deleted file mode 100644 index 05df427..0000000 --- a/keys.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { test, expect } from 'bun:test' -import { generatePrivateKey, getPublicKey } from './keys.ts' - -test('private key generation', () => { - expect(generatePrivateKey()).toMatch(/[a-f0-9]{64}/) -}) - -test('public key generation', () => { - expect(getPublicKey(generatePrivateKey())).toMatch(/[a-f0-9]{64}/) -}) - -test('public key from private key deterministic', () => { - let sk = generatePrivateKey() - let pk = getPublicKey(sk) - - for (let i = 0; i < 5; i++) { - expect(getPublicKey(sk)).toEqual(pk) - } -}) diff --git a/keys.ts b/keys.ts deleted file mode 100644 index 03edd1c..0000000 --- a/keys.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { schnorr } from '@noble/curves/secp256k1' -import { bytesToHex } from '@noble/hashes/utils' - -export function generatePrivateKey(): string { - return bytesToHex(schnorr.utils.randomPrivateKey()) -} - -export function getPublicKey(privateKey: string): string { - return bytesToHex(schnorr.getPublicKey(privateKey)) -} diff --git a/package.json b/package.json index d1ef8ed..efa395c 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,8 @@ "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", - "@scure/bip39": "1.2.1" + "@scure/bip39": "1.2.1", + "nostr-wasm": "v0.0.3" }, "peerDependencies": { "typescript": ">=5.0.0" diff --git a/pure.ts b/pure.ts new file mode 100644 index 0000000..e8af48d --- /dev/null +++ b/pure.ts @@ -0,0 +1,59 @@ +import { schnorr } from '@noble/curves/secp256k1' +import { bytesToHex } from '@noble/hashes/utils' +import { Nostr, Event, EventTemplate, UnsignedEvent, VerifiedEvent, verifiedSymbol, validateEvent } from './core' +import { sha256 } from '@noble/hashes/sha256' + +import { utf8Encoder } from './utils.ts' + +class JS implements Nostr { + generateSecretKey(): Uint8Array { + return schnorr.utils.randomPrivateKey() + } + getPublicKey(secretKey: Uint8Array): string { + return bytesToHex(schnorr.getPublicKey(secretKey)) + } + finalizeEvent(t: EventTemplate, secretKey: Uint8Array): VerifiedEvent { + const event = t as VerifiedEvent + event.pubkey = this.getPublicKey(secretKey) + event.id = getEventHash(event) + event.sig = bytesToHex(schnorr.sign(getEventHash(event), secretKey)) + event[verifiedSymbol] = true + return event + } + verifyEvent(event: Event): event is VerifiedEvent { + if (typeof event[verifiedSymbol] === 'boolean') return event[verifiedSymbol] + + const hash = getEventHash(event) + if (hash !== event.id) { + event[verifiedSymbol] = false + return false + } + + try { + const valid = schnorr.verify(event.sig, hash, event.pubkey) + event[verifiedSymbol] = valid + return valid + } catch (err) { + event[verifiedSymbol] = false + return false + } + } +} + +export function serializeEvent(evt: UnsignedEvent): string { + if (!validateEvent(evt)) throw new Error("can't serialize event with wrong or missing properties") + return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]) +} + +export function getEventHash(event: UnsignedEvent): string { + let eventHash = sha256(utf8Encoder.encode(serializeEvent(event))) + return bytesToHex(eventHash) +} + +const i = new JS() + +export const generateSecretKey = i.generateSecretKey +export const getPublicKey = i.getPublicKey +export const finalizeEvent = i.finalizeEvent +export const verifyEvent = i.verifyEvent +export * from './core.ts' diff --git a/wasm.ts b/wasm.ts new file mode 100644 index 0000000..e4c1fe6 --- /dev/null +++ b/wasm.ts @@ -0,0 +1,38 @@ +import { bytesToHex } from '@noble/hashes/utils' +import { Nostr as NostrWasm } from 'nostr-wasm' +import { EventTemplate, Event, Nostr, VerifiedEvent, verifiedSymbol } from './core' + +let nw: NostrWasm + +export function setNostrWasm(x: NostrWasm) { + nw = x +} + +class Wasm implements Nostr { + generateSecretKey(): Uint8Array { + return nw.generateSecretKey() + } + getPublicKey(secretKey: Uint8Array): string { + return bytesToHex(nw.getPublicKey(secretKey)) + } + finalizeEvent(t: EventTemplate, secretKey: Uint8Array): VerifiedEvent { + nw.finalizeEvent(t as any, secretKey) + return t as VerifiedEvent + } + verifyEvent(event: Event): event is VerifiedEvent { + try { + nw.verifyEvent(event) + event[verifiedSymbol] = true + return true + } catch (err) { + return false + } + } +} + +const i = new Wasm() +export const generateSecretKey = i.generateSecretKey +export const getPublicKey = i.getPublicKey +export const finalizeEvent = i.finalizeEvent +export const verifyEvent = i.verifyEvent +export * from './core.ts'