mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-09 00:28:51 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63ccc8b4c8 | ||
|
|
7cf7df88db | ||
|
|
bded539122 | ||
|
|
3647bbd68a | ||
|
|
fb085ffdf7 | ||
|
|
280d483ef4 | ||
|
|
54b55b98f1 | ||
|
|
84f9881812 | ||
|
|
db6baf2e6b | ||
|
|
bb1e6f4356 | ||
|
|
5626d3048b |
1
kinds.ts
1
kinds.ts
@@ -78,7 +78,6 @@ export const ClientAuth = 22242
|
|||||||
export const NWCWalletRequest = 23194
|
export const NWCWalletRequest = 23194
|
||||||
export const NWCWalletResponse = 23195
|
export const NWCWalletResponse = 23195
|
||||||
export const NostrConnect = 24133
|
export const NostrConnect = 24133
|
||||||
export const NostrConnectAdmin = 24134
|
|
||||||
export const HTTPAuth = 27235
|
export const HTTPAuth = 27235
|
||||||
export const Followsets = 30000
|
export const Followsets = 30000
|
||||||
export const Genericlists = 30001
|
export const Genericlists = 30001
|
||||||
|
|||||||
@@ -1,18 +1,9 @@
|
|||||||
import { test, expect } from 'bun:test'
|
import { test, expect } from 'bun:test'
|
||||||
import crypto from 'node:crypto'
|
|
||||||
|
|
||||||
import { encrypt, decrypt } from './nip04.ts'
|
import { encrypt, decrypt } from './nip04.ts'
|
||||||
import { getPublicKey, generateSecretKey } from './pure.ts'
|
import { getPublicKey, generateSecretKey } from './pure.ts'
|
||||||
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
||||||
|
|
||||||
try {
|
|
||||||
// @ts-ignore
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
globalThis.crypto = crypto
|
|
||||||
} catch (err) {
|
|
||||||
/***/
|
|
||||||
}
|
|
||||||
|
|
||||||
test('encrypt and decrypt message', async () => {
|
test('encrypt and decrypt message', async () => {
|
||||||
let sk1 = generateSecretKey()
|
let sk1 = generateSecretKey()
|
||||||
let sk2 = generateSecretKey()
|
let sk2 = generateSecretKey()
|
||||||
|
|||||||
20
nip04.ts
20
nip04.ts
@@ -1,15 +1,10 @@
|
|||||||
import { bytesToHex, randomBytes } from '@noble/hashes/utils'
|
import { bytesToHex, randomBytes } from '@noble/hashes/utils'
|
||||||
import { secp256k1 } from '@noble/curves/secp256k1'
|
import { secp256k1 } from '@noble/curves/secp256k1'
|
||||||
|
import { cbc } from '@noble/ciphers/aes'
|
||||||
import { base64 } from '@scure/base'
|
import { base64 } from '@scure/base'
|
||||||
|
|
||||||
import { utf8Decoder, utf8Encoder } from './utils.ts'
|
import { utf8Decoder, utf8Encoder } from './utils.ts'
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
if (typeof crypto !== 'undefined' && !crypto.subtle && crypto.webcrypto) {
|
|
||||||
// @ts-ignore
|
|
||||||
crypto.subtle = crypto.webcrypto.subtle
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function encrypt(secretKey: string | Uint8Array, pubkey: string, text: string): Promise<string> {
|
export async function encrypt(secretKey: string | Uint8Array, pubkey: string, text: string): Promise<string> {
|
||||||
const privkey: string = secretKey instanceof Uint8Array ? bytesToHex(secretKey) : secretKey
|
const privkey: string = secretKey instanceof Uint8Array ? bytesToHex(secretKey) : secretKey
|
||||||
const key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
|
const key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
|
||||||
@@ -17,8 +12,9 @@ export async function encrypt(secretKey: string | Uint8Array, pubkey: string, te
|
|||||||
|
|
||||||
let iv = Uint8Array.from(randomBytes(16))
|
let iv = Uint8Array.from(randomBytes(16))
|
||||||
let plaintext = utf8Encoder.encode(text)
|
let plaintext = utf8Encoder.encode(text)
|
||||||
let cryptoKey = await crypto.subtle.importKey('raw', normalizedKey, { name: 'AES-CBC' }, false, ['encrypt'])
|
|
||||||
let ciphertext = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, cryptoKey, plaintext)
|
let ciphertext = cbc(normalizedKey, iv).encrypt(plaintext)
|
||||||
|
|
||||||
let ctb64 = base64.encode(new Uint8Array(ciphertext))
|
let ctb64 = base64.encode(new Uint8Array(ciphertext))
|
||||||
let ivb64 = base64.encode(new Uint8Array(iv.buffer))
|
let ivb64 = base64.encode(new Uint8Array(iv.buffer))
|
||||||
|
|
||||||
@@ -31,14 +27,12 @@ export async function decrypt(secretKey: string | Uint8Array, pubkey: string, da
|
|||||||
let key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
|
let key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
|
||||||
let normalizedKey = getNormalizedX(key)
|
let normalizedKey = getNormalizedX(key)
|
||||||
|
|
||||||
let cryptoKey = await crypto.subtle.importKey('raw', normalizedKey, { name: 'AES-CBC' }, false, ['decrypt'])
|
|
||||||
let ciphertext = base64.decode(ctb64)
|
|
||||||
let iv = base64.decode(ivb64)
|
let iv = base64.decode(ivb64)
|
||||||
|
let ciphertext = base64.decode(ctb64)
|
||||||
|
|
||||||
let plaintext = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, ciphertext)
|
let plaintext = cbc(normalizedKey, iv).decrypt(ciphertext)
|
||||||
|
|
||||||
let text = utf8Decoder.decode(plaintext)
|
return utf8Decoder.decode(plaintext)
|
||||||
return text
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNormalizedX(key: Uint8Array): Uint8Array {
|
function getNormalizedX(key: Uint8Array): Uint8Array {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe('requesting relay as for NIP11', () => {
|
|||||||
test('testing a relay', async () => {
|
test('testing a relay', async () => {
|
||||||
const info = await fetchRelayInformation('wss://atlas.nostr.land')
|
const info = await fetchRelayInformation('wss://atlas.nostr.land')
|
||||||
expect(info.name).toEqual('nostr.land')
|
expect(info.name).toEqual('nostr.land')
|
||||||
expect(info.description).toEqual('nostr.land family of relays (us-or-01)')
|
expect(info.description).toContain('nostr.land family')
|
||||||
expect(info.fees).toBeTruthy()
|
expect(info.fees).toBeTruthy()
|
||||||
expect(info.supported_nips).toEqual([1, 2, 4, 9, 11, 12, 16, 20, 22, 28, 33, 40])
|
expect(info.supported_nips).toEqual([1, 2, 4, 9, 11, 12, 16, 20, 22, 28, 33, 40])
|
||||||
expect(info.software).toEqual('custom')
|
expect(info.software).toEqual('custom')
|
||||||
|
|||||||
4
nip44.ts
4
nip44.ts
@@ -1,5 +1,5 @@
|
|||||||
import { chacha20 } from '@noble/ciphers/chacha'
|
import { chacha20 } from '@noble/ciphers/chacha'
|
||||||
import { ensureBytes, equalBytes } from '@noble/ciphers/utils'
|
import { equalBytes } from '@noble/ciphers/utils'
|
||||||
import { secp256k1 } from '@noble/curves/secp256k1'
|
import { secp256k1 } from '@noble/curves/secp256k1'
|
||||||
import { extract as hkdf_extract, expand as hkdf_expand } from '@noble/hashes/hkdf'
|
import { extract as hkdf_extract, expand as hkdf_expand } from '@noble/hashes/hkdf'
|
||||||
import { hmac } from '@noble/hashes/hmac'
|
import { hmac } from '@noble/hashes/hmac'
|
||||||
@@ -23,8 +23,6 @@ const u = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getMessageKeys(conversationKey: Uint8Array, nonce: Uint8Array) {
|
getMessageKeys(conversationKey: Uint8Array, nonce: Uint8Array) {
|
||||||
ensureBytes(conversationKey, 32)
|
|
||||||
ensureBytes(nonce, 32)
|
|
||||||
const keys = hkdf_expand(sha256, conversationKey, nonce, 76)
|
const keys = hkdf_expand(sha256, conversationKey, nonce, 76)
|
||||||
return {
|
return {
|
||||||
chacha_key: keys.subarray(0, 32),
|
chacha_key: keys.subarray(0, 32),
|
||||||
|
|||||||
31
nip46.ts
31
nip46.ts
@@ -4,7 +4,7 @@ import { AbstractSimplePool, SubCloser } from './abstract-pool.ts'
|
|||||||
import { decrypt, encrypt } from './nip04.ts'
|
import { decrypt, encrypt } from './nip04.ts'
|
||||||
import { NIP05_REGEX } from './nip05.ts'
|
import { NIP05_REGEX } from './nip05.ts'
|
||||||
import { SimplePool } from './pool.ts'
|
import { SimplePool } from './pool.ts'
|
||||||
import { Handlerinformation, NostrConnect, NostrConnectAdmin } from './kinds.ts'
|
import { Handlerinformation, NostrConnect } from './kinds.ts'
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
|
|
||||||
var _fetch: any
|
var _fetch: any
|
||||||
@@ -83,6 +83,7 @@ export class BunkerSigner {
|
|||||||
reject: (_: string) => void
|
reject: (_: string) => void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private waitingForAuth: { [id: string]: boolean }
|
||||||
private secretKey: Uint8Array
|
private secretKey: Uint8Array
|
||||||
public bp: BunkerPointer
|
public bp: BunkerPointer
|
||||||
|
|
||||||
@@ -104,17 +105,21 @@ export class BunkerSigner {
|
|||||||
this.idPrefix = Math.random().toString(36).substring(7)
|
this.idPrefix = Math.random().toString(36).substring(7)
|
||||||
this.serial = 0
|
this.serial = 0
|
||||||
this.listeners = {}
|
this.listeners = {}
|
||||||
|
this.waitingForAuth = {}
|
||||||
|
|
||||||
const listeners = this.listeners
|
const listeners = this.listeners
|
||||||
|
const waitingForAuth = this.waitingForAuth
|
||||||
|
|
||||||
this.subCloser = this.pool.subscribeMany(
|
this.subCloser = this.pool.subscribeMany(
|
||||||
this.bp.relays,
|
this.bp.relays,
|
||||||
[{ kinds: [NostrConnect, NostrConnectAdmin], '#p': [getPublicKey(this.secretKey)] }],
|
[{ kinds: [NostrConnect], '#p': [getPublicKey(this.secretKey)] }],
|
||||||
{
|
{
|
||||||
async onevent(event: NostrEvent) {
|
async onevent(event: NostrEvent) {
|
||||||
const { id, result, error } = JSON.parse(await decrypt(clientSecretKey, event.pubkey, event.content))
|
const { id, result, error } = JSON.parse(await decrypt(clientSecretKey, event.pubkey, event.content))
|
||||||
|
|
||||||
if (result === 'auth_url') {
|
if (result === 'auth_url' && waitingForAuth[id]) {
|
||||||
|
delete waitingForAuth[id]
|
||||||
|
|
||||||
if (params.onauth) {
|
if (params.onauth) {
|
||||||
params.onauth(error)
|
params.onauth(error)
|
||||||
} else {
|
} else {
|
||||||
@@ -155,7 +160,7 @@ export class BunkerSigner {
|
|||||||
// the request event
|
// the request event
|
||||||
const verifiedEvent: VerifiedEvent = finalizeEvent(
|
const verifiedEvent: VerifiedEvent = finalizeEvent(
|
||||||
{
|
{
|
||||||
kind: method === 'create_account' ? NostrConnectAdmin : NostrConnect,
|
kind: NostrConnect,
|
||||||
tags: [['p', this.bp.pubkey]],
|
tags: [['p', this.bp.pubkey]],
|
||||||
content: encryptedContent,
|
content: encryptedContent,
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
@@ -165,6 +170,7 @@ export class BunkerSigner {
|
|||||||
|
|
||||||
// setup callback listener
|
// setup callback listener
|
||||||
this.listeners[id] = { resolve, reject }
|
this.listeners[id] = { resolve, reject }
|
||||||
|
this.waitingForAuth[id] = true
|
||||||
|
|
||||||
// publish the event
|
// publish the event
|
||||||
await Promise.any(this.pool.publish(this.bp.relays, verifiedEvent))
|
await Promise.any(this.pool.publish(this.bp.relays, verifiedEvent))
|
||||||
@@ -273,22 +279,35 @@ export async function createAccount(
|
|||||||
return rpc
|
return rpc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @deprecated use fetchBunkerProviders instead
|
||||||
|
export const fetchCustodialBunkers = fetchBunkerProviders
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches info on available providers that announce themselves using NIP-89 events.
|
* Fetches info on available providers that announce themselves using NIP-89 events.
|
||||||
* @returns A promise that resolves to an array of available bunker objects.
|
* @returns A promise that resolves to an array of available bunker objects.
|
||||||
*/
|
*/
|
||||||
export async function fetchCustodialBunkers(pool: AbstractSimplePool, relays: string[]): Promise<BunkerProfile[]> {
|
export async function fetchBunkerProviders(pool: AbstractSimplePool, relays: string[]): Promise<BunkerProfile[]> {
|
||||||
const events = await pool.querySync(relays, {
|
const events = await pool.querySync(relays, {
|
||||||
kinds: [Handlerinformation],
|
kinds: [Handlerinformation],
|
||||||
'#k': [NostrConnect.toString()],
|
'#k': [NostrConnect.toString()],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
events.sort((a, b) => b.created_at - a.created_at)
|
||||||
|
|
||||||
// validate bunkers by checking their NIP-05 and pubkey
|
// validate bunkers by checking their NIP-05 and pubkey
|
||||||
// map to a more useful object
|
// map to a more useful object
|
||||||
const validatedBunkers = await Promise.all(
|
const validatedBunkers = await Promise.all(
|
||||||
events.map(async event => {
|
events.map(async (event, i) => {
|
||||||
try {
|
try {
|
||||||
const content = JSON.parse(event.content)
|
const content = JSON.parse(event.content)
|
||||||
|
|
||||||
|
// skip duplicates
|
||||||
|
try {
|
||||||
|
if (events.findIndex(ev => JSON.parse(ev.content).nip05 === content.nip05) !== i) return undefined
|
||||||
|
} catch (err) {
|
||||||
|
/***/
|
||||||
|
}
|
||||||
|
|
||||||
const bp = await queryBunkerProfile(content.nip05)
|
const bp = await queryBunkerProfile(content.nip05)
|
||||||
if (bp && bp.pubkey === event.pubkey && bp.relays.length) {
|
if (bp && bp.pubkey === event.pubkey && bp.relays.length) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import crypto from 'node:crypto'
|
|
||||||
import { describe, test, expect } from 'bun:test'
|
import { describe, test, expect } from 'bun:test'
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
import { makeNwcRequestEvent, parseConnectionString } from './nip47'
|
import { makeNwcRequestEvent, parseConnectionString } from './nip47'
|
||||||
import { decrypt } from './nip04.ts'
|
import { decrypt } from './nip04.ts'
|
||||||
import { NWCWalletRequest } from './kinds.ts'
|
import { NWCWalletRequest } from './kinds.ts'
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
globalThis.crypto = crypto
|
|
||||||
|
|
||||||
describe('parseConnectionString', () => {
|
describe('parseConnectionString', () => {
|
||||||
test('returns pubkey, relay, and secret if connection string is valid', () => {
|
test('returns pubkey, relay, and secret if connection string is valid', () => {
|
||||||
const connectionString =
|
const connectionString =
|
||||||
|
|||||||
14
nip96.ts
14
nip96.ts
@@ -1,5 +1,7 @@
|
|||||||
|
import { sha256 } from '@noble/hashes/sha256'
|
||||||
import { EventTemplate } from './core'
|
import { EventTemplate } from './core'
|
||||||
import { FileServerPreference } from './kinds'
|
import { FileServerPreference } from './kinds'
|
||||||
|
import { bytesToHex } from '@noble/hashes/utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the configuration for a server compliant with NIP-96.
|
* Represents the configuration for a server compliant with NIP-96.
|
||||||
@@ -576,15 +578,5 @@ export function generateFSPEventTemplate(serverUrls: string[]): EventTemplate {
|
|||||||
* @returns A promise that resolves to the SHA-256 hash of the file.
|
* @returns A promise that resolves to the SHA-256 hash of the file.
|
||||||
*/
|
*/
|
||||||
export async function calculateFileHash(file: Blob): Promise<string> {
|
export async function calculateFileHash(file: Blob): Promise<string> {
|
||||||
// Read the file as an ArrayBuffer
|
return bytesToHex(sha256(new Uint8Array(await file.arrayBuffer())))
|
||||||
const buffer = await file.arrayBuffer()
|
|
||||||
|
|
||||||
// Calculate the SHA-256 hash of the file
|
|
||||||
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer)
|
|
||||||
|
|
||||||
// Convert the hash to a hexadecimal string
|
|
||||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
||||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
|
||||||
|
|
||||||
return hashHex
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "nostr-tools",
|
"name": "nostr-tools",
|
||||||
"version": "2.2.0",
|
"version": "2.3.1",
|
||||||
"description": "Tools for making a Nostr client.",
|
"description": "Tools for making a Nostr client.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
},
|
},
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/ciphers": "0.2.0",
|
"@noble/ciphers": "^0.5.1",
|
||||||
"@noble/curves": "1.2.0",
|
"@noble/curves": "1.2.0",
|
||||||
"@noble/hashes": "1.3.1",
|
"@noble/hashes": "1.3.1",
|
||||||
"@scure/base": "1.1.1",
|
"@scure/base": "1.1.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user