bugfix: signing is made off radio

This commit is contained in:
forcer
2025-10-21 19:43:16 +02:00
parent 3d8d3de968
commit a235128d24
3 changed files with 237 additions and 5 deletions

View File

@@ -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...');
});

229
supercop/index.mjs Normal file
View File

@@ -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);
}

BIN
supercop/supercop.wasm Normal file

Binary file not shown.