Compare commits

..

1 Commits

Author SHA1 Message Date
fiatjaf
0d0557b9a2 fix buffer import and use rollup for transpiling the package. 2021-12-11 08:03:27 -03:00
14 changed files with 101 additions and 189 deletions

View File

@@ -1,5 +1,4 @@
{ {
"root": true,
"parserOptions": { "parserOptions": {
"ecmaVersion": 9, "ecmaVersion": 9,
"ecmaFeatures": { "ecmaFeatures": {

View File

@@ -67,6 +67,4 @@ pool.addRelay('<url>')
// will automatically subscribe to the all the events called with .sub above // will automatically subscribe to the all the events called with .sub above
``` ```
All functions expect bytearrays as hex strings and output bytearrays as hex strings.
For other utils please read the source (for now). For other utils please read the source (for now).

View File

@@ -1,6 +1,7 @@
import {Buffer} from 'buffer' import Buffer from 'buffer'
import createHash from 'create-hash' import * as secp256k1 from '@noble/secp256k1'
import {signSchnorr, verifySchnorr} from 'tiny-secp256k1'
import {sha256} from './utils'
export function getBlankEvent() { export function getBlankEvent() {
return { return {
@@ -23,24 +24,20 @@ export function serializeEvent(evt) {
]) ])
} }
export function getEventHash(event) { export async function getEventHash(event) {
let eventHash = createHash('sha256') let eventHash = await sha256(Buffer.from(serializeEvent(event)))
.update(Buffer.from(serializeEvent(event)))
.digest()
return Buffer.from(eventHash).toString('hex') return Buffer.from(eventHash).toString('hex')
} }
export function verifySignature(event) { export async function verifySignature(event) {
if (event.id !== getEventHash(event)) return false return await secp256k1.schnorr.verify(
return verifySchnorr( event.sig,
Buffer.from(event.id, 'hex'), await getEventHash(event),
Buffer.from(event.pubkey, 'hex'), event.pubkey
Buffer.from(event.sig, 'hex')
) )
} }
export function signEvent(event, key) { export async function signEvent(event, key) {
let eventHash = Buffer.from(getEventHash(event), 'hex') let eventHash = await getEventHash(event)
let keyB = Buffer.from(key, 'hex') return await secp256k1.schnorr.sign(eventHash, key)
return Buffer.from(signSchnorr(eventHash, keyB)).toString('hex')
} }

View File

@@ -1,27 +0,0 @@
export function matchFilter(filter, event) {
if (filter.id && event.id !== filter.id) return false
if (filter.kind && event.kind !== filter.kind) return false
if (filter.author && event.pubkey !== filter.author) return false
if (filter.authors && filter.authors.indexOf(event.pubkey) === -1)
return false
if (
filter['#e'] &&
!event.tags.find(([t, v]) => t === 'e' && v === filter['#e'])
)
return false
if (
filter['#p'] &&
!event.tags.find(([t, v]) => t === 'p' && v === filter['#p'])
)
return false
if (filter.since && event.created_at <= filter.since) return false
return true
}
export function matchFilters(filters, event) {
for (let i = 0; i < filters.length; i++) {
if (matchFilter(filters[i], event)) return true
}
return false
}

View File

@@ -1,4 +1,3 @@
import {generatePrivateKey, getPublicKey} from './keys'
import {relayConnect} from './relay' import {relayConnect} from './relay'
import {relayPool} from './pool' import {relayPool} from './pool'
import { import {
@@ -8,18 +7,19 @@ import {
serializeEvent, serializeEvent,
getEventHash getEventHash
} from './event' } from './event'
import {matchFilter, matchFilters} from './filter' import {makeRandom32, sha256, getPublicKey} from './utils'
export { export {
generatePrivateKey,
relayConnect, relayConnect,
relayPool, relayPool,
signEvent, signEvent,
verifySignature, verifySignature,
serializeEvent, serializeEvent,
getEventHash, getEventHash,
makeRandom32,
sha256,
getPublicKey, getPublicKey,
getBlankEvent, getBlankEvent
matchFilter,
matchFilters
} }
export * from './nip04'
export * from './nip05'

19
keys.js
View File

@@ -1,19 +0,0 @@
import randomBytes from 'randombytes'
import {isPrivate, pointFromScalar} from 'tiny-secp256k1'
export function generatePrivateKey() {
let i = 8
while (i--) {
let r32 = Buffer.from(randomBytes(32))
if (isPrivate(r32)) return r32.toString('hex')
}
throw new Error(
'Valid private key was not found in 8 iterations. PRNG is broken'
)
}
export function getPublicKey(privateKey) {
return Buffer.from(pointFromScalar(Buffer.from(privateKey, 'hex'), true))
.toString('hex')
.slice(2)
}

View File

@@ -1,14 +1,12 @@
import aes from 'browserify-cipher' import Buffer from 'buffer'
import {Buffer} from 'buffer'
import randomBytes from 'randombytes'
import * as secp256k1 from '@noble/secp256k1' import * as secp256k1 from '@noble/secp256k1'
export function encrypt(privkey, pubkey, text) { export function encrypt(privkey, pubkey, text) {
const key = secp256k1.getSharedSecret(privkey, '02' + pubkey) const key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
const normalizedKey = getOnlyXFromFullSharedSecret(key) const normalizedKey = getOnlyXFromFullSharedSecret(key)
let iv = Uint8Array.from(randomBytes(16)) let iv = crypto.randomFillSync(new Uint8Array(16))
var cipher = aes.createCipheriv( var cipher = crypto.createCipheriv(
'aes-256-cbc', 'aes-256-cbc',
Buffer.from(normalizedKey, 'hex'), Buffer.from(normalizedKey, 'hex'),
iv iv
@@ -23,7 +21,7 @@ export function decrypt(privkey, pubkey, ciphertext, iv) {
const key = secp256k1.getSharedSecret(privkey, '02' + pubkey) const key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
const normalizedKey = getOnlyXFromFullSharedSecret(key) const normalizedKey = getOnlyXFromFullSharedSecret(key)
var decipher = aes.createDecipheriv( var decipher = crypto.createDecipheriv(
'aes-256-cbc', 'aes-256-cbc',
Buffer.from(normalizedKey, 'hex'), Buffer.from(normalizedKey, 'hex'),
Buffer.from(iv, 'base64') Buffer.from(iv, 'base64')

View File

@@ -1,4 +1,4 @@
import {Buffer} from 'buffer' import Buffer from 'buffer'
import dnsPacket from 'dns-packet' import dnsPacket from 'dns-packet'
const dohProviders = [ const dohProviders = [

View File

@@ -1,27 +0,0 @@
import {wordlist} from 'micro-bip39/wordlists/english'
import {
generateMnemonic,
mnemonicToSeedSync,
validateMnemonic
} from 'micro-bip39'
import BIP32Factory from 'bip32'
import * as ecc from 'tiny-secp256k1'
const bip32 = BIP32Factory(ecc)
export function privateKeyFromSeed(seed) {
let root = bip32.fromSeed(Buffer.from(seed, 'hex'))
return root.derivePath(`m/44'/1237'/0'/0'`).privateKey.toString('hex')
}
export function seedFromWords(mnemonic) {
return Buffer.from(mnemonicToSeedSync(mnemonic, wordlist)).toString('hex')
}
export function generateSeedWords() {
return generateMnemonic(wordlist)
}
export function validateWords(words) {
return validateMnemonic(words, wordlist)
}

View File

@@ -1,21 +1,18 @@
{ {
"name": "nostr-tools", "name": "nostr-tools",
"version": "0.12.4", "version": "0.6.3",
"description": "Tools for making a Nostr client.", "description": "Tools for making a Nostr client.",
"main": "dist/nostr-tools.esm.min.js",
"module": "dist/nostr-tools.esm.min.js",
"browser": "dist/nostr-tools.umd.min.js",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/fiatjaf/nostr-tools.git" "url": "https://github.com/fiatjaf/nostr-tools.git"
}, },
"dependencies": { "dependencies": {
"@noble/secp256k1": "^1.3.0", "@noble/secp256k1": "^1.3.0",
"bip32": "^3.0.1", "buffer": "^6.0.3",
"browserify-cipher": ">=1",
"buffer": ">=5",
"create-hash": "^1.2.0",
"dns-packet": "^5.2.4", "dns-packet": "^5.2.4",
"micro-bip39": "^0.1.3",
"randombytes": ">=2",
"tiny-secp256k1": "^2.1.2",
"websocket-polyfill": "^0.0.3" "websocket-polyfill": "^0.0.3"
}, },
"keywords": [ "keywords": [
@@ -31,7 +28,9 @@
"client" "client"
], ],
"devDependencies": { "devDependencies": {
"eslint": "^8.5.0", "rollup": "^2.61.1"
"eslint-plugin-babel": "^5.3.1" },
"scripts": {
"prepublish": "rollup -c"
} }
} }

51
pool.js
View File

@@ -3,6 +3,7 @@ import {relayConnect, normalizeRelayURL} from './relay'
export function relayPool(globalPrivateKey) { export function relayPool(globalPrivateKey) {
const relays = {} const relays = {}
const globalSub = []
const noticeCallbacks = [] const noticeCallbacks = []
function propagateNotice(notice, relayURL) { function propagateNotice(notice, relayURL) {
@@ -20,41 +21,32 @@ export function relayPool(globalPrivateKey) {
.filter(({policy}) => policy.read) .filter(({policy}) => policy.read)
.map(({relay}) => [ .map(({relay}) => [
relay.url, relay.url,
relay.sub({filter, cb: event => cb(event, relay.url)}, id) relay.sub({filter, cb: event => cb(event, relay.url)})
]) ])
) )
const activeCallback = cb const activeCallback = cb
const activeFilters = filter const activeFilters = filter
const unsub = () => {
Object.values(subControllers).forEach(sub => sub.unsub())
delete activeSubscriptions[id]
}
const sub = ({cb = activeCallback, filter = activeFilters}) => {
Object.entries(subControllers).map(([relayURL, sub]) => [
relayURL,
sub.sub({cb, filter}, id)
])
return activeSubscriptions[id]
}
const addRelay = relay => {
subControllers[relay.url] = relay.sub({cb, filter}, id)
return activeSubscriptions[id]
}
const removeRelay = relayURL => {
if (relayURL in subControllers) {
subControllers[relayURL].unsub()
if (Object.keys(subControllers).length === 0) unsub()
}
return activeSubscriptions[id]
}
activeSubscriptions[id] = { activeSubscriptions[id] = {
sub, sub: ({cb = activeCallback, filter = activeFilters}) =>
unsub, Object.entries(subControllers).map(([relayURL, sub]) => [
addRelay, relayURL,
removeRelay sub.sub({cb, filter}, id)
]),
addRelay: relay => {
subControllers[relay.url] = relay.sub({cb, filter})
},
removeRelay: relayURL => {
if (relayURL in subControllers) {
subControllers[relayURL].unsub()
if (Object.keys(subControllers).length === 0) unsub()
}
},
unsub: () => {
Object.values(subControllers).forEach(sub => sub.unsub())
delete activeSubscriptions[id]
}
} }
return activeSubscriptions[id] return activeSubscriptions[id]
@@ -99,12 +91,11 @@ export function relayPool(globalPrivateKey) {
if (index !== -1) noticeCallbacks.splice(index, 1) if (index !== -1) noticeCallbacks.splice(index, 1)
}, },
async publish(event, statusCallback = (status, relayURL) => {}) { async publish(event, statusCallback = (status, relayURL) => {}) {
event.id = await getEventHash(event)
if (!event.sig) { if (!event.sig) {
event.tags = event.tags || [] event.tags = event.tags || []
if (globalPrivateKey) { if (globalPrivateKey) {
event.id = await getEventHash(event)
event.sig = await signEvent(event, globalPrivateKey) event.sig = await signEvent(event, globalPrivateKey)
} else { } else {
throw new Error( throw new Error(

View File

@@ -1,9 +1,6 @@
/* global WebSocket */
import 'websocket-polyfill' import 'websocket-polyfill'
import {verifySignature} from './event' import {verifySignature} from './event'
import {matchFilters} from './filter'
export function normalizeRelayURL(url) { export function normalizeRelayURL(url) {
let [host, ...qs] = url.split('?') let [host, ...qs] = url.split('?')
@@ -13,10 +10,10 @@ export function normalizeRelayURL(url) {
return [host, ...qs].join('?') return [host, ...qs].join('?')
} }
export function relayConnect(url, onNotice = () => {}, onError = () => {}) { export function relayConnect(url, onNotice) {
url = normalizeRelayURL(url) url = normalizeRelayURL(url)
var ws, resolveOpen, untilOpen, wasClosed var ws, resolveOpen, untilOpen
var openSubs = {} var openSubs = {}
let attemptNumber = 1 let attemptNumber = 1
let nextAttemptSeconds = 1 let nextAttemptSeconds = 1
@@ -37,26 +34,19 @@ export function relayConnect(url, onNotice = () => {}, onError = () => {}) {
resolveOpen() resolveOpen()
// restablish old subscriptions // restablish old subscriptions
if (wasClosed) { for (let channel in openSubs) {
wasClosed = false let filters = openSubs[channel]
for (let channel in openSubs) { let cb = channels[channel]
let filters = openSubs[channel] sub({cb, filter: filters}, channel)
let cb = channels[channel]
sub({cb, filter: filters}, channel)
}
} }
} }
ws.onerror = (err) => { ws.onerror = () => {
console.log('error connecting to relay', url) console.log('error connecting to relay', url)
onError(err)
} }
ws.onclose = () => { ws.onclose = () => {
resetOpenState() resetOpenState()
attemptNumber++ attemptNumber++
nextAttemptSeconds += attemptNumber ** 3 nextAttemptSeconds += attemptNumber
if (nextAttemptSeconds > 14400) {
nextAttemptSeconds = 14400 // 4 hours
}
console.log( console.log(
`relay ${url} connection closed. reconnecting in ${nextAttemptSeconds} seconds.` `relay ${url} connection closed. reconnecting in ${nextAttemptSeconds} seconds.`
) )
@@ -65,8 +55,6 @@ export function relayConnect(url, onNotice = () => {}, onError = () => {}) {
connect() connect()
} catch (err) {} } catch (err) {}
}, nextAttemptSeconds * 1000) }, nextAttemptSeconds * 1000)
wasClosed = true
} }
ws.onmessage = async e => { ws.onmessage = async e => {
@@ -92,12 +80,12 @@ export function relayConnect(url, onNotice = () => {}, onError = () => {}) {
let channel = data[1] let channel = data[1]
let event = data[2] let event = data[2]
if ( if (await verifySignature(event)) {
(await verifySignature(event)) && if (channels[channel]) {
channels[channel] && channels[channel](event)
matchFilters(openSubs[channel], event) }
) { } else {
channels[channel](event) console.warn('got event with invalid signature from ' + url, event)
} }
return return
} }
@@ -147,26 +135,19 @@ export function relayConnect(url, onNotice = () => {}, onError = () => {}) {
return { return {
url, url,
sub, sub,
async publish(event, statusCallback) { async publish(event, statusCallback = status => {}) {
try { try {
await trySend(['EVENT', event]) await trySend(['EVENT', event])
if (statusCallback) { statusCallback(0)
statusCallback(0) let {unsub} = relay.sub({
let {unsub} = sub( cb: () => {
{ statusCallback(1)
cb: () => { },
statusCallback(1) filter: {id: event.id}
unsub() })
clearTimeout(willUnsub) setTimeout(unsub, 5000)
},
filter: {id: event.id}
},
`monitor-${event.id.slice(0, 5)}`
)
let willUnsub = setTimeout(unsub, 5000)
}
} catch (err) { } catch (err) {
if (statusCallback) statusCallback(-1) statusCallback(-1)
} }
}, },
close() { close() {

16
rollup.config.js Normal file
View File

@@ -0,0 +1,16 @@
import pkg from './package.json'
export default {
input: 'index.js',
output: [
{
name: 'nostrtools',
file: pkg.browser,
format: 'umd'
},
{
file: pkg.module,
format: 'es'
}
]
}

6
utils.js Normal file
View File

@@ -0,0 +1,6 @@
import * as secp256k1 from '@noble/secp256k1'
export const makeRandom32 = () => secp256k1.utils.randomPrivateKey()
export const sha256 = m => secp256k1.utils.sha256(Uint8Array.from(m))
export const getPublicKey = privateKey =>
secp256k1.schnorr.getPublicKey(privateKey)