Skip to content

Commit 12bc4c9

Browse files
authored
Merge pull request #13 from void-57/master
feat: integrate Tor v3 Onion address derivation from WIF
2 parents b9099c3 + 3d50f11 commit 12bc4c9

2 files changed

Lines changed: 139 additions & 3 deletions

File tree

index.html

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<script src="https://cdn.jsdelivr.net/npm/xrpl@2.7.0/build/xrpl-latest-min.js"></script>
2222
<script src="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl.min.js"></script>
2323
<script src="https://cdn.jsdelivr.net/npm/js-sha512@0.8.0/build/sha512.min.js"></script>
24+
<script src="https://cdn.jsdelivr.net/npm/js-sha3@0.8.0/build/sha3.min.js"></script>
2425
<script src="https://unpkg.com/tonweb/dist/tonweb.js"></script>
2526
<script src="https://cdn.jsdelivr.net/npm/tronweb@5.3.2/dist/TronWeb.js"></script>
2627
<script src="https://unpkg.com/@polkadot/util@12.6.2/bundle-polkadot-util.js"></script>
@@ -75,7 +76,8 @@
7576
const result = await floDapps.launchStartUp();
7677
console.log(result)
7778
if (!await canConnectToCloud()) {
78-
document.body.prepend(document.createElement('adblocker-warning'))
79+
// document.body.prepend(document.createElement('adblocker-warning'))
80+
notify("Cloud offline", "error")
7981
return
8082
}
8183
let activeChain = localStorage.getItem(`${floGlobals.application}#activeChain`);
@@ -102,6 +104,7 @@
102104
floGlobals.myAlgoID = null;
103105
floGlobals.myXlmID = null;
104106
floGlobals.mySolID = null;
107+
floGlobals.myTorID = null;
105108
} else {
106109
floGlobals.myFloID = floCrypto.getFloID(floDapps.user.public);
107110
floGlobals.myBtcID = btcOperator.convert.legacy2bech(floGlobals.myFloID)
@@ -132,6 +135,7 @@
132135
floGlobals.myAlgoID = null;
133136
floGlobals.myXlmID = null;
134137
floGlobals.mySolID = null;
138+
floGlobals.myTorID = null;
135139
// Note: Cardano (ADA) address will be derived from private key later
136140
}
137141
document.querySelectorAll('.user-profile-id').forEach(el => el.textContent = floGlobals.myFloID)
@@ -202,6 +206,7 @@
202206
try { floGlobals.myXlmID = convertWIFtoStellarAddress(privKey); } catch (e) { console.warn('XLM derivation failed:', e); }
203207
try { floGlobals.mySolID = convertWIFtoSolanaAddress(privKey); } catch (e) { console.warn('SOL derivation failed:', e); }
204208
try { floGlobals.myAdaID = await convertWIFtoCardanoAddress(privKey); } catch (e) { console.warn('ADA derivation failed:', e); }
209+
try { const tor = convertWIFtoTOR(privKey); floGlobals.myTorID = tor ? tor.address : null; } catch (e) { console.warn('TOR derivation failed:', e); }
205210
}
206211
} catch (e) {
207212
console.warn('Failed to derive addresses with stored password:', e);
@@ -228,6 +233,7 @@
228233
try { floGlobals.myXlmID = convertWIFtoStellarAddress(privKey); } catch (e) { console.warn('XLM derivation failed:', e); }
229234
try { floGlobals.mySolID = convertWIFtoSolanaAddress(privKey); } catch (e) { console.warn('SOL derivation failed:', e); }
230235
try { floGlobals.myAdaID = await convertWIFtoCardanoAddress(privKey); } catch (e) { console.warn('ADA derivation failed:', e); }
236+
try { const tor = convertWIFtoTOR(privKey); floGlobals.myTorID = tor ? tor.address : null; } catch (e) { console.warn('TOR derivation failed:', e); }
231237
}
232238
} catch (e) {
233239
// Private key is password-secured but we don't have the password
@@ -247,7 +253,8 @@
247253
}).catch(error => {
248254
let isBrave = navigator.brave !== undefined
249255
if (error === 'Cloud offline') {
250-
document.body.prepend(document.createElement('adblocker-warning'))
256+
// document.body.prepend(document.createElement('adblocker-warning'))
257+
notify(error, "error")
251258
} else if (error === "App database initiation failed") {
252259

253260
} else {
@@ -2139,7 +2146,8 @@ <h4 class="h4">Members</h4>
21392146
{ id: floGlobals.myAlgoID, label: 'ALGO' },
21402147
{ id: floGlobals.myXlmID, label: 'XLM' },
21412148
{ id: floGlobals.mySolID, label: 'SOL' },
2142-
{ id: floGlobals.myAdaID, label: 'ADA' }
2149+
{ id: floGlobals.myAdaID, label: 'ADA' },
2150+
{ id: floGlobals.myTorID, label: 'TOR' }
21432151
];
21442152

21452153
const idList = allIds.filter(item => {
@@ -2748,6 +2756,12 @@ <h4 class="h4">Members</h4>
27482756
} catch (e) {
27492757
console.warn('ADA derivation failed:', e);
27502758
}
2759+
try {
2760+
const tor = convertWIFtoTOR(privKey);
2761+
floGlobals.myTorID = tor ? tor.address : null;
2762+
} catch (e) {
2763+
console.warn('TOR derivation failed:', e);
2764+
}
27512765
// Re-render profile to show new addresses
27522766
routeTo(window.location.hash);
27532767
notify('Addresses unlocked successfully!', 'success');
@@ -3477,6 +3491,10 @@ <h4>
34773491
<b>My ADA (Cardano) address</b>
34783492
<sm-copy class="user-ada-id" value=${floGlobals.myAdaID || 'Not available'}></sm-copy>
34793493
</div>
3494+
<div class="grid gap-0-5">
3495+
<b>My TOR (.onion) address</b>
3496+
<sm-copy class="user-tor-id" value=${floGlobals.myTorID || 'Not available'}></sm-copy>
3497+
</div>
34803498
${(!floGlobals.myXrpID && floGlobals.isPrivKeySecured) ? html`
34813499
<button class="button button--primary justify-self-start" onclick=${derivePrivKeyAddresses}>
34823500
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>

scripts/blockchainAddresses.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ async function deriveAllBlockchainAddresses(wif) {
439439
xlm: null,
440440
sol: null,
441441
ada: null,
442+
tor: null,
442443
};
443444

444445
try {
@@ -512,10 +513,127 @@ async function deriveAllBlockchainAddresses(wif) {
512513
} catch (e) {
513514
console.warn("ADA derivation failed:", e);
514515
}
516+
try {
517+
const torKeys = convertWIFtoTOR(wif);
518+
if (torKeys) {
519+
addresses.tor = torKeys.address;
520+
}
521+
} catch (e) {
522+
console.warn("TOR derivation failed:", e);
523+
}
515524

516525
return addresses;
517526
}
518527

528+
/**
529+
* Convert WIF private key to TOR v3 Onion address
530+
* Uses Ed25519 keypair and js-sha3 for checksum
531+
* @param {string} wif - WIF format private key
532+
* @returns {Object|null} Object containing onion address, public key, and Tor files or null on error
533+
*/
534+
function convertWIFtoTOR(wif) {
535+
try {
536+
const hexToBytes = (hex) => new Uint8Array(Crypto.util.hexToBytes(hex));
537+
538+
// 1. WIF -> 32-byte seed
539+
const privKeyHex = bitjs.wif2privkey(wif).privkey;
540+
const seed = hexToBytes(privKeyHex.substring(0, 64));
541+
542+
// 2. Expand the seed using SHA-512 (Tor protocol style)
543+
const hash = nacl.hash(seed); // Returns 64-byte Uint8Array
544+
545+
const secretScalar = hash.slice(0, 32);
546+
const rightHalfRH = hash.slice(32, 64);
547+
548+
// 3. "Clamp" the secret scalar (Ed25519 standard requirement)
549+
secretScalar[0] &= 248;
550+
secretScalar[31] &= 127;
551+
secretScalar[31] |= 64;
552+
553+
// 4. Derive Public Key from the Clamped Secret Scalar
554+
const kp = nacl.sign.keyPair.fromSeed(seed);
555+
const publicKey = kp.publicKey;
556+
557+
// 5. Corrected Tor headers
558+
const secretHeader = new Uint8Array([
559+
0x3d,0x3d,0x20,0x65, 0x64,0x32,0x35,0x35,
560+
0x31,0x39,0x76,0x31, 0x2d,0x73,0x65,0x63,
561+
0x72,0x65,0x74,0x3a, 0x20,0x74,0x79,0x70,
562+
0x65,0x30,0x20,0x3d, 0x3d,0x0a,0x00,0x00
563+
]);
564+
const publicHeader = new Uint8Array([
565+
0x3d,0x3d,0x20,0x65, 0x64,0x32,0x35,0x35,
566+
0x31,0x39,0x76,0x31, 0x2d,0x70,0x75,0x62,
567+
0x6c,0x69,0x63,0x3a, 0x20,0x74,0x79,0x70,
568+
0x65,0x30,0x20,0x3d, 0x3d,0x0a,0x00,0x00
569+
]);
570+
571+
// 6. Build genuine Tor secret file layout
572+
const secretFile = new Uint8Array(96);
573+
secretFile.set(secretHeader, 0);
574+
secretFile.set(secretScalar, 32);
575+
secretFile.set(rightHalfRH, 64);
576+
577+
// 7. Build Tor public file layout
578+
const publicFile = new Uint8Array(64);
579+
publicFile.set(publicHeader, 0);
580+
publicFile.set(publicKey, 32);
581+
582+
// 8. Generate Onion address
583+
const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
584+
585+
const prefix = new TextEncoder().encode(".onion checksum");
586+
const version = new Uint8Array([0x03]);
587+
588+
const checksumInput = new Uint8Array(prefix.length + publicKey.length + version.length);
589+
checksumInput.set(prefix, 0);
590+
checksumInput.set(publicKey, prefix.length);
591+
checksumInput.set(version, prefix.length + publicKey.length);
592+
593+
// SHA3-256 (requires js-sha3 library)
594+
const hashHex = sha3_256(checksumInput);
595+
596+
const checksum = new Uint8Array([
597+
parseInt(hashHex.substring(0, 2), 16),
598+
parseInt(hashHex.substring(2, 4), 16)
599+
]);
600+
601+
const onionBytes = new Uint8Array(publicKey.length + checksum.length + version.length);
602+
onionBytes.set(publicKey, 0);
603+
onionBytes.set(checksum, publicKey.length);
604+
onionBytes.set(version, publicKey.length + checksum.length);
605+
606+
let bits = 0;
607+
let value = 0;
608+
let output = "";
609+
610+
for (const b of onionBytes) {
611+
value = (value << 8) | b;
612+
bits += 8;
613+
while (bits >= 5) {
614+
output += alphabet[(value >>> (bits - 5)) & 31];
615+
bits -= 5;
616+
}
617+
}
618+
619+
if (bits > 0) {
620+
output += alphabet[(value << (5 - bits)) & 31];
621+
}
622+
623+
const onionAddress = output.toLowerCase() + ".onion";
624+
625+
return {
626+
address: onionAddress,
627+
publicKey,
628+
secretFile,
629+
publicFile
630+
};
631+
} catch (e) {
632+
console.error("WIF to TOR conversion error:", e);
633+
return null;
634+
}
635+
}
636+
519637
/**
520638
* Convert WIF private key to DOGE (Dogecoin) address
521639
* Uses secp256k1 with version byte 0x1e (30)

0 commit comments

Comments
 (0)