Skip to content

Instantly share code, notes, and snippets.

@bmatusiak
Last active December 21, 2025 03:01
Show Gist options
  • Select an option

  • Save bmatusiak/c25e6e0da752a1062740387764cea383 to your computer and use it in GitHub Desktop.

Select an option

Save bmatusiak/c25e6e0da752a1062740387764cea383 to your computer and use it in GitHub Desktop.
gen-onion
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 };
{
"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"
}
}
#!/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