mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-08 16:28:49 +00:00
revamp core api + option to use nostr-wasm instead of noble-curves.
This commit is contained in:
293
core.test.ts
Normal file
293
core.test.ts
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
50
core.ts
Normal file
50
core.ts
Normal file
@@ -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<Event, 'kind' | 'tags' | 'content' | 'created_at'>
|
||||||
|
export type UnsignedEvent = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at' | 'pubkey'>
|
||||||
|
|
||||||
|
/** An event whose signature has been verified. */
|
||||||
|
export interface VerifiedEvent extends Event {
|
||||||
|
[verifiedSymbol]: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRecord = (obj: unknown): obj is Record<string, unknown> => obj instanceof Object
|
||||||
|
|
||||||
|
export function validateEvent<T>(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
|
||||||
|
}
|
||||||
339
event.test.ts
339
event.test.ts
@@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
99
event.ts
99
event.ts
@@ -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<Event, 'kind' | 'tags' | 'content' | 'created_at'>
|
|
||||||
export type UnsignedEvent = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at' | 'pubkey'>
|
|
||||||
|
|
||||||
/** 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<string, unknown> => obj instanceof Object
|
|
||||||
|
|
||||||
export function validateEvent<T>(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))
|
|
||||||
}
|
|
||||||
19
keys.test.ts
19
keys.test.ts
@@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
10
keys.ts
10
keys.ts
@@ -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))
|
|
||||||
}
|
|
||||||
@@ -152,7 +152,8 @@
|
|||||||
"@noble/hashes": "1.3.1",
|
"@noble/hashes": "1.3.1",
|
||||||
"@scure/base": "1.1.1",
|
"@scure/base": "1.1.1",
|
||||||
"@scure/bip32": "1.3.1",
|
"@scure/bip32": "1.3.1",
|
||||||
"@scure/bip39": "1.2.1"
|
"@scure/bip39": "1.2.1",
|
||||||
|
"nostr-wasm": "v0.0.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": ">=5.0.0"
|
"typescript": ">=5.0.0"
|
||||||
|
|||||||
59
pure.ts
Normal file
59
pure.ts
Normal file
@@ -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'
|
||||||
38
wasm.ts
Normal file
38
wasm.ts
Normal file
@@ -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'
|
||||||
Reference in New Issue
Block a user