How to generate a tor service onion address from the public key in java?

I am trying to generate the onion address that is generated from a public key.

If the following line is added to the code in a previous post, just after privateKeyEncoded

String publicKeyEncoded = encoder.encodeToString(publicKey.getEncoded());

When I put the privateKeyEncoded into the /var/lib/tor/hidden_service/private_key, save the publicKeyEncoded and start the tor service, a new onion address is created. I am trying to get the same onion address as the tor service and from one created from the publicKeyEncoded. Using this code

import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Base64;

//base64 string from the public key created
//the onion address the tor service gives when the private key is used
String onionAddressTest = "qqkhrc4men3fiqyl";

byte[] publicKeyDecoded = Base64.decodeBase64(publicKeyTest);
MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
byte[] sha1hash = messageDigest.digest(publicKeyDecoded);
int numberOfCharacters = 10;
byte[] reducedHash = new byte[numberOfCharacters];
for(int i = 0; i < numberOfCharacters; i++) {
    reducedHash[i] = sha1hash[i];
Base32 base32encoder = new Base32();
String onionAddress = base32encoder.encodeAsString(reducedHash).toLowerCase();
System.out.println(onionAddress);  // but this gives "7j3iz4of464hje2e"

I've tried using spongycastle to replicate my conversion but get the same answer. Which makes me think there's something wrong with how I generate the public key or there's something wrong in my initial conversion from base64.

Given the public key (publicKeyTest) how can you get the onion address (onionAddressTest) using java?

1 answer

  • answered 2018-04-14 15:47 dave_thompson_085

    According to this and this you need to hash only the part starting at offset 22 of the X.509 SubjectPublicKeyInfo encoding used by Java which calls it 'X.509' and by OpenSSL which calls it 'PUBKEY'. I can't find any actual Tor doc on this, but I don't believe it can be accidental that this is exactly the beginning of the algorithm-dependent data in SPKI format for an RSA-1024 key:

    $ openssl asn1parse -i <49833260.b64
        0:d=0  hl=3 l= 159 cons: SEQUENCE
        3:d=1  hl=2 l=  13 cons:  SEQUENCE
        5:d=2  hl=2 l=   9 prim:   OBJECT            :rsaEncryption
       16:d=2  hl=2 l=   0 prim:   NULL
       18:d=1  hl=3 l= 141 prim:  BIT STRING
    # 18 +3 for DER tag+len +1 for unusedbitcount in BITSTRING = 22
    # and the content beginning at 22 is:
    $ openssl asn1parse -i -strparse 22 <49833260.b64
        0:d=0  hl=3 l= 137 cons: SEQUENCE
        3:d=1  hl=3 l= 129 prim:  INTEGER           :8C9C59094CC6766719EA2F00A207F11
      135:d=1  hl=2 l=   3 prim:  INTEGER           :010001
    # which is (exactly) the RSAPublicKey structure from PKCS1

    So to do this in Java you can just assume RSA-1024, or with BouncyCastle (and I assume, but haven't tested, spongycastle as well) you can actually parse the ASN.1 properly:

    byte[] pubkeyder = Base64.decode("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCMnFkJTMZ2ZxnqLwCiB/EWHjsHbnC+sKEIrGbyOTYiTl3LygsekAX6LhgcllscLUFqSKlMRB3jRB0GAPrIc73E/hTnmWBtF8NT8DhZzl06LZ1BtNjfON1pHm87STMAayiSaXPmSOwIqOA89aJPcA9m4v4IhtjYSFXmCAsE4RqoAwIDAQAB");
    MessageDigest sha1 = MessageDigest.getInstance("SHA1");
    // method 1
    byte[] x1 = sha1.digest (Arrays.copyOfRange(pubkeyder, 22, pubkeyder.length));
    System.out.println (new String(b32enc(Arrays.copyOf(x1,10))).toLowerCase());
    // method 2
    byte[] x2 = sha1.digest (SubjectPublicKeyInfo.getInstance(pubkeyder).getPublicKeyData().getOctets());
    System.out.println (new String(b32enc(Arrays.copyOf(x2,10))).toLowerCase());