// The browser wallet against the PHP one. // // Sirius Press has two independent implementations of the same cryptography: // PHP on the server, which verifies, and JavaScript in the page, which signs. // If they disagree by one byte, nobody can log in — and the error will look // like a rejected password rather than a hash mismatch, which is a miserable // thing to debug at three in the morning. // // So this compares them directly. Every vector is the output of the other // implementation, not of this one. // // node tests/interop.mjs // // Needs no dependencies: the wallet is loaded as plain source, with `window` // pointed at the Node global so WebCrypto is found where the browser puts it. import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; const here = dirname(fileURLToPath(import.meta.url)); const assets = join(here, "..", "plugins", "sirius-press-auth", "assets"); globalThis.window = globalThis; new Function(readFileSync(join(assets, "bip39-en.js"), "utf8"))(); new Function(readFileSync(join(assets, "wallet.js"), "utf8"))(); const W = window.SiriusWallet; const I = W._internals; const hex = (b) => [...b].map((x) => x.toString(16).padStart(2, "0")).join(""); let passed = 0; let failed = 0; function is(actual, expected, what) { if (actual === expected) { passed++; console.log(` ok ${what}`); } else { failed++; console.log(` FAIL ${what}`); console.log(` expected: ${expected}`); console.log(` actual: ${actual}`); } } function ok(condition, what) { is(Boolean(condition), true, what); } // --- RIPEMD-160, against the published vectors ------------------------------- // WebCrypto has no RIPEMD-160, so the wallet carries its own. These are the // reference values from the algorithm's own specification. console.log("\n RIPEMD-160"); const enc = new TextEncoder(); is(hex(I.ripemd160(new Uint8Array(0))), "9c1185a5c5e9fc54612808977ee8f548b2258d31", 'the empty string'); is(hex(I.ripemd160(enc.encode("abc"))), "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc", '"abc"'); is( hex(I.ripemd160(enc.encode("message digest"))), "5d0689ef49d2fae572b881b123a85ffa21595f36", '"message digest"', ); is( hex(I.ripemd160(enc.encode("abcdefghijklmnopqrstuvwxyz"))), "f71c27109c692c1b56bbdceb5b9d2865b3708dbc", "the lowercase alphabet", ); // Longer than one 64-byte block, which exercises the padding and the length // field — the parts a single-block test never reaches. is( hex(I.ripemd160(enc.encode("1234567890".repeat(8)))), "9b752e45573d4b39f4dbd3323cab82bf63326bfb", "eighty bytes, spanning two blocks", ); // --- secp256k1 --------------------------------------------------------------- console.log("\n secp256k1"); const privOne = Uint8Array.from([...new Array(31).fill(0), 1]); is( hex(await I.publicKey(privOne)), "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "the public key for private key 1", ); is( await W.addressFromPublicKey(await I.publicKey(privOne), "bitcoincash"), "bitcoincash:qp63uahgrxged4z5jswyt5dn5v3lzsem6cy4spdc2h", "the mainnet address for private key 1", ); // --- derivation, against libauth --------------------------------------------- // This phrase, and the address below it, came out of BuiltInWallet in // Argus/src/lib/wallet-web.js — the wallet the Sirius portal runs. console.log("\n BIP-39 and BIP-32"); const phrase = "trash key flip dawn impulse float medal rain sell hand neither hub"; const wallet = await W.fromPhrase(phrase, { prefix: "bchtest" }); is(wallet.address, "bchtest:qrq05hk8hurcsjx0slw4yjknmlujfzme3vxjhtwpwy", "derives the portal wallet's address"); const messy = await W.fromPhrase(" Trash KEY flip dawn impulse\tfloat medal rain sell hand neither hub \n", { prefix: "bchtest", }); is(messy.address, wallet.address, "spacing and capitals do not change the wallet"); // --- signing, against PHP ----------------------------------------------------- // The signature below was produced by SP_Message::sign() in PHP. Both sides run // RFC 6979, so the same key and message give the same bytes — an equality this // strict is only possible because neither implementation uses randomness. console.log("\n signatures match PHP"); const loginMessage = [ "SIRIUS-PRESS-LOGIN1", "https://example.bch", wallet.address, "abc123", "1758412800000", ].join("\n"); is( hex(await I.messageDigest(loginMessage)), "98d1b0e89586b933f1117fcd0dd167fe126ec5bb8e34f28685890f6030befe19", "the BIP-137 digest matches PHP and the Theseus wallet", ); is( await wallet.sign(loginMessage), "IGr9FSoDInLUr+iKZGw8w5LPyi/JsZAU6mFvGc80uQReP3IQZU7KMODzwS78FkRE57sh2osmWq1sVHZam81PTkY=", "the signature is byte-identical to the one PHP produces", ); is(await wallet.sign(loginMessage), await wallet.sign(loginMessage), "signing is deterministic"); // --- the upload envelope ------------------------------------------------------ // Manual-mode publishing signs a raw digest rather than a text message. The // expected value is SP_Message::site_digest() from PHP. console.log("\n BNS-SITE1 upload envelope"); const body = "

hi

"; const bodyHash = hex(new Uint8Array(await crypto.subtle.digest("SHA-256", enc.encode(body)))); const envelope = `BNS-SITE1\nexample.bch\nindex.html\n${bodyHash}\n1758412800000`; const siteDigest = new Uint8Array(await crypto.subtle.digest("SHA-256", enc.encode(envelope))); is( hex(siteDigest), "a71c4db0bd7268cd2b2103e2c0753ad37b046b203a4bf1ba7e84271b816d10de", "the upload digest matches PHP", ); const rawSig = await wallet.signRaw(siteDigest); is(atob(rawSig).length, 65, "signRaw produces 65 bytes"); // --- phrase generation -------------------------------------------------------- console.log("\n phrase handling"); const generated = await W.generatePhrase(12); is(generated.split(" ").length, 12, "a generated phrase has twelve words"); ok(W.validatePhrase(generated).ok, "a generated phrase validates"); const generated24 = await W.generatePhrase(24); is(generated24.split(" ").length, 24, "twenty-four words on request"); ok(W.validatePhrase(generated24).ok, "the long phrase validates too"); ok((await W.fromPhrase(generated, { prefix: "bchtest" })).address.startsWith("bchtest:"), "and opens a wallet"); ok(!W.validatePhrase("").ok, "an empty phrase is refused"); ok(!W.validatePhrase("one two three").ok, "a three-word phrase is refused"); is( W.validatePhrase("abandon abandon recieve abandon abandon abandon abandon abandon abandon abandon abandon about") .error, "Word 3, “recieve”, is not a recovery-phrase word.", "a misspelled word is named and numbered", ); console.log(""); if (failed > 0) { console.log(` ${passed + failed} checks, ${failed} FAILED\n`); process.exit(1); } console.log(` ${passed + failed} checks, all passed\n`);