mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-15 03:18:51 +00:00
add nip41: stateless revocations.
This commit is contained in:
160
nip41.ts
Normal file
160
nip41.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import * as secp256k1 from '@noble/secp256k1'
|
||||
import {sha256} from '@noble/hashes/sha256'
|
||||
import {mnemonicToSeedSync} from '@scure/bip39'
|
||||
import {HARDENED_OFFSET, HDKey} from '@scure/bip32'
|
||||
|
||||
import {getPublicKey} from './keys'
|
||||
import {Event, getEventHash, Kind, signEvent, verifySignature} from './event'
|
||||
|
||||
const MaxKeys = 11
|
||||
|
||||
function getRootFromMnemonic(mnemonic: string): HDKey {
|
||||
return HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic)).derive(
|
||||
`m/44'/1237'/41'`
|
||||
)
|
||||
}
|
||||
|
||||
export function getPrivateKeyAtIndex(
|
||||
mnemonic: string,
|
||||
targetIdx: number
|
||||
): string {
|
||||
let root = getRootFromMnemonic(mnemonic)
|
||||
let rootPrivateKey = secp256k1.utils.bytesToHex(root.privateKey as Uint8Array)
|
||||
let currentPrivateKey = rootPrivateKey
|
||||
|
||||
for (let idx = 1; idx <= targetIdx; idx++) {
|
||||
let hiddenPrivateKey = secp256k1.utils.bytesToHex(
|
||||
root.deriveChild(idx + HARDENED_OFFSET).privateKey as Uint8Array
|
||||
)
|
||||
currentPrivateKey = getChildPrivateKey(currentPrivateKey, hiddenPrivateKey)
|
||||
}
|
||||
|
||||
return currentPrivateKey
|
||||
}
|
||||
|
||||
export function getPublicKeyAtIndex(
|
||||
root: HDKey,
|
||||
targetIdx: number
|
||||
): Uint8Array {
|
||||
let rootPublicKey = root.publicKey as Uint8Array
|
||||
|
||||
let currentPublicKey = rootPublicKey
|
||||
for (let idx = 1; idx <= targetIdx; idx++) {
|
||||
let hiddenPublicKey = root.deriveChild(idx + HARDENED_OFFSET)
|
||||
.publicKey as Uint8Array
|
||||
currentPublicKey = getChildPublicKey(currentPublicKey, hiddenPublicKey)
|
||||
}
|
||||
|
||||
return currentPublicKey
|
||||
}
|
||||
|
||||
function getIndexOfPublicKey(root: HDKey, publicKey: string): number {
|
||||
let rootPublicKey = root.publicKey as Uint8Array
|
||||
if (secp256k1.utils.bytesToHex(rootPublicKey).slice(2) === publicKey) return 0
|
||||
|
||||
let currentPublicKey = rootPublicKey
|
||||
for (let idx = 1; idx <= MaxKeys; idx++) {
|
||||
let hiddenPublicKey = root.deriveChild(idx + HARDENED_OFFSET)
|
||||
.publicKey as Uint8Array
|
||||
let pubkeyAtIndex = getChildPublicKey(currentPublicKey, hiddenPublicKey)
|
||||
if (secp256k1.utils.bytesToHex(pubkeyAtIndex).slice(2) === publicKey)
|
||||
return idx
|
||||
|
||||
currentPublicKey = pubkeyAtIndex
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`public key ${publicKey} not in the set of the first ${MaxKeys} public keys`
|
||||
)
|
||||
}
|
||||
|
||||
export function getChildPublicKey(
|
||||
parentPublicKey: Uint8Array,
|
||||
hiddenPublicKey: Uint8Array
|
||||
): Uint8Array {
|
||||
if (parentPublicKey.length !== 33 || hiddenPublicKey.length !== 33)
|
||||
throw new Error(
|
||||
'getChildPublicKey() requires public keys with the leading differentiator byte.'
|
||||
)
|
||||
|
||||
let hash = sha256(
|
||||
secp256k1.utils.concatBytes(hiddenPublicKey, parentPublicKey)
|
||||
)
|
||||
let hashPoint = secp256k1.Point.fromPrivateKey(hash)
|
||||
let point = secp256k1.Point.fromHex(hiddenPublicKey).add(hashPoint)
|
||||
return point.toRawBytes(true)
|
||||
}
|
||||
|
||||
export function getChildPrivateKey(
|
||||
parentPrivateKey: string,
|
||||
hiddenPrivateKey: string
|
||||
): string {
|
||||
let parentPublicKey = secp256k1.getPublicKey(parentPrivateKey, true)
|
||||
let hiddenPublicKey = secp256k1.getPublicKey(hiddenPrivateKey, true)
|
||||
let hash = sha256(
|
||||
secp256k1.utils.concatBytes(hiddenPublicKey, parentPublicKey)
|
||||
)
|
||||
let hashScalar = BigInt(`0x${secp256k1.utils.bytesToHex(hash)}`)
|
||||
let hiddenPrivateKeyScalar = BigInt(`0x${hiddenPrivateKey}`)
|
||||
let sumScalar = hiddenPrivateKeyScalar + hashScalar
|
||||
let modulo = secp256k1.utils.mod(sumScalar, secp256k1.CURVE.n)
|
||||
return modulo.toString(16).padStart(64, '0')
|
||||
}
|
||||
|
||||
export function buildRevocationEvent(
|
||||
mnemonic: string,
|
||||
compromisedKey: string,
|
||||
content = ''
|
||||
): {
|
||||
parentPrivateKey: string
|
||||
event: Event
|
||||
} {
|
||||
let root = getRootFromMnemonic(mnemonic)
|
||||
let idx = getIndexOfPublicKey(root, compromisedKey)
|
||||
let hiddenKey = secp256k1.utils.bytesToHex(
|
||||
root.deriveChild(idx + HARDENED_OFFSET).publicKey as Uint8Array
|
||||
)
|
||||
let parentPrivateKey = getPrivateKeyAtIndex(mnemonic, idx - 1)
|
||||
let parentPublicKey = getPublicKey(parentPrivateKey)
|
||||
|
||||
let event: Event = {
|
||||
kind: 13,
|
||||
tags: [
|
||||
['p', compromisedKey],
|
||||
['hidden-key', hiddenKey]
|
||||
],
|
||||
created_at: Math.round(Date.now() / 1000),
|
||||
content,
|
||||
pubkey: parentPublicKey
|
||||
}
|
||||
|
||||
event.sig = signEvent(event, parentPrivateKey)
|
||||
event.id = getEventHash(event)
|
||||
|
||||
return {parentPrivateKey, event}
|
||||
}
|
||||
|
||||
export function validateRevocation(event: Event): boolean {
|
||||
if (event.kind !== Kind.StatelessRevocation) return false
|
||||
if (!verifySignature(event)) return false
|
||||
|
||||
let invalidKeyTag = event.tags.find(([t, v]) => t === 'p' && v)
|
||||
if (!invalidKeyTag) return false
|
||||
let invalidKey = invalidKeyTag[1]
|
||||
|
||||
let hiddenKeyTag = event.tags.find(([t, v]) => t === 'hidden-key' && v)
|
||||
if (!hiddenKeyTag) return false
|
||||
let hiddenKey = secp256k1.utils.hexToBytes(hiddenKeyTag[1])
|
||||
if (hiddenKey.length !== 33) return false
|
||||
|
||||
let currentKeyAlt1 = secp256k1.utils.hexToBytes('02' + event.pubkey)
|
||||
let currentKeyAlt2 = secp256k1.utils.hexToBytes('03' + event.pubkey)
|
||||
let childKeyAlt1 = secp256k1.utils
|
||||
.bytesToHex(getChildPublicKey(currentKeyAlt1, hiddenKey))
|
||||
.slice(2)
|
||||
let childKeyAlt2 = secp256k1.utils
|
||||
.bytesToHex(getChildPublicKey(currentKeyAlt2, hiddenKey))
|
||||
.slice(2)
|
||||
|
||||
return childKeyAlt1 === invalidKey || childKeyAlt2 === invalidKey
|
||||
}
|
||||
Reference in New Issue
Block a user