fix: endurecer la validación del xpub tras revisar la criptografía
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.
This commit is contained in:
@@ -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
|
||||
|
||||
+56
-6
@@ -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"]]) {
|
||||
|
||||
@@ -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.
|
||||
@@ -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`);
|
||||
})();
|
||||
@@ -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`);
|
||||
})();
|
||||
@@ -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");
|
||||
})();
|
||||
@@ -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`);
|
||||
})();
|
||||
Reference in New Issue
Block a user