const { B32, SECP, deriveChildPubkey, ripemd160, hash160, toBech32, deriveAddresses } = require('/tmp/cripto/crypto.js'); const hex = a => Array.from(a).map(b=>b.toString(16).padStart(2,'0')).join(''); let hallazgos=[]; (async () => { console.log("=== 7. ¿Se verifica la checksum del xpub? ==="); // xpub válido con UN carácter cambiado al final (checksum rota) const bueno = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"; const malo = bueno.slice(0,-1) + (bueno.slice(-1)==="8" ? "9" : "8"); try { const r = B32.decodeBase58(malo); console.log(" ✗ ACEPTA un xpub con checksum inválida — no la comprueba"); hallazgos.push({sev:"medio", t:"No se verifica la checksum base58 del xpub", d:"decodeBase58 descarta los 4 bytes de checksum sin comprobarlos. Un xpub mal copiado (un carácter cambiado) se acepta y genera direcciones que no son las del usuario."}); } catch(e) { console.log(" ✓ rechaza:", e.message); } console.log("\n=== 8. ¿Se valida la longitud del xpub? ==="); try { const r = B32.decodeBase58("xpub661MyMwAqRbcFtXgS5sYJ"); // truncado console.log(` ✗ ACEPTA un xpub truncado → ${r.length} bytes (deberían ser 78)`); hallazgos.push({sev:"medio", t:"No se valida la longitud del xpub decodificado", d:"Un xpub truncado produce un array corto; chainCode y pubKey salen vacíos o parciales y la derivación falla de forma confusa o produce basura."}); } catch(e){ console.log(" ✓ rechaza:", e.message); } console.log("\n=== 9. ¿Se valida que la clave pública esté en la curva? ==="); // Punto que NO está en secp256k1: x=1 no tiene y entera para y²=x³+7 → 8 no es residuo try { const falso = Uint8Array.from([2, ...new Array(31).fill(0), 1]); // x=1 const pt = SECP.decompress(falso); const enCurva = (pt[1]*pt[1] - (pt[0]**3n + 7n)) % SECP.P === 0n; if (!enCurva) { console.log(" ✗ decompress DEVUELVE un punto que no está en la curva (no valida)"); hallazgos.push({sev:"medio", t:"decompress no comprueba que el punto esté en la curva", d:"Con una x que no corresponde a ningún punto de secp256k1, devuelve un par (x,y) inválido en vez de fallar. La derivación seguiría y produciría direcciones sin sentido. Solo alcanzable con un xpub manipulado."}); } else console.log(" ✓ el punto resultante sí está en la curva"); } catch(e){ console.log(" ✓ rechaza:", e.message); } console.log("\n=== 10. Índices endurecidos (no derivables desde xpub) ==="); const raw=B32.decodeBase58(bueno), ch=raw.slice(13,45), pb=raw.slice(45,78); try { await deriveChildPubkey(pb, ch, 0x80000000); console.log(" ✗ ACEPTA un índice endurecido — matemáticamente imposible desde una clave pública"); hallazgos.push({sev:"bajo", t:"No se rechaza el índice endurecido en deriveChildPubkey", d:"Un índice ≥ 0x80000000 no se puede derivar desde una clave pública. Hoy no se llama nunca con esos valores (deriveAddresses usa 0 y 1), así que no es explotable, pero la función no se defiende sola."}); } catch(e){ console.log(" ✓ rechaza:", e.message); } console.log("\n=== 11. Consistencia: 100 direcciones seguidas ==="); const a = await deriveAddresses(bueno, 100); const todas = [...a.receive, ...a.change]; const unicas = new Set(todas); console.log(` direcciones generadas: ${todas.length}, únicas: ${unicas.size}`); console.log(` todas empiezan por bc1q: ${todas.every(x=>x.startsWith("bc1q"))}`); console.log(` longitud correcta (42): ${todas.every(x=>x.length===42)}`); if (unicas.size !== todas.length) hallazgos.push({sev:"alto", t:"Direcciones duplicadas en la derivación", d:"Dos índices distintos producen la misma dirección."}); console.log("\n=== 12. Detección de red (mainnet vs testnet) ==="); for (const [p,esperado] of [["xpub","bc"],["zpub","bc"],["ypub","bc"],["tpub","?"],["vpub","tb"],["upub","tb"]]) { const hrp = p.startsWith("tb")||p.startsWith("u")||p.startsWith("v") ? "tb":"bc"; const marca = (p==="tpub" && hrp==="bc") ? " ✗" : " ·"; console.log(`${marca} ${p} → ${hrp}`); } hallazgos.push({sev:"medio", t:"tpub (testnet) se trata como mainnet", d:"La detección mira si empieza por 'tb', 'u' o 'v'. Un tpub —el formato más común de testnet— empieza por 't' y NO por 'tb', así que cae en la rama de mainnet y genera direcciones bc1... a partir de claves de testnet."}); console.log("\n\n════ HALLAZGOS ════"); for (const h of hallazgos) console.log(`\n[${h.sev.toUpperCase()}] ${h.t}\n ${h.d}`); if (!hallazgos.length) console.log("ninguno"); })();