You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
248 lines
9.9 KiB
248 lines
9.9 KiB
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 };
|
|
|