Files
2025-09-29 07:21:46 -04:00

249 lines
9.9 KiB
JavaScript

function defineWasmEnv(label) {
label += ': ';
let AB_HEAP;
let ATU8_HEAP;
let ATU32_HEAP;
// eslint-disable-next-line no-console
const console_out = (s_channel, s_out) => console[s_channel](label + s_out.replace(/\0/g, '\n'));
let s_error = '';
// for converting bytes to text
const utf8 = new TextDecoder();
const h_fds = {
// stdout
1(s_out) {
console_out('debug', s_out);
},
// stderr
2(s_out) {
console_out('error', (s_error = s_out));
}
};
const imports = {
abort() {
throw Error(label + (s_error || 'An unknown error occurred'));
},
memcpy: (ip_dst, ip_src, nb_size) => ATU8_HEAP.copyWithin(ip_dst, ip_src, ip_src + nb_size),
resize(w) {
throw Error(label + `Out of memory (resizing ${w})`);
},
write(i_fd, ip_iov, nl_iovs, ip_written) {
// output string
let s_out = '';
// track number of bytes read from buffers
let cb_read = 0;
// each pending iov
for (let i_iov = 0; i_iov < nl_iovs; i_iov++) {
// start of buffer in memory
const ip_start = ATU32_HEAP[ip_iov >> 2];
// size of buffer
const nb_len = ATU32_HEAP[(ip_iov + 4) >> 2];
ip_iov += 8;
// extract text from buffer
s_out += utf8.decode(ATU8_HEAP.subarray(ip_start, ip_start + nb_len));
// update number of bytes read
cb_read += nb_len;
}
// route to fd
if (h_fds[i_fd]) {
h_fds[i_fd](s_out);
}
else {
// no fd found
throw new Error(`libsecp256k1 tried writing to non-open file descriptor: ${i_fd}\n${s_out}`);
}
// write bytes read
ATU32_HEAP[ip_written >> 2] = cb_read;
// no error
return 0;
}
};
return [
imports,
(d_memory) => [
(AB_HEAP = d_memory.buffer),
(ATU8_HEAP = new Uint8Array(AB_HEAP)),
(ATU32_HEAP = new Uint32Array(AB_HEAP))
]
];
}
/*
* ================================
* GENERATED FILE WARNING
* Do not edit this file manually.
* ================================
*/
const map_wasm_imports = (g_imports) => ({
a: {
a: g_imports.abort,
f: g_imports.memcpy,
d: g_imports.resize,
e: () => 52, // _fd_close,
c: () => 70, // _fd_seek,
b: g_imports.write,
},
});
const map_wasm_exports = (g_exports) => ({
malloc: g_exports['i'],
free: g_exports['j'],
sha256_initialize: g_exports['l'],
sha256_write: g_exports['m'],
sha256_finalize: g_exports['n'],
context_create: g_exports['o'],
xonly_pubkey_parse: g_exports['p'],
xonly_pubkey_serialize: g_exports['q'],
keypair_create: g_exports['r'],
keypair_xonly_pub: g_exports['s'],
schnorrsig_sign32: g_exports['t'],
schnorrsig_verify: g_exports['u'],
sbrk: g_exports['sbrk'],
memory: g_exports['g'],
init: () => g_exports['h'](),
});
/**
* Creates a new instance of the secp256k1 WASM and returns the Nostr wrapper
* @param z_src - a Response containing the WASM binary, a Promise that resolves to one,
* or the raw bytes to the WASM binary as a {@link BufferSource}
* @returns the wrapper API
*/
const NostrWasm = async (z_src) => {
// prepare the runtime
const [defs, f_bind_heap] = defineWasmEnv('nostr-wasm');
const g_imports = map_wasm_imports(defs);
// prep the wasm module
let d_wasm;
// instantiate wasm binary by streaming the response bytes
if (z_src instanceof Response || z_src instanceof Promise) {
d_wasm = await WebAssembly.instantiateStreaming(z_src, g_imports);
}
else {
// instantiate using raw binary
d_wasm = await WebAssembly.instantiate(z_src, g_imports);
}
// create the exports struct
const g_wasm = map_wasm_exports(d_wasm.instance.exports);
// bind the heap and ref its view(s)
const [, ATU8_HEAP] = f_bind_heap(g_wasm.memory);
// call into the wasm module's init method
g_wasm.init();
const ip_sk = g_wasm.malloc(32 /* ByteLens.PRIVATE_KEY */);
const ip_ent = g_wasm.malloc(32 /* ByteLens.NONCE_ENTROPY */);
const ip_msg_hash = g_wasm.malloc(32 /* ByteLens.MSG_HASH */);
// scratch spaces
const ip_pubkey_scratch = g_wasm.malloc(32 /* ByteLens.XONLY_PUBKEY */);
const ip_sig_scratch = g_wasm.malloc(64 /* ByteLens.BIP340_SIG */);
// library handle: secp256k1_keypair;
const ip_keypair = g_wasm.malloc(96 /* ByteLens.KEYPAIR_LIB */);
// library handle: secp256k1_xonly_pubkey;
const ip_xonly_pubkey = g_wasm.malloc(64 /* ByteLens.XONLY_KEY_LIB */);
// library handle: secp256k1_sha256;
const ip_sha256 = g_wasm.malloc(104 /* ByteLens.SHA256_LIB */);
// create a reusable context
const ip_ctx = g_wasm.context_create(513 /* Flags.CONTEXT_SIGN */ | 257 /* Flags.CONTEXT_VERIFY */);
// an encoder for hashing strings
const utf8 = new TextEncoder();
/**
* Puts the given private key into program memory, runs the given callback, then zeroes out the key
* @param atu8_sk - the private key
* @param f_use - callback to use the key
* @returns whatever the callback returns
*/
const with_keypair = (atu8_sk, f_use) => {
// prep callback return
let w_return;
// in case of any exception..
try {
// copy input bytes into place
ATU8_HEAP.set(atu8_sk, ip_sk);
// instantiate keypair
g_wasm.keypair_create(ip_ctx, ip_keypair, ip_sk);
// use private key
w_return = f_use();
}
finally {
// zero-out private key and keypair
ATU8_HEAP.fill(1, ip_sk, ip_sk + 32 /* ByteLens.PRIVATE_KEY */);
ATU8_HEAP.fill(2, ip_keypair, ip_keypair + 96 /* ByteLens.KEYPAIR_LIB */);
}
// forward result
return w_return;
};
const compute_event_id = (event) => {
const message = utf8.encode(`[0,"${event.pubkey}",${event.created_at},${event.kind},${JSON.stringify(event.tags)},${JSON.stringify(event.content)}]`);
const ip_message = g_wasm.malloc(message.length);
ATU8_HEAP.set(message, ip_message);
g_wasm.sha256_initialize(ip_sha256);
g_wasm.sha256_write(ip_sha256, ip_message, message.length);
g_wasm.sha256_finalize(ip_sha256, ip_msg_hash);
g_wasm.free(ip_message);
return ATU8_HEAP.slice(ip_msg_hash, ip_msg_hash + 32 /* ByteLens.MSG_HASH */);
};
return {
generateSecretKey: () => crypto.getRandomValues(new Uint8Array(32 /* ByteLens.PRIVATE_KEY */)),
getPublicKey(sk) {
if (1 /* BinaryResult.SUCCESS */ !==
with_keypair(sk, () => g_wasm.keypair_xonly_pub(ip_ctx, ip_xonly_pubkey, null, ip_keypair))) {
throw Error('failed to get pubkey from keypair');
}
// serialize the public key
g_wasm.xonly_pubkey_serialize(ip_ctx, ip_pubkey_scratch, ip_xonly_pubkey);
// extract result
return ATU8_HEAP.slice(ip_pubkey_scratch, ip_pubkey_scratch + 32 /* ByteLens.XONLY_PUBKEY */);
},
finalizeEvent(event, seckey, ent) {
with_keypair(seckey, () => {
// get public key (as in getPublicKey function above)
g_wasm.keypair_xonly_pub(ip_ctx, ip_xonly_pubkey, null, ip_keypair);
g_wasm.xonly_pubkey_serialize(ip_ctx, ip_pubkey_scratch, ip_xonly_pubkey);
const pubkey = ATU8_HEAP.slice(ip_pubkey_scratch, ip_pubkey_scratch + 32 /* ByteLens.XONLY_PUBKEY */);
event.pubkey = toHex(pubkey);
// compute event id
event.id = toHex(compute_event_id(event));
// copy entropy bytes into place, if they are provided
if (!ent && crypto.getRandomValues) {
ATU8_HEAP.set(crypto.getRandomValues(new Uint8Array(32)), ip_ent);
}
// perform signature (ip_msg_hash is already set from procedure above)
if (1 /* BinaryResult.SUCCESS */ !==
g_wasm.schnorrsig_sign32(ip_ctx, ip_sig_scratch, ip_msg_hash, ip_keypair, ip_ent)) {
throw Error('failed to sign');
}
});
const sig = ATU8_HEAP.slice(ip_sig_scratch, ip_sig_scratch + 64 /* ByteLens.BIP340_SIG */);
event.sig = toHex(sig);
},
verifyEvent(event) {
const id = fromHex(event.id);
// check event hash
const computed = compute_event_id(event);
for (let i = 0; i < id.length; i++) {
if (id[i] !== computed[i])
throw Error('id is invalid');
}
// copy event data into place
ATU8_HEAP.set(fromHex(event.sig), ip_sig_scratch);
ATU8_HEAP.set(fromHex(event.id), ip_msg_hash);
ATU8_HEAP.set(fromHex(event.pubkey), ip_pubkey_scratch);
// parse the public key
if (1 /* BinaryResult.SUCCESS */ !==
g_wasm.xonly_pubkey_parse(ip_ctx, ip_xonly_pubkey, ip_pubkey_scratch)) {
throw Error('pubkey is invalid');
}
// verify the signature
if (1 /* BinaryResult.SUCCESS */ !==
g_wasm.schnorrsig_verify(ip_ctx, ip_sig_scratch, ip_msg_hash, 32 /* ByteLens.MSG_HASH */, ip_xonly_pubkey)) {
throw Error('signature is invalid');
}
}
};
};
function toHex(bytes) {
return bytes.reduce((hex, byte) => hex + byte.toString(16).padStart(2, '0'), '');
}
function fromHex(hex) {
return new Uint8Array(hex.length / 2).map((_, i) => parseInt(hex.slice(i * 2, i * 2 + 2), 16));
}
export { NostrWasm as N };