130 lines
3.6 KiB
JavaScript
130 lines
3.6 KiB
JavaScript
function generatePeerId() {
|
|
const bytes = new Uint8Array(8);
|
|
crypto.getRandomValues(bytes);
|
|
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
async function generateSecret() {
|
|
const secret = new Uint8Array(32);
|
|
crypto.getRandomValues(secret);
|
|
return secret;
|
|
}
|
|
|
|
async function commit(secret) {
|
|
const hash = await crypto.subtle.digest('SHA-256', secret);
|
|
return new Uint8Array(hash);
|
|
}
|
|
|
|
function arraysEqual(a, b) {
|
|
if (a.length !== b.length) return false;
|
|
let diff = 0;
|
|
for (let i = 0; i < a.length; i++) {
|
|
diff |= a[i] ^ b[i];
|
|
}
|
|
return diff === 0;
|
|
}
|
|
|
|
function combineAll(secrets) {
|
|
const result = new Uint8Array(32);
|
|
for (const secret of secrets) {
|
|
for (let i = 0; i < 32; i++) {
|
|
result[i] ^= secret[i];
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function computePeerResult(sharedSeed, peerId) {
|
|
const data = new TextEncoder().encode(peerId);
|
|
const combined = new Uint8Array(sharedSeed.length + data.length);
|
|
combined.set(sharedSeed);
|
|
combined.set(data, sharedSeed.length);
|
|
const hash = await crypto.subtle.digest('SHA-256', combined);
|
|
return new Uint8Array(hash)[0] & 1;
|
|
}
|
|
|
|
function bufToBase64(buf) {
|
|
const bytes = new Uint8Array(buf);
|
|
let binary = '';
|
|
for (let i = 0; i < bytes.length; i++) {
|
|
binary += String.fromCharCode(bytes[i]);
|
|
}
|
|
return btoa(binary);
|
|
}
|
|
|
|
function base64ToBuf(base64) {
|
|
const binary = atob(base64);
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let i = 0; i < binary.length; i++) {
|
|
bytes[i] = binary.charCodeAt(i);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
function hexToBuf(hex) {
|
|
const bytes = new Uint8Array(hex.length / 2);
|
|
for (let i = 0; i < hex.length; i += 2) {
|
|
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
/* ─── QR compression: deflate-raw + base45 ─── */
|
|
|
|
function stripSDP(sdp) {
|
|
return sdp.split(/\r?\n/).map(line => {
|
|
if (line === '' || line.startsWith('a=max-message-size:') || line.startsWith('a=sctp-port:')) return null;
|
|
if (!line.startsWith('a=candidate:')) return line;
|
|
const parts = line.split(' ');
|
|
if (parts[2] === 'tcp') return null;
|
|
if (parts[7] !== 'host') return null;
|
|
if (parts[4] && parts[4].includes(':')) return null;
|
|
return line;
|
|
}).filter(Boolean).join('\r\n');
|
|
}
|
|
|
|
async function pack(obj) {
|
|
const json = JSON.stringify(obj);
|
|
const blob = new Blob([json]);
|
|
const cs = new CompressionStream('deflate-raw');
|
|
const ab = await new Response(blob.stream().pipeThrough(cs)).arrayBuffer();
|
|
return new Uint8Array(ab);
|
|
}
|
|
|
|
async function unpack(bytes) {
|
|
const blob = new Blob([bytes]);
|
|
const ds = new DecompressionStream('deflate-raw');
|
|
const ab = await new Response(blob.stream().pipeThrough(ds)).arrayBuffer();
|
|
return JSON.parse(new TextDecoder().decode(ab));
|
|
}
|
|
|
|
const B45 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
|
|
|
|
function encode45(buf) {
|
|
const bytes = new Uint8Array(buf);
|
|
let s = '';
|
|
for (let i = 0; i < bytes.length; i += 2) {
|
|
if (i + 1 < bytes.length) {
|
|
const v = bytes[i] * 256 + bytes[i + 1];
|
|
s += B45[v % 45] + B45[Math.floor(v / 45) % 45] + B45[Math.floor(v / 2025)];
|
|
} else {
|
|
const v = bytes[i];
|
|
s += B45[v % 45] + B45[Math.floor(v / 45)];
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
function decode45(str) {
|
|
const bytes = [];
|
|
for (let i = 0; i < str.length; i += 3) {
|
|
if (i + 2 < str.length) {
|
|
const v = B45.indexOf(str[i]) + B45.indexOf(str[i + 1]) * 45 + B45.indexOf(str[i + 2]) * 2025;
|
|
bytes.push(Math.floor(v / 256), v % 256);
|
|
} else {
|
|
const v = B45.indexOf(str[i]) + B45.indexOf(str[i + 1]) * 45;
|
|
bytes.push(v);
|
|
}
|
|
}
|
|
return Uint8Array.from(bytes);
|
|
}
|