diff --git a/index.mjs b/index.mjs index 17a9724..15d6e56 100644 --- a/index.mjs +++ b/index.mjs @@ -4,6 +4,7 @@ import { Advert } from '@liamcottle/meshcore.js'; +import { KeyPair } from './supercop/index.mjs'; import crypto from 'crypto'; const device = process.argv[2] ?? '/dev/ttyACM0'; @@ -11,13 +12,13 @@ const apiURL = 'https://map.meshcore.dev/api/v1/uploader/node'; const seenAdverts = {}; let clientInfo = {}; -const signData = async (connection, data) => { +const signData = async (kp, data) => { const json = JSON.stringify(data); const dataHash = new Uint8Array( await crypto.subtle.digest('SHA-256', new TextEncoder().encode(json)) ); - return { data: json, signature: BufferUtils.bytesToHex(await connection.sign(dataHash)) } + return { data: json, signature: BufferUtils.bytesToHex(await kp.sign(dataHash)) } } const processPacket = async (connection, rawPacket) => { @@ -26,7 +27,7 @@ const processPacket = async (connection, rawPacket) => { if(packet.payload_type_string !== 'ADVERT') return; const advert = Advert.fromBytes(packet.payload); - // console.log(advert); + // console.debug('DEBUG: got advert', advert); if(advert.parsed.type === 'CHAT') return; const pubKey = BufferUtils.bytesToHex(advert.publicKey); @@ -59,7 +60,7 @@ const processPacket = async (connection, rawPacket) => { links: [`meshcore://${BufferUtils.bytesToHex(rawPacket)}`] }; - const requestData = await signData(connection, data); + const requestData = await signData(clientInfo.kp, data); requestData.publicKey = BufferUtils.bytesToHex(clientInfo.publicKey); const req = await fetch(apiURL, { @@ -67,6 +68,8 @@ const processPacket = async (connection, rawPacket) => { body: JSON.stringify(requestData) }); + // console.debug('DEBUG: sent request', req); + console.log('sending', requestData) console.log(await req.json()); @@ -89,7 +92,7 @@ connection.on('connected', async () => { connection.setManualAddContacts(); clientInfo = await connection.getSelfInfo(); - // console.log('info', clientInfo); + clientInfo.kp = KeyPair.from({ publicKey: clientInfo.publicKey, secretKey: (await connection.exportPrivateKey()).privateKey }); console.log('Map uploader waiting for adverts...'); }); diff --git a/supercop/index.mjs b/supercop/index.mjs new file mode 100644 index 0000000..2d1e928 --- /dev/null +++ b/supercop/index.mjs @@ -0,0 +1,229 @@ +import fs from 'node:fs'; + +const Module = (async () => { + const memory = new WebAssembly.Memory({ initial: 4 }); + const imports = { env: { memory } }; + const program = await WebAssembly.instantiate(fs.readFileSync('./supercop/supercop.wasm'), imports); + + return { + memory: memory, + instance: program.instance, + exports: program.instance.exports, + }; +})(); + +function toBuffer(data) { + return data instanceof Buffer ? data : Buffer.from(data); +} + +function randomBytes(length) { + return Buffer.from(new Array(length).fill(0).map(() => Math.floor(Math.random() * 256))); +} + +export function createSeed() { + return randomBytes(32); +} + +export function isSeed(data) { + return data.length === 32; +} + +export function isPublicKey(data) { + return data.length === 32; +} + +export function isSignature(data) { + return data.length === 64; +} + +export function isSecretKey(data) { + return data.length === 64; +} + +export class KeyPair { + publicKey; + secretKey; + constructor() { + // Intentionally empty + } + // Passes signing on to the exported stand-alone method + // Async, so the error = promise rejection + async sign(message) { + if (!isSecretKey(this.secretKey)) + throw new Error('No secret key on this keypair, only verification is possible'); + if (!isPublicKey(this.publicKey)) + throw new Error('Invalid public key'); + return sign(message, this.publicKey, this.secretKey); + } + // Passes verification on to the exported stand-alone method + verify(signature, message) { + if (!isPublicKey(this.publicKey)) + throw new Error('Invalid public key'); + return verify(signature, message, this.publicKey); + } + keyExchange(theirPublicKey) { + if (!isSecretKey(this.secretKey)) + throw new Error('Invalid secret key'); + return keyExchange(theirPublicKey, this.secretKey); + } + toJSON() { + return { + publicKey: this.publicKey ? [...this.publicKey] : undefined, + secretKey: this.secretKey ? [...this.secretKey] : undefined, + }; + } + static create(seed) { + return createKeyPair(seed); + } + static from(data) { + return keyPairFrom(data); + } +} + +export function keyPairFrom(data) { + if (typeof data !== 'object') + throw new Error('Invalid input data'); + // Sanitization and sanity checking + data = { + publicKey: toBuffer(data.publicKey), + secretKey: toBuffer(data.secretKey) + }; + + if (!isPublicKey(data.publicKey)) + throw new Error('Invalid public key'); + // Not checking the secretKey, allowed to be missing + const keypair = new KeyPair(); + Object.assign(keypair, data); + + return keypair; +} + +export async function createKeyPair(seed) { + // Pre-fetch module components + const fn = (await Module).exports; + const mem = (await Module).memory; + // Ensure we have a valid seed + if (Array.isArray(seed)) + seed = Buffer.from(seed); + if (!isSeed(seed)) + throw new Error('Invalid seed'); + // Reserve wasm-side memory + const seedPtr = fn._malloc(32); + const publicKeyPtr = fn._malloc(32); + const secretKeyPtr = fn._malloc(64); + const seedBuf = new Uint8Array(mem.buffer, seedPtr, 32); + const publicKey = new Uint8Array(mem.buffer, publicKeyPtr, 32); + const secretKey = new Uint8Array(mem.buffer, secretKeyPtr, 64); + seedBuf.set(seed); + fn.create_keypair(publicKeyPtr, secretKeyPtr, seedPtr); + fn._free(seedPtr); + fn._free(publicKeyPtr); + fn._free(secretKeyPtr); + + return keyPairFrom({ + publicKey: Buffer.from(publicKey), + secretKey: Buffer.from(secretKey), + }); +} + +export async function sign(message, publicKey, secretKey) { + // Pre-fetch module components + const fn = (await Module).exports; + const mem = (await Module).memory; + + // Sanitization and sanity checking + if (Array.isArray(publicKey)) + publicKey = Buffer.from(publicKey); + if (Array.isArray(secretKey)) + secretKey = Buffer.from(secretKey); + if (!isPublicKey(publicKey)) + throw new Error('Invalid public key'); + if (!isSecretKey(secretKey)) + throw new Error('Invalid secret key'); + if ('string' === typeof message) + message = Buffer.from(message); + // Allocate memory on the wasm side to transfer variables + const messageLen = message.length; + const messageArrPtr = fn._malloc(messageLen); + const messageArr = new Uint8Array(mem.buffer, messageArrPtr, messageLen); + const publicKeyArrPtr = fn._malloc(32); + const publicKeyArr = new Uint8Array(mem.buffer, publicKeyArrPtr, 32); + const secretKeyArrPtr = fn._malloc(64); + const secretKeyArr = new Uint8Array(mem.buffer, secretKeyArrPtr, 64); + const sigPtr = fn._malloc(64); + const sig = new Uint8Array(mem.buffer, sigPtr, 64); + messageArr.set(message); + publicKeyArr.set(publicKey); + secretKeyArr.set(secretKey); + await fn.sign(sigPtr, messageArrPtr, messageLen, publicKeyArrPtr, secretKeyArrPtr); + // Free used memory on wasm side + fn._free(messageArrPtr); + fn._free(publicKeyArrPtr); + fn._free(secretKeyArrPtr); + fn._free(sigPtr); + + return Buffer.from(sig); +} +export async function verify(signature, message, publicKey) { + const fn = (await Module).exports; + const mem = (await Module).memory; + + // Sanitization and sanity checking + if (Array.isArray(signature)) + signature = Buffer.from(signature); + if (Array.isArray(publicKey)) + publicKey = Buffer.from(publicKey); + if (!isPublicKey(publicKey)) + throw new Error('Invalid public key'); + if (!isSignature(signature)) + throw new Error('Invalid signature'); + if ('string' === typeof message) + message = Buffer.from(message); + // Allocate memory on the wasm side to transfer variables + const messageLen = message.length; + const messageArrPtr = fn._malloc(messageLen); + const messageArr = new Uint8Array(mem.buffer, messageArrPtr, messageLen); + const signatureArrPtr = fn._malloc(64); + const signatureArr = new Uint8Array(mem.buffer, signatureArrPtr, 64); + const publicKeyArrPtr = fn._malloc(32); + const publicKeyArr = new Uint8Array(mem.buffer, publicKeyArrPtr, 32); + messageArr.set(message); + signatureArr.set(signature); + publicKeyArr.set(publicKey); + const res = fn.verify(signatureArrPtr, messageArrPtr, messageLen, publicKeyArrPtr) === 1; + // Free used memory on wasm side + fn._free(messageArrPtr); + fn._free(signatureArrPtr); + fn._free(publicKeyArrPtr); + + return res; +} +export async function keyExchange(theirPublicKey, ourSecretKey) { + const fn = (await Module).exports; + const mem = (await Module).memory; + + // Sanitization and sanity checking + if (Array.isArray(theirPublicKey)) + theirPublicKey = Buffer.from(theirPublicKey); + if (Array.isArray(ourSecretKey)) + ourSecretKey = Buffer.from(ourSecretKey); + if (!isPublicKey(theirPublicKey)) + throw new Error('Invalid public key'); + if (!isSecretKey(ourSecretKey)) + throw new Error('Invalid secret key'); + // Allocate memory on the wasm side to transfer variables + const sharedSecretArrPtr = fn._malloc(32); + const sharedSecretArr = new Uint8Array(mem.buffer, sharedSecretArrPtr, 32); + const publicKeyArrPtr = fn._malloc(32); + const publicKeyArr = new Uint8Array(mem.buffer, publicKeyArrPtr, 32); + const secretKeyArrPtr = fn._malloc(64); + const secretKeyArr = new Uint8Array(mem.buffer, secretKeyArrPtr, 64); + publicKeyArr.set(theirPublicKey); + secretKeyArr.set(ourSecretKey); + fn.key_exchange(sharedSecretArrPtr, publicKeyArrPtr, secretKeyArrPtr); + // Free used memory on wasm side + fn._free(sharedSecretArrPtr); + fn._free(publicKeyArrPtr); + fn._free(secretKeyArrPtr); + return Buffer.from(sharedSecretArr); +} diff --git a/supercop/supercop.wasm b/supercop/supercop.wasm new file mode 100644 index 0000000..42de268 Binary files /dev/null and b/supercop/supercop.wasm differ