mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-08 16:28:49 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb05ee188f | ||
|
|
fa9e169c46 | ||
|
|
bb1e3f2fa6 | ||
|
|
160987472f | ||
|
|
8b18341ebb | ||
|
|
901445dea1 | ||
|
|
91b67cd0d5 | ||
|
|
1e696e0f3b | ||
|
|
4b36848b2d | ||
|
|
3cb351a5f4 | ||
|
|
5db1934fa4 | ||
|
|
50c3f24b25 |
16
README.md
16
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Tools for developing [Nostr](https://github.com/fiatjaf/nostr) clients.
|
Tools for developing [Nostr](https://github.com/fiatjaf/nostr) clients.
|
||||||
|
|
||||||
Very lean on dependencies.
|
Only depends on _@scure_ and _@noble_ packages.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -30,11 +30,11 @@ let event = {
|
|||||||
kind: 1,
|
kind: 1,
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
tags: [],
|
tags: [],
|
||||||
content: 'hello'
|
content: 'hello',
|
||||||
|
pubkey: getPublicKey(privateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
event.id = getEventHash(event)
|
event.id = getEventHash(event)
|
||||||
event.pubkey = getPublicKey(privateKey)
|
|
||||||
event.sig = signEvent(event, privateKey)
|
event.sig = signEvent(event, privateKey)
|
||||||
|
|
||||||
let ok = validateEvent(event)
|
let ok = validateEvent(event)
|
||||||
@@ -102,13 +102,13 @@ event.sig = signEvent(event, sk)
|
|||||||
|
|
||||||
let pub = relay.publish(event)
|
let pub = relay.publish(event)
|
||||||
pub.on('ok', () => {
|
pub.on('ok', () => {
|
||||||
console.log(`{relay.url} has accepted our event`)
|
console.log(`${relay.url} has accepted our event`)
|
||||||
})
|
})
|
||||||
pub.on('seen', () => {
|
pub.on('seen', () => {
|
||||||
console.log(`we saw the event on {relay.url}`)
|
console.log(`we saw the event on ${relay.url}`)
|
||||||
})
|
})
|
||||||
pub.on('failed', reason => {
|
pub.on('failed', reason => {
|
||||||
console.log(`failed to publish to {relay.url}: ${reason}`)
|
console.log(`failed to publish to ${relay.url}: ${reason}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
await relay.close()
|
await relay.close()
|
||||||
@@ -182,7 +182,7 @@ let pk2 = getPublicKey(sk2)
|
|||||||
|
|
||||||
// on the sender side
|
// on the sender side
|
||||||
let message = 'hello'
|
let message = 'hello'
|
||||||
let ciphertext = await nip04.encrypt(sk1, pk2, 'hello')
|
let ciphertext = await nip04.encrypt(sk1, pk2, message)
|
||||||
|
|
||||||
let event = {
|
let event = {
|
||||||
kind: 4,
|
kind: 4,
|
||||||
@@ -196,7 +196,7 @@ sendEvent(event)
|
|||||||
|
|
||||||
// on the receiver side
|
// on the receiver side
|
||||||
sub.on('event', (event) => {
|
sub.on('event', (event) => {
|
||||||
let sender = event.tags.find(([k, v]) => k === 'p' && && v && v !== '')[1]
|
let sender = event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1]
|
||||||
pk1 === sender
|
pk1 === sender
|
||||||
let plaintext = await nip04.decrypt(sk2, pk1, event.content)
|
let plaintext = await nip04.decrypt(sk2, pk1, event.content)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ const {
|
|||||||
validateEvent,
|
validateEvent,
|
||||||
verifySignature,
|
verifySignature,
|
||||||
signEvent,
|
signEvent,
|
||||||
getEventHash,
|
|
||||||
getPublicKey
|
getPublicKey
|
||||||
} = require('./lib/nostr.cjs')
|
} = require('./lib/nostr.cjs')
|
||||||
|
|
||||||
@@ -35,15 +34,15 @@ test('validate event', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('check signature', async () => {
|
test('check signature', async () => {
|
||||||
expect(await verifySignature(event)).toBeTruthy()
|
expect(verifySignature(event)).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('sign event', async () => {
|
test('sign event', async () => {
|
||||||
let sig = await signEvent(unsigned, privateKey)
|
|
||||||
let hash = getEventHash(unsigned)
|
|
||||||
let pubkey = getPublicKey(privateKey)
|
let pubkey = getPublicKey(privateKey)
|
||||||
|
let authored = {...unsigned, pubkey}
|
||||||
|
|
||||||
let signed = {...unsigned, id: hash, sig, pubkey}
|
let sig = signEvent(authored, privateKey)
|
||||||
|
let signed = {...authored, sig}
|
||||||
|
|
||||||
expect(await verifySignature(signed)).toBeTruthy()
|
expect(verifySignature(signed)).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|||||||
19
event.ts
19
event.ts
@@ -3,7 +3,6 @@ import {sha256} from '@noble/hashes/sha256'
|
|||||||
|
|
||||||
import {utf8Encoder} from './utils'
|
import {utf8Encoder} from './utils'
|
||||||
|
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
export enum Kind {
|
export enum Kind {
|
||||||
Metadata = 0,
|
Metadata = 0,
|
||||||
@@ -17,7 +16,7 @@ export enum Kind {
|
|||||||
ChannelMetadata = 41,
|
ChannelMetadata = 41,
|
||||||
ChannelMessage = 42,
|
ChannelMessage = 42,
|
||||||
ChannelHideMessage = 43,
|
ChannelHideMessage = 43,
|
||||||
ChannelMuteUser = 44,
|
ChannelMuteUser = 44
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Event = {
|
export type Event = {
|
||||||
@@ -41,6 +40,9 @@ export function getBlankEvent(): Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function serializeEvent(evt: Event): string {
|
export function serializeEvent(evt: Event): string {
|
||||||
|
if (!validateEvent(evt))
|
||||||
|
throw new Error("can't serialize event with wrong or missing properties")
|
||||||
|
|
||||||
return JSON.stringify([
|
return JSON.stringify([
|
||||||
0,
|
0,
|
||||||
evt.pubkey,
|
evt.pubkey,
|
||||||
@@ -57,9 +59,10 @@ export function getEventHash(event: Event): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function validateEvent(event: Event): boolean {
|
export function validateEvent(event: Event): boolean {
|
||||||
if (event.id !== getEventHash(event)) return false
|
|
||||||
if (typeof event.content !== 'string') return false
|
if (typeof event.content !== 'string') return false
|
||||||
if (typeof event.created_at !== 'number') 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
|
if (!Array.isArray(event.tags)) return false
|
||||||
for (let i = 0; i < event.tags.length; i++) {
|
for (let i = 0; i < event.tags.length; i++) {
|
||||||
@@ -73,10 +76,12 @@ export function validateEvent(event: Event): boolean {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export function verifySignature(
|
export function verifySignature(event: Event & {sig: string}): boolean {
|
||||||
event: Event & {id: string; sig: string}
|
return secp256k1.schnorr.verifySync(
|
||||||
): boolean {
|
event.sig,
|
||||||
return secp256k1.schnorr.verifySync(event.sig, event.id, event.pubkey)
|
getEventHash(event),
|
||||||
|
event.pubkey
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function signEvent(event: Event, key: string): string {
|
export function signEvent(event: Event, key: string): string {
|
||||||
|
|||||||
10
nip04.ts
10
nip04.ts
@@ -1,6 +1,6 @@
|
|||||||
import {randomBytes} from '@noble/hashes/utils'
|
import {randomBytes} from '@noble/hashes/utils'
|
||||||
import * as secp256k1 from '@noble/secp256k1'
|
import * as secp256k1 from '@noble/secp256k1'
|
||||||
import {encode as b64encode, decode as b64decode} from 'base64-arraybuffer'
|
import {base64} from '@scure/base'
|
||||||
|
|
||||||
import {utf8Decoder, utf8Encoder} from './utils'
|
import {utf8Decoder, utf8Encoder} from './utils'
|
||||||
|
|
||||||
@@ -26,8 +26,8 @@ export async function encrypt(
|
|||||||
cryptoKey,
|
cryptoKey,
|
||||||
plaintext
|
plaintext
|
||||||
)
|
)
|
||||||
let ctb64 = b64encode(ciphertext)
|
let ctb64 = base64.encode(new Uint8Array(ciphertext))
|
||||||
let ivb64 = b64encode(iv.buffer)
|
let ivb64 = base64.encode(new Uint8Array(iv.buffer))
|
||||||
|
|
||||||
return `${ctb64}?iv=${ivb64}`
|
return `${ctb64}?iv=${ivb64}`
|
||||||
}
|
}
|
||||||
@@ -48,8 +48,8 @@ export async function decrypt(
|
|||||||
false,
|
false,
|
||||||
['decrypt']
|
['decrypt']
|
||||||
)
|
)
|
||||||
let ciphertext = b64decode(ctb64)
|
let ciphertext = base64.decode(ctb64)
|
||||||
let iv = b64decode(ivb64)
|
let iv = base64.decode(ivb64)
|
||||||
|
|
||||||
let plaintext = await crypto.subtle.decrypt(
|
let plaintext = await crypto.subtle.decrypt(
|
||||||
{name: 'AES-CBC', iv},
|
{name: 'AES-CBC', iv},
|
||||||
|
|||||||
15
nip06.test.js
Normal file
15
nip06.test.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* eslint-env jest */
|
||||||
|
const {nip06} = require('./lib/nostr.cjs')
|
||||||
|
|
||||||
|
test('generate private key from a mnemonic', async () => {
|
||||||
|
const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
|
||||||
|
const privateKey = nip06.privateKeyFromSeedWords(mnemonic)
|
||||||
|
expect(privateKey).toEqual('c26cf31d8ba425b555ca27d00ca71b5008004f2f662470f8c8131822ec129fe2')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generate private key from a mnemonic and passphrase', async () => {
|
||||||
|
const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
|
||||||
|
const passphrase = '123'
|
||||||
|
const privateKey = nip06.privateKeyFromSeedWords(mnemonic, passphrase)
|
||||||
|
expect(privateKey).toEqual('55a22b8203273d0aaf24c22c8fbe99608e70c524b17265641074281c8b978ae4')
|
||||||
|
})
|
||||||
4
nip06.ts
4
nip06.ts
@@ -7,8 +7,8 @@ import {
|
|||||||
} from '@scure/bip39'
|
} from '@scure/bip39'
|
||||||
import {HDKey} from '@scure/bip32'
|
import {HDKey} from '@scure/bip32'
|
||||||
|
|
||||||
export function privateKeyFromSeedWords(mnemonic: string): string {
|
export function privateKeyFromSeedWords(mnemonic: string, passphrase?: string): string {
|
||||||
let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic))
|
let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))
|
||||||
let privateKey = root.derive(`m/44'/1237'/0'/0/0`).privateKey
|
let privateKey = root.derive(`m/44'/1237'/0'/0/0`).privateKey
|
||||||
if (!privateKey) throw new Error('could not derive private key')
|
if (!privateKey) throw new Error('could not derive private key')
|
||||||
return secp256k1.utils.bytesToHex(privateKey)
|
return secp256k1.utils.bytesToHex(privateKey)
|
||||||
|
|||||||
12
nip19.ts
12
nip19.ts
@@ -1,8 +1,10 @@
|
|||||||
import * as secp256k1 from '@noble/secp256k1'
|
import * as secp256k1 from '@noble/secp256k1'
|
||||||
import {bech32} from 'bech32'
|
import {bech32} from '@scure/base'
|
||||||
|
|
||||||
import {utf8Decoder, utf8Encoder} from './utils'
|
import {utf8Decoder, utf8Encoder} from './utils'
|
||||||
|
|
||||||
|
const Bech32MaxSize = 5000
|
||||||
|
|
||||||
export type ProfilePointer = {
|
export type ProfilePointer = {
|
||||||
pubkey: string // hex
|
pubkey: string // hex
|
||||||
relays?: string[]
|
relays?: string[]
|
||||||
@@ -17,7 +19,7 @@ export function decode(nip19: string): {
|
|||||||
type: string
|
type: string
|
||||||
data: ProfilePointer | EventPointer | string
|
data: ProfilePointer | EventPointer | string
|
||||||
} {
|
} {
|
||||||
let {prefix, words} = bech32.decode(nip19, 1000)
|
let {prefix, words} = bech32.decode(nip19, Bech32MaxSize)
|
||||||
let data = new Uint8Array(bech32.fromWords(words))
|
let data = new Uint8Array(bech32.fromWords(words))
|
||||||
|
|
||||||
if (prefix === 'nprofile') {
|
if (prefix === 'nprofile') {
|
||||||
@@ -87,7 +89,7 @@ export function noteEncode(hex: string): string {
|
|||||||
function encodeBytes(prefix: string, hex: string): string {
|
function encodeBytes(prefix: string, hex: string): string {
|
||||||
let data = secp256k1.utils.hexToBytes(hex)
|
let data = secp256k1.utils.hexToBytes(hex)
|
||||||
let words = bech32.toWords(data)
|
let words = bech32.toWords(data)
|
||||||
return bech32.encode(prefix, words, 1000)
|
return bech32.encode(prefix, words, Bech32MaxSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nprofileEncode(profile: ProfilePointer): string {
|
export function nprofileEncode(profile: ProfilePointer): string {
|
||||||
@@ -96,7 +98,7 @@ export function nprofileEncode(profile: ProfilePointer): string {
|
|||||||
1: (profile.relays || []).map(url => utf8Encoder.encode(url))
|
1: (profile.relays || []).map(url => utf8Encoder.encode(url))
|
||||||
})
|
})
|
||||||
let words = bech32.toWords(data)
|
let words = bech32.toWords(data)
|
||||||
return bech32.encode('nprofile', words, 1000)
|
return bech32.encode('nprofile', words, Bech32MaxSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function neventEncode(event: EventPointer): string {
|
export function neventEncode(event: EventPointer): string {
|
||||||
@@ -105,7 +107,7 @@ export function neventEncode(event: EventPointer): string {
|
|||||||
1: (event.relays || []).map(url => utf8Encoder.encode(url))
|
1: (event.relays || []).map(url => utf8Encoder.encode(url))
|
||||||
})
|
})
|
||||||
let words = bech32.toWords(data)
|
let words = bech32.toWords(data)
|
||||||
return bech32.encode('nevent', words, 1000)
|
return bech32.encode('nevent', words, Bech32MaxSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeTLV(tlv: TLV): Uint8Array {
|
function encodeTLV(tlv: TLV): Uint8Array {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nostr-tools",
|
"name": "nostr-tools",
|
||||||
"version": "1.0.1",
|
"version": "1.1.2",
|
||||||
"description": "Tools for making a Nostr client.",
|
"description": "Tools for making a Nostr client.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -11,10 +11,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "^0.5.7",
|
"@noble/hashes": "^0.5.7",
|
||||||
"@noble/secp256k1": "^1.7.0",
|
"@noble/secp256k1": "^1.7.0",
|
||||||
|
"@scure/base": "^1.1.1",
|
||||||
"@scure/bip32": "^1.1.1",
|
"@scure/bip32": "^1.1.1",
|
||||||
"@scure/bip39": "^1.1.0",
|
"@scure/bip39": "^1.1.0"
|
||||||
"base64-arraybuffer": "^1.0.2",
|
|
||||||
"bech32": "^2.0.0"
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"decentralization",
|
"decentralization",
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ test('listening (twice) and publishing', async () => {
|
|||||||
content: 'nostr-tools test suite'
|
content: 'nostr-tools test suite'
|
||||||
}
|
}
|
||||||
event.id = getEventHash(event)
|
event.id = getEventHash(event)
|
||||||
event.sig = await signEvent(event, sk)
|
event.sig = signEvent(event, sk)
|
||||||
|
|
||||||
relay.publish(event)
|
relay.publish(event)
|
||||||
return expect(
|
return expect(
|
||||||
|
|||||||
Reference in New Issue
Block a user