Last active
December 21, 2025 03:01
-
-
Save bmatusiak/c25e6e0da752a1062740387764cea383 to your computer and use it in GitHub Desktop.
gen-onion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const { generateKeyPairSync, createHash, getHashes } = require('crypto'); | |
| function rawPublicKeyFromSpki(spkiDer) { | |
| // For ed25519 SPKI, the raw public key is the last 32 bytes | |
| return Buffer.from(spkiDer.slice(-32)); | |
| } | |
| function sha3_256(buf) { | |
| const hashes = getHashes(); | |
| if (!hashes.includes('sha3-256')) { | |
| throw new Error('Node crypto does not support sha3-256. Use Node >=16 with OpenSSL supporting sha3 or install a JS sha3 library.'); | |
| } | |
| return createHash('sha3-256').update(buf).digest(); | |
| } | |
| function base32Encode(buf) { | |
| const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; | |
| let bits = 0; | |
| let value = 0; | |
| let output = ''; | |
| for (let i = 0; i < buf.length; i++) { | |
| value = (value << 8) | buf[i]; | |
| bits += 8; | |
| while (bits >= 5) { | |
| const index = (value >>> (bits - 5)) & 31; | |
| bits -= 5; | |
| output += alphabet[index]; | |
| } | |
| } | |
| if (bits > 0) { | |
| const index = (value << (5 - bits)) & 31; | |
| output += alphabet[index]; | |
| } | |
| return output; // no padding, lowercase | |
| } | |
| function base32Decode(s) { | |
| const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; | |
| const lookup = Object.create(null); | |
| for (let i = 0; i < alphabet.length; i++) lookup[alphabet[i]] = i; | |
| s = s.toLowerCase().replace(/=+$/g, ''); | |
| const bytes = []; | |
| let bits = 0; | |
| let value = 0; | |
| for (let i = 0; i < s.length; i++) { | |
| const idx = lookup[s[i]]; | |
| if (idx === undefined) throw new Error('Invalid base32 character: ' + s[i]); | |
| value = (value << 5) | idx; | |
| bits += 5; | |
| if (bits >= 8) { | |
| bits -= 8; | |
| bytes.push((value >>> bits) & 0xff); | |
| } | |
| } | |
| return Buffer.from(bytes); | |
| } | |
| function verifyV3Onion(onion) { | |
| if (onion.endsWith('.onion')) onion = onion.slice(0, -6); | |
| const decoded = base32Decode(onion); | |
| if (decoded.length !== 35) return { ok: false, reason: 'invalid decoded length' }; | |
| const pub = decoded.slice(0, 32); | |
| const checksum = decoded.slice(32, 34); | |
| const version = decoded[34]; | |
| if (version !== 0x03) return { ok: false, reason: 'invalid version' }; | |
| const expected = sha3_256(Buffer.concat([Buffer.from('.onion checksum'), pub, Buffer.from([version])])).slice(0, 2); | |
| if (!expected.equals(checksum)) return { ok: false, reason: 'checksum mismatch' }; | |
| return { ok: true, pubKey: pub.toString('hex') }; | |
| } | |
| function generateV3Onion() { | |
| const { publicKey, privateKey } = generateKeyPairSync('ed25519'); | |
| const pubDer = publicKey.export({ type: 'spki', format: 'der' }); | |
| const privDer = privateKey.export({ type: 'pkcs8', format: 'der' }); | |
| const pubKeyRaw = rawPublicKeyFromSpki(pubDer); // 32 bytes | |
| const version = Buffer.from([0x03]); | |
| const checksumInput = Buffer.concat([Buffer.from('.onion checksum'), pubKeyRaw, version]); | |
| const checksumFull = sha3_256(checksumInput); | |
| const checksum = checksumFull.slice(0, 2); | |
| const addrBytes = Buffer.concat([pubKeyRaw, checksum, version]); // 35 bytes | |
| const addrBase32 = base32Encode(addrBytes); | |
| const onion = `${addrBase32}.onion`; | |
| return { | |
| onion, | |
| address: addrBase32, | |
| publicKey: pubKeyRaw.toString('hex'), | |
| privateKey: privDer.toString('hex'), | |
| }; | |
| } | |
| function generateV3OnionVanity(keyword, maxAttempts = 1000000) { | |
| if (!keyword) return generateV3Onion(); | |
| keyword = String(keyword).toLowerCase(); | |
| for (let i = 0; i < maxAttempts; i++) { | |
| const { onion, publicKey, privateKey } = generateV3Onion(); | |
| // require prefix match: keyword at start of base32 address | |
| const addr = onion.toLowerCase().endsWith('.onion') ? onion.slice(0, -6) : onion.toLowerCase(); | |
| if (addr.startsWith(keyword)) return { onion, publicKey, privateKey, attempts: i + 1 }; | |
| } | |
| throw new Error('vanity not found within maxAttempts'); | |
| } | |
| if (require.main === module) { | |
| try { | |
| const res = generateV3Onion(); | |
| console.log(res.onion); | |
| console.log('publicKey hex:', res.publicKey); | |
| console.log('privateKey (pkcs8 der) hex:', res.privateKey); | |
| const v = verifyV3Onion(res.address); | |
| if (v.ok) { | |
| console.log('verification: ok'); | |
| } else { | |
| console.error('verification: failed -', v.reason); | |
| process.exit(2); | |
| } | |
| } catch (e) { | |
| console.error('Error:', e && e.message ? e.message : String(e)); | |
| process.exit(1); | |
| } | |
| } | |
| module.exports = { generateV3Onion, generateV3OnionVanity }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "gen-onion", | |
| "version": "1.0.0", | |
| "description": "", | |
| "license": "ISC", | |
| "author": "", | |
| "type": "commonjs", | |
| "main": "index.js", | |
| "scripts": { | |
| "test": "echo \"Error: no test specified\" && exit 1" | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| const gen = require('./index'); | |
| function extractSeedFromPkcs8(pkcs8Buf) { | |
| for (let i = 0; i <= pkcs8Buf.length - 34; i++) { | |
| if (pkcs8Buf[i] === 0x04 && pkcs8Buf[i + 1] === 0x20) { | |
| return pkcs8Buf.slice(i + 2, i + 34); | |
| } | |
| } | |
| throw new Error('seed not found in pkcs8'); | |
| } | |
| function parseArgs() { | |
| const args = process.argv.slice(2); | |
| const out = { vanity: null, max: 1000000000000, keep: false, count: 1 }; | |
| for (const a of args) { | |
| if (a === '--no-vanity' || a === '--novanity') { out.vanity = false; continue; } | |
| if (a.startsWith('--vanity=')) { out.vanity = a.split('=')[1]; continue; } | |
| if (a.startsWith('--max=')) { out.max = Number(a.split('=')[1]) || out.max; continue; } | |
| if (a === '--keep' || a === '--continuous') { out.keep = true; out.count = 0; continue; } | |
| if (a.startsWith('--count=')) { out.count = Number(a.split('=')[1]) || out.count; continue; } | |
| // bare arg treated as keyword | |
| if (!out.vanity) out.vanity = a; | |
| } | |
| return out; | |
| } | |
| async function main() { | |
| const opts = parseArgs(); | |
| try { | |
| if (opts.vanity === false) { | |
| const res = gen.generateV3Onion(); | |
| const pubHex = res.publicKey; | |
| const pkcs8 = Buffer.from(res.privateKey, 'hex'); | |
| const seed = extractSeedFromPkcs8(pkcs8); | |
| console.log("--") | |
| console.log('onion:', res.onion); | |
| console.log('publicKey hex:', pubHex); | |
| console.log('seed hex:', seed.toString('hex')); | |
| return; | |
| } | |
| const wantMultiple = opts.keep || (opts.count && opts.count > 1); | |
| let found = 0; | |
| do { | |
| let res; | |
| if (opts.vanity) { | |
| if (typeof gen.generateV3OnionVanity !== 'function') throw new Error('vanity generator not available'); | |
| res = gen.generateV3OnionVanity(opts.vanity, opts.max); | |
| } else { | |
| res = gen.generateV3Onion(); | |
| } | |
| const pubHex = res.publicKey; | |
| const pkcs8 = Buffer.from(res.privateKey, 'hex'); | |
| const seed = extractSeedFromPkcs8(pkcs8); | |
| console.log("--") | |
| console.log('onion:', res.onion); | |
| if (res.attempts) console.log('attempts:', res.attempts); | |
| console.log('publicKey hex:', pubHex); | |
| console.log('seed hex:', seed.toString('hex')); | |
| found++; | |
| if (!wantMultiple) break; | |
| } while (opts.keep || (opts.count && found < opts.count)); | |
| } catch (e) { | |
| console.error('error:', e && e.message ? e.message : String(e)); | |
| process.exit(1); | |
| } | |
| } | |
| if (require.main === module) main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment