Compare commits

...

8 Commits

Author SHA1 Message Date
fiatjaf
60fc0d7940 use tiny-secp256k1, updated nip06 and other utils. 2021-12-29 14:29:43 -03:00
fiatjaf
faa308049f always add event.id 2021-12-28 20:44:35 -03:00
fiatjaf
7b0220c1b8 use browserify-cipher for aes.
it seems everybody was including this by default before, but now webpack and others are not.
2021-12-18 20:30:58 -03:00
fiatjaf
d8eee25e3a another typo: null != undefined. 2021-12-14 22:06:31 -03:00
fiatjaf
d5e93e0c30 fix a typo in matchFilter function. 2021-12-14 22:02:56 -03:00
fiatjaf
fff31b5ff4 automatically run received events through the filters they should pass (double-check the work made by the relay). 2021-12-14 22:00:42 -03:00
fiatjaf
cd7ffb8911 add local event filter functions. 2021-12-14 21:56:07 -03:00
fiatjaf
4f0cae0eb8 add missing id arguments. 2021-12-13 21:22:23 -03:00
11 changed files with 118 additions and 49 deletions

View File

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

27
filter.js Normal file
View File

@@ -0,0 +1,27 @@
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,3 +1,4 @@
import {generatePrivateKey, getPublicKey} from './keys'
import {relayConnect} from './relay' import {relayConnect} from './relay'
import {relayPool} from './pool' import {relayPool} from './pool'
import { import {
@@ -7,7 +8,7 @@ import {
serializeEvent, serializeEvent,
getEventHash getEventHash
} from './event' } from './event'
import {makeRandom32, sha256, getPublicKey} from './utils' import {matchFilter, matchFilters} from './filter'
export { export {
relayConnect, relayConnect,
@@ -16,8 +17,8 @@ export {
verifySignature, verifySignature,
serializeEvent, serializeEvent,
getEventHash, getEventHash,
makeRandom32,
sha256,
getPublicKey, getPublicKey,
getBlankEvent getBlankEvent,
matchFilter,
matchFilters
} }

19
keys.js Normal file
View File

@@ -0,0 +1,19 @@
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')
}

View File

@@ -1,3 +1,4 @@
import aes from 'browserify-cipher'
import {Buffer} from 'buffer' import {Buffer} from 'buffer'
import randomBytes from 'randombytes' import randomBytes from 'randombytes'
import * as secp256k1 from '@noble/secp256k1' import * as secp256k1 from '@noble/secp256k1'
@@ -7,7 +8,7 @@ export function encrypt(privkey, pubkey, text) {
const normalizedKey = getOnlyXFromFullSharedSecret(key) const normalizedKey = getOnlyXFromFullSharedSecret(key)
let iv = Uint8Array.from(randomBytes(16)) let iv = Uint8Array.from(randomBytes(16))
var cipher = crypto.createCipheriv( var cipher = aes.createCipheriv(
'aes-256-cbc', 'aes-256-cbc',
Buffer.from(normalizedKey, 'hex'), Buffer.from(normalizedKey, 'hex'),
iv iv
@@ -22,7 +23,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 = crypto.createDecipheriv( var decipher = aes.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,17 +1,28 @@
import createHmac from 'create-hmac' import createHmac from 'create-hmac'
import randomBytes from 'randombytes' import {wordlist} from 'micro-bip39/wordlists/english'
import * as bip39 from 'bip39' 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) { export function privateKeyFromSeed(seed) {
let hmac = createHmac('sha512', Buffer.from('Nostr seed', 'utf8')) let root = bip32.fromSeed(Buffer.from(seed, 'hex'))
hmac.update(seed) return root.derivePath(`m/44'/1237'/0'/0'`).privateKey.toString('hex')
return hmac.digest().slice(0, 32).toString('hex')
} }
export function seedFromWords(mnemonic) { export function seedFromWords(mnemonic) {
return bip39.mnemonicToSeedSync(mnemonic) return Buffer.from(mnemonicToSeedSync(mnemonic, wordlist)).toString('hex')
} }
export function generateSeedWords() { export function generateSeedWords() {
return bip39.entropyToMnemonic(randomBytes(16).toString('hex')) return generateMnemonic(wordlist)
}
export function validateWords(words) {
return validateMnemonic(words, wordlist)
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "nostr-tools", "name": "nostr-tools",
"version": "0.9.0", "version": "0.12.1",
"description": "Tools for making a Nostr client.", "description": "Tools for making a Nostr client.",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -8,11 +8,15 @@
}, },
"dependencies": { "dependencies": {
"@noble/secp256k1": "^1.3.0", "@noble/secp256k1": "^1.3.0",
"bip39": "^3.0.4", "bip32": "^3.0.1",
"buffer": "^6.0.3", "browserify-cipher": ">=1",
"create-hmac": "^1.1.7", "buffer": ">=5",
"create-hash": "^1.2.0",
"create-hmac": ">=1",
"dns-packet": "^5.2.4", "dns-packet": "^5.2.4",
"randombytes": "^2.1.0", "micro-bip39": "^0.1.3",
"randombytes": ">=2",
"tiny-secp256k1": "^2.1.2",
"websocket-polyfill": "^0.0.3" "websocket-polyfill": "^0.0.3"
}, },
"keywords": [ "keywords": [

View File

@@ -21,7 +21,7 @@ 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)}) relay.sub({filter, cb: event => cb(event, relay.url)}, id)
]) ])
) )
@@ -37,7 +37,7 @@ export function relayPool(globalPrivateKey) {
return activeSubscriptions[id] return activeSubscriptions[id]
}, },
addRelay: relay => { addRelay: relay => {
subControllers[relay.url] = relay.sub({cb, filter}) subControllers[relay.url] = relay.sub({cb, filter}, id)
return activeSubscriptions[id] return activeSubscriptions[id]
}, },
removeRelay: relayURL => { removeRelay: relayURL => {
@@ -95,11 +95,12 @@ 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,6 +1,7 @@
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('?')
@@ -88,11 +89,13 @@ export function relayConnect(url, onNotice) {
let channel = data[1] let channel = data[1]
let event = data[2] let event = data[2]
if (await verifySignature(event)) { if (
if (channels[channel]) { (await verifySignature(event)) &&
channels[channel] &&
matchFilters(openSubs[channel], event)
) {
channels[channel](event) channels[channel](event)
} }
}
return return
} }
} }
@@ -145,12 +148,15 @@ export function relayConnect(url, onNotice) {
try { try {
await trySend(['EVENT', event]) await trySend(['EVENT', event])
statusCallback(0) statusCallback(0)
let {unsub} = relay.sub({ let {unsub} = relay.sub(
{
cb: () => { cb: () => {
statusCallback(1) statusCallback(1)
}, },
filter: {id: event.id} filter: {id: event.id}
}) },
`monitor-${event.id.slice(0, 5)}`
)
setTimeout(unsub, 5000) setTimeout(unsub, 5000)
} catch (err) { } catch (err) {
statusCallback(-1) statusCallback(-1)

View File

@@ -1,6 +0,0 @@
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)