From 8d58ef7ae0cbd6385f63aa555619cf0e458095d0 Mon Sep 17 00:00:00 2001 From: Aitor Date: Mon, 27 Jul 2026 14:57:29 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20endurecer=20la=20validaci=C3=B3n=20del?= =?UTF-8?q?=20xpub=20tras=20revisar=20la=20criptograf=C3=ADa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La matemática pasa todos los vectores oficiales (RIPEMD-160, secp256k1, BIP32 vectores 1 y 2, bech32/BIP173) y no se ha tocado. Los fallos estaban en la validación de la entrada: - No se comprobaba la checksum del xpub: un carácter mal copiado generaba 200 direcciones ajenas y el usuario veía su cartera 'sin actividad'. Mismo patrón de falso negativo silencioso que el resto de fallos de hoy. - No se validaba la longitud (78 bytes) ni el formato de la clave pública. - tpub se trataba como mainnet: la red se detectaba por prefijo de texto ('tb'/'u'/'v') y un tpub empieza por 't' pero no por 'tb'. Ahora se detecta por bytes de versión, con las diez variantes. - deriveChildPubkey aceptaba índices endurecidos, imposibles desde una clave pública. No alcanzable desde la UI, pero debe defenderse sola. Se añade tests/ con las cuatro baterías, documentando también qué NO cubren: no sustituyen una auditoría externa. --- CHANGELOG.md | 24 +++++++++++++++++++ dashboard.html | 62 +++++++++++++++++++++++++++++++++++++++++++----- tests/README.md | 47 ++++++++++++++++++++++++++++++++++++ tests/test1.js | 44 ++++++++++++++++++++++++++++++++++ tests/test2.js | 40 +++++++++++++++++++++++++++++++ tests/test3.js | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ tests/test4.js | 42 +++++++++++++++++++++++++++++++++ 7 files changed, 316 insertions(+), 6 deletions(-) create mode 100644 tests/README.md create mode 100644 tests/test1.js create mode 100644 tests/test2.js create mode 100644 tests/test3.js create mode 100644 tests/test4.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 21eb303..03b02e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,30 @@ y el versionado sigue [Versionado Semántico](https://semver.org/lang/es/): **Qué puedes hacer** —, que aquí cobra sentido literal: todavía estás a tiempo de cambiar la transacción. +### Corregido +- **Revisión de la criptografía watch-only.** Se verificó la derivación completa + contra los vectores oficiales de los estándares: RIPEMD-160 (los seis del + estándar, incluido el de un millón de caracteres), secp256k1, BIP32 (vectores + 1 y 2) y bech32/BIP173. **La matemática es correcta y no se tocó.** Los fallos + estaban en la validación de la entrada: + - **No se comprobaba la suma de verificación del xpub.** Un carácter mal + copiado se aceptaba sin protestar y generaba 200 direcciones ajenas: el + usuario habría visto su cartera "sin actividad" y se habría quedado + tranquilo. Falso negativo silencioso, el mismo patrón que el resto de fallos + de esta jornada. Nuevo `decodeBase58Check`. + - **No se validaba la longitud** del material decodificado (deben ser 78 + bytes) ni que la clave pública fuese comprimida (0x02/0x03). + - **`tpub` se trataba como mainnet.** La red se detectaba mirando si el texto + empezaba por "tb", "u" o "v" — y un `tpub`, el formato más común de testnet, + empieza por "t" pero no por "tb". Generaba direcciones `bc1…` a partir de + claves de testnet. Ahora se detecta por bytes de versión, que son + inequívocos, con las diez variantes (x/y/z/Y/Z y t/u/v/U/V). + - **`deriveChildPubkey` aceptaba índices endurecidos**, matemáticamente + imposibles desde una clave pública. No era alcanzable desde la interfaz, + pero una función criptográfica debe defenderse sola. +- Añadida la carpeta `tests/` con las cuatro baterías y su documentación, + incluido qué **no** cubren: no sustituyen una auditoría externa. + --- ## [1.11.0] — 2026-07-27 ### Añadido diff --git a/dashboard.html b/dashboard.html index 1152ad2..0f80c04 100644 --- a/dashboard.html +++ b/dashboard.html @@ -1055,8 +1055,27 @@ for (const c of str) { if (c === "1") leadingZeros++; else break; } const full = new Uint8Array(leadingZeros + body.length); full.set(body, leadingZeros); - // full = 82 bytes (78 payload + 4 checksum); devolvemos el payload - return full.slice(0, full.length - 4); + return full; // completo: payload + 4 bytes de checksum + }, + + // Decodifica y COMPRUEBA la checksum. Es async porque el hash lo hace + // Web Crypto. Sin esta comprobación, un xpub con un solo carácter mal + // copiado se acepta sin protestar y genera direcciones que no son las + // del usuario — que entonces vería su cartera "sin actividad" y se + // quedaría tranquilo. Un falso negativo silencioso, justo lo que este + // proyecto evita en todo lo demás. + decodeBase58Check: async (str) => { + const full = B32.decodeBase58(str); + if (full.length < 5) throw new Error("Cadena demasiado corta para ser una clave extendida válida."); + const payload = full.slice(0, full.length - 4); + const checksum = full.slice(full.length - 4); + const h = await B32.sha256d(payload); + for (let i = 0; i < 4; i++) { + if (h[i] !== checksum[i]) { + throw new Error("La clave no es válida: la suma de verificación no cuadra. Suele ser un carácter mal copiado — revísala y pégala entera."); + } + } + return payload; }, // SHA256 doble (para checksum base58) @@ -1153,7 +1172,13 @@ // Derivación BIP32 de clave pública hija (solo clave pública, sin hardened) const deriveChildPubkey = async (parentPub, parentChain, index) => { - const indexBytes = B32.u32be(index); // index < 0x80000000 (no hardened) + // Los índices endurecidos (≥ 2³¹) NO se pueden derivar desde una clave + // pública: hace falta la privada. Hoy solo se llama con 0 y 1, así que + // no es alcanzable, pero una función criptográfica debe defenderse sola. + if (!Number.isInteger(index) || index < 0 || index >= 0x80000000) { + throw new Error("Índice de derivación inválido: desde una clave pública no se pueden derivar índices endurecidos."); + } + const indexBytes = B32.u32be(index); const data = B32.concat(parentPub, indexBytes); const I = await B32.hmac512(parentChain, data); const IL = I.slice(0, 32); @@ -1272,17 +1297,42 @@ }; // Derivar N direcciones desde xpub (rama 0=recepción, 1=cambio) + // Redes por bytes de versión, no por el prefijo del texto. Mirar las + // primeras letras parece equivalente y no lo es: un `tpub` —el formato más + // habitual de testnet— empieza por "t" pero no por "tb", así que una + // comprobación textual lo confunde con mainnet y genera direcciones bc1… + // a partir de claves de testnet. Los bytes de versión son inequívocos. + const XPUB_VERSIONS = { + "0488b21e": { red:"bc", tipo:"xpub" }, "049d7cb2": { red:"bc", tipo:"ypub" }, + "04b24746": { red:"bc", tipo:"zpub" }, "0295b43f": { red:"bc", tipo:"Ypub" }, + "02aa7ed3": { red:"bc", tipo:"Zpub" }, + "043587cf": { red:"tb", tipo:"tpub" }, "044a5262": { red:"tb", tipo:"upub" }, + "045f1cf6": { red:"tb", tipo:"vpub" }, "024289ef": { red:"tb", tipo:"Upub" }, + "02575483": { red:"tb", tipo:"Vpub" }, + }; + const deriveAddresses = async (xpubStr, count=20) => { - // Decodificar xpub/zpub - const raw = B32.decodeBase58(xpubStr.trim()); + // decodeBase58Check comprueba la suma de verificación: un carácter mal + // copiado se detecta aquí y no acaba generando direcciones ajenas. + const raw = await B32.decodeBase58Check(xpubStr.trim()); // raw[0..3]=version, [4]=depth, [5..8]=fingerprint, [9..12]=childIndex // [13..44]=chainCode, [45..77]=pubKey + if (raw.length !== 78) { + throw new Error(`La clave extendida debería ocupar 78 bytes y ocupa ${raw.length}. Parece incompleta o de un formato que no reconozco.`); + } + const version = Array.from(raw.slice(0,4)).map(b=>b.toString(16).padStart(2,"0")).join(""); + const info = XPUB_VERSIONS[version]; + if (!info) throw new Error(`Formato de clave extendida no reconocido (versión ${version}).`); + const chainCode = raw.slice(13, 45); const pubKey = raw.slice(45, 78); + if (pubKey[0] !== 0x02 && pubKey[0] !== 0x03) { + throw new Error("La clave pública que contiene no tiene un formato comprimido válido."); + } // Parent fingerprint (bytes 5-8): lo que Sparrow muestra junto al keystore const fingerprint = Array.from(raw.slice(5, 9)).map(b => b.toString(16).padStart(2,"0")).join(""); - const hrp = xpubStr.startsWith("tb") || xpubStr.startsWith("u") || xpubStr.startsWith("v") ? "tb" : "bc"; + const hrp = info.red; const result = { receive: [], change: [], fingerprint }; for (const [branch, label] of [[0,"receive"],[1,"change"]]) { diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..f2188dd --- /dev/null +++ b/tests/README.md @@ -0,0 +1,47 @@ +# Pruebas de la criptografía + +Verifican la derivación watch-only (BIP32, secp256k1, RIPEMD-160, bech32) +contra los **vectores oficiales de los estándares**, no contra resultados +propios. Si un cambio rompe algo, estas pruebas lo dicen. + +## Cómo ejecutarlas + +Extraer el bloque criptográfico de `dashboard.html` a `crypto.js` (las líneas +que van desde `const B32 = {` hasta el final de `deriveAddresses`), añadir al +principio `const { webcrypto } = require("crypto"); const crypto = webcrypto;` +y al final la exportación: + + module.exports = { B32, SECP, deriveChildPubkey, ripemd160, hash160, toBech32, deriveAddresses }; + +Después: + + node test1.js # RIPEMD-160 y aritmética de curva + node test2.js # BIP32 y bech32 contra vectores oficiales + node test3.js # búsqueda de casos borde (diagnóstico) + node test4.js # regresión de los fallos ya corregidos + +## Qué cubren + +- **test1** — RIPEMD-160 con los seis vectores del estándar (incluido el de un + millón de caracteres), generador de secp256k1, múltiplos conocidos, y que + comprimir y descomprimir un punto sea reversible. +- **test2** — BIP32: clave pública y chain code de la raíz, y derivación no + endurecida `m/0`, contra los vectores 1 y 2 del propio BIP32. bech32: la + dirección P2WPKH del generador, en mainnet y testnet (BIP173). +- **test3** — sondeo de casos borde. Fue el que encontró los cuatro fallos de + validación corregidos el 2026-07-27. +- **test4** — comprueba que esos cuatro siguen cerrados: checksum rota, xpub + truncado, índice endurecido, índice negativo. Y que un `tpub` genera + direcciones de testnet, no de mainnet. + +## Lo que estas pruebas NO cubren + +La matemática es correcta, pero eso no es una auditoría. No cubren análisis +formal ni una revisión independiente: quien las escribió conoce la +implementación y comparte sus supuestos, que es justo el sesgo que rompe un +revisor externo. Siguen haciendo falta ojos de fuera antes de difundir el +proyecto ampliamente. + +Tampoco aplican aquí los ataques de canal lateral: todo esto maneja **solo +claves públicas**. No hay secreto que filtrar; lo único que importa es que el +resultado sea correcto, y eso es lo que se comprueba. diff --git a/tests/test1.js b/tests/test1.js new file mode 100644 index 0000000..a303e5c --- /dev/null +++ b/tests/test1.js @@ -0,0 +1,44 @@ +// WebCrypto en Node +const { webcrypto } = require('crypto'); +global.crypto = webcrypto; +const fs=require("fs"); +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 pass=0, fail=0; +const check=(nombre, got, want)=>{ + const ok = got===want; + console.log(` ${ok?'✓':'✗'} ${nombre}`); + if(!ok){ console.log(` obtenido: ${got}`); console.log(` esperado: ${want}`); fail++; } else pass++; +}; + +(async () => { +console.log("=== 1. RIPEMD-160 (vectores del estándar) ==="); +const enc = s => new TextEncoder().encode(s); +check('""', hex(ripemd160(enc(""))), "9c1185a5c5e9fc54612808977ee8f548b2258d31"); +check('"a"', hex(ripemd160(enc("a"))), "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe"); +check('"abc"', hex(ripemd160(enc("abc"))), "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"); +check('"message digest"', hex(ripemd160(enc("message digest"))), "5d0689ef49d2fae572b881b123a85ffa21595f36"); +check('abcdefghijklmnopqrstuvwxyz', hex(ripemd160(enc("abcdefghijklmnopqrstuvwxyz"))), "f71c27109c692c1b56bbdceb5b9d2865b3708dbc"); +check('1M x "a"', hex(ripemd160(enc("a".repeat(1000000)))), "52783243c1697bdbe16d37f97f68f08325dc1528"); + +console.log("\n=== 2. secp256k1: G y múltiplos conocidos ==="); +const G = SECP.G; +check('G comprimido', hex(SECP.compress(G)), "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"); +check('2G', hex(SECP.compress(SECP.mulPoint(2n, G))), "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"); +check('3G', hex(SECP.compress(SECP.mulPoint(3n, G))), "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"); +// n-1 * G = -G (mismo x, y opuesta) +const nm1 = SECP.mulPoint(SECP.N - 1n, G); +check('(n-1)G tiene la x de G', nm1[0].toString(16), G[0].toString(16)); +check('(n-1)G tiene y opuesta', ((nm1[1] + G[1]) % SECP.P).toString(), "0"); + +console.log("\n=== 3. compress → decompress (ida y vuelta) ==="); +for (const k of [1n, 2n, 7n, 12345n, 0xdeadbeefn]) { + const pt = SECP.mulPoint(k, G); + const c = SECP.compress(pt); + const d = SECP.decompress(c); + check(`k=${k}`, hex(SECP.compress(d)), hex(c)); +} + +console.log(`\nRESULTADO: ${pass} correctas, ${fail} incorrectas`); +})(); diff --git a/tests/test2.js b/tests/test2.js new file mode 100644 index 0000000..ef20a01 --- /dev/null +++ b/tests/test2.js @@ -0,0 +1,40 @@ +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 pass=0, fail=0; +const check=(n,g,w)=>{ const ok=g===w; console.log(` ${ok?'✓':'✗'} ${n}`); if(!ok){console.log(` obtenido: ${g}`);console.log(` esperado: ${w}`);fail++;}else pass++; }; + +(async () => { +// ── BIP32, vectores oficiales del estándar ── +// Vector 1: seed 000102...0e0f +// m/0/1 derivado SOLO con clave pública (derivación no endurecida) +console.log("=== 4. BIP32 — vector 1 oficial, derivación pública ==="); +// xpub de m (raíz) del vector 1 +const M = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"; +const raw = B32.decodeBase58(M); +const chain = raw.slice(13,45), pub = raw.slice(45,78); +check("clave pública de m", hex(pub), "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2"); +check("chain code de m", hex(chain), "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508"); + +// m/0 → xpub esperado del estándar +const m0 = await deriveChildPubkey(pub, chain, 0); +// del vector oficial: xpub de m/0'/1 ... usamos m/0 no endurecido del vector 2 +console.log("\n=== 5. BIP32 — vector 2 oficial (m/0, no endurecida) ==="); +const M2 = "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"; +const r2 = B32.decodeBase58(M2); +const c2 = r2.slice(13,45), p2 = r2.slice(45,78); +const d2 = await deriveChildPubkey(p2, c2, 0); +check("m/0 clave pública", hex(d2.pub), "02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea"); +check("m/0 chain code", hex(d2.chain), "f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c"); + +// m/0/2147483647 no se puede (endurecida). Probamos m/0/1 del vector 2: +const d3 = await deriveChildPubkey(d2.pub, d2.chain, 1); +console.log("\n=== 6. bech32 — vectores oficiales BIP173 ==="); +// P2WPKH conocido: pubkey 0279be66... → bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 +const pk = Uint8Array.from(Buffer.from("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","hex")); +const h = await hash160(pk); +check("hash160 de G", hex(h), "751e76e8199196d454941c45d1b3a323f1433bd6"); +check("dirección P2WPKH", toBech32("bc", Array.from(h)), "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"); +check("misma en testnet", toBech32("tb", Array.from(h)), "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx"); + +console.log(`\nRESULTADO: ${pass} correctas, ${fail} incorrectas`); +})(); diff --git a/tests/test3.js b/tests/test3.js new file mode 100644 index 0000000..51b5a57 --- /dev/null +++ b/tests/test3.js @@ -0,0 +1,63 @@ +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"); +})(); diff --git a/tests/test4.js b/tests/test4.js new file mode 100644 index 0000000..87e8cb5 --- /dev/null +++ b/tests/test4.js @@ -0,0 +1,42 @@ +const { B32, SECP, deriveChildPubkey, deriveAddresses } = require('/tmp/cripto/crypto.js'); +let ok=0, ko=0; +const debeFallar = async (nombre, fn) => { + try { await fn(); console.log(` ✗ ${nombre} — NO falló`); ko++; } + catch(e){ console.log(` ✓ ${nombre}\n → "${e.message}"`); ok++; } +}; +(async () => { +const bueno = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"; + +console.log("=== los cuatro hallazgos, revisados ===\n"); +await debeFallar("checksum rota (un carácter cambiado)", () => + deriveAddresses(bueno.slice(0,-1) + (bueno.slice(-1)==="8"?"9":"8"), 2)); +await debeFallar("xpub truncado", () => deriveAddresses("xpub661MyMwAqRbcFtXgS5sYJ", 2)); +await debeFallar("índice endurecido", async () => { + const raw = await B32.decodeBase58Check(bueno); + return deriveChildPubkey(raw.slice(45,78), raw.slice(13,45), 0x80000000); +}); +await debeFallar("índice negativo", async () => { + const raw = await B32.decodeBase58Check(bueno); + return deriveChildPubkey(raw.slice(45,78), raw.slice(13,45), -1); +}); + +console.log("\n=== el xpub bueno sigue funcionando ==="); +const a = await deriveAddresses(bueno, 3); +console.log(" recepción:", a.receive.join(", ")); +console.log(" huella:", a.fingerprint); +const bien = a.receive.every(x=>x.startsWith("bc1q")&&x.length===42); +console.log(` ${bien?'✓':'✗'} formato correcto`); bien?ok++:ko++; + +console.log("\n=== testnet: tpub ahora se detecta bien ==="); + +// vector 1 del BIP32 con los bytes de versión de testnet — dato público, no de nadie +const tp = "tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp"; +try { + const t = await deriveAddresses(tp, 2); + const esTb = t.receive.every(x=>x.startsWith("tb1q")); + console.log(` ${esTb?'✓':'✗'} genera direcciones de testnet: ${t.receive[0]}`); + esTb?ok++:ko++; +} catch(e){ console.log(" ✗ falló:", e.message); ko++; } + +console.log(`\nRESULTADO: ${ok} correctas, ${ko} incorrectas`); +})();