Reversing Dynadot two-factor token authentication

So I was reading another scary post how someone`s domain was stolen and blah blah blah it is time to enable 2-factor auth at your domain registrar (and if it doesn't support it, find a better one). I’ve checked mine, a couple of those I use, and it seems Dynadot was a good fit to use two-factor with and move most important domains to be hosted there.

To enable 2-factor auth on dynadot.com, I clicked through my account settings and found the menu: “Dynadot Token Authentication for iPhone/Android”. Hmm.. why only iPhone/Android?

Only available for iOS/Android?

OK, I pulled iOS app from Appstore, installed it and entered secure code which Dynadot provided to me. The code is 8 digits long. Not very secure (i.e. absolutely not secure - it is enough for hacker to see one your code to run a successful brute-force attack to get the secret key), but let’s live with this for now. What bothered me was the limitation to have one-time password generator stay on my phone only.. not in my Linux shell where I spent most of the time. Is Dynadot only catering for fucking hipsters carrying iPhones??? Or Google lovers?? How about poor Nokia??! ;)

I tried to use this secret code with standard HOTP/TOTP code generation algorithms (rfc 4226 and RFC-6238).

No luck.

Well, I only use iOS but I figured out it might be easier to decompile the app taken from Android store – you know how easy it is to convert Dalvik byte-code back to Java source.. So I got the app and found inside seemingly standard HMAC-SHA1 generator:

package com.dynadot.android.generator.classes;
 
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
 
// Referenced classes of package com.dynadot.android.generator.classes:
// Encryption
 
public class HmacSha1Encryption extends Encryption
{
 
public HmacSha1Encryption()
{
}
 
public String encrypt(String s)
{
KEY = s;
return shortHash(encrypt((new StringBuilder(String.valueOf(System.currentTimeMillis() / 60000L))).toString(), KEY), 6);
}
 
public byte[] encrypt(String s, String s1)
{
byte abyte0[] = (byte[])null;
byte abyte1[];
Mac mac = Mac.getInstance("HmacSha1");
mac.init(new SecretKeySpec(KEY.getBytes(), "HmacSha1"));
abyte1 = mac.doFinal(s.getBytes());
abyte0 = abyte1;
....
}

As I was using two-factor auth already for Amazon and Google via CLI-based, Java-written Authenticator tool I tried to make it to work with Dynadot. The source of Authenticator was using standard RFC algo in a similar fashion:

final byte[] keyBytes = Base32String.decode(secret);
Mac mac = Mac.getInstance("HMACSHA1");
mac.init(new SecretKeySpec(keyBytes, ""));
PasscodeGenerator pcg = new PasscodeGenerator(mac);
return pcg.generateTimeoutCode();

Now, I never liked and never studied JAVA, except enough to be able to make a change here or there.. So whilst the code looked comparably to me, there must have been something different in the encoding of secret/counter data, 'coz the generator produced non-matching authentication codes to what was showing in Dynadot's iPhone app.

(Meanwhile I also wrote to Dynadot support to ask what algo they use. Guess what? Their answer was quite lame - I thought better of their tech team:

I'm sorry, but we cannot give out those details. What is
preventing you from using the apps?

Best Regards,
Dynadot Support Team

To which I replied:

Hi there,

> I'm sorry, but we cannot give out those details.

This is silly.. Simply because security through obscurity does
not make sense and if you are using secure algorithms, there
can be nothing gained from the algorithm code itself.

I had a better impression of your service.

> What is preventing you from using the apps?

Make a guess? I don't have Android or Iphone?
In fact I do own one, but want to use it on my PC too... but
how about customers who don't?

Anyway, I already reversed your app:

Who is "Lee Taiger"? Say "Hi" to him for using "I am Lee Taiger" as
AES key for storing SerialNo encrypted on the phone storage.

You are using standard Hmac-SHA1 one-time-password generation
algorithm with a serial number from the website as a KEY.

What prevents you from releasing this app open-source? There are
no security implications - you can ask any expert if you don't
believe me.

Can't be polite when I am annoyed, sorry :*) Anyway.. being persistent I couldn't stop at this stage :) I pulled a couple of Python examples for HMAC one time password generation. They are all very basic and simple but whilst I was able to reproduce functionality for Google/Amazon on any of those generators, the one for Dynadot wouldn't want to show same codes...

Damn... I had no other choice but to reverse-enginer & debug their iPhone app. It took me some time to jailbreak my phone and gather all required tools. (Hehe, no, I didn't jailbreak just for the sake of doing this, it was just time to finally jailbreak.. ;)

But finally I have a decrypted DynadotToken.app sitting in Hopper disassembler, I have shell to my iPhone with gdb attached to the process and I am ready to make this bitch behave! =)

Gdb view

Half an hour late.. the riddle is solved! Sorry, nothing exciting to report :)

The generators use the same algo, but the incompatibility is only due to implementation of key/message strings encoding. What's funny is that should I have downloaded PHP or Ruby implementation to try it on in the very first place (instead of using HOTP generators I had in Python & Java), I'd never wrote this post and spent time researching what the heck it's all about... The codes would have been matching instantly:

$message = round(time() / 60); // time since epoh / 60
$key = "00000000"; // your secret dynadot key
$hash = hash_hmac("sha1", $message, $key);
var_dump($hash);
// this only produces hash for illustration purposes, 
// code should be extracted then
?>

Or Ruby:

require 'openssl'
require 'base64'

secret = '00000000'
data = '23265696'

p OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha1'), secret, data)

All is very simple. This generates codes identical to Dynadot app. However, non-identical to Google OTP or JAVA/Python based OTP generators.

Below is how complete generator source should look in Python.

import hmac, base64, struct, hashlib, time

def get_hotp_token(secret, intervals_no):

h = hmac.new(secret, str(intervals_no), hashlib.sha1).digest()
o = ord(h[19]) & 15
h = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
return h

if name == 'main':

tm = int(time.time())

print "Time: %s" % tm
print "Interval (time counter): %s" % int(tm // 60)

secret = '00000000'
print secret

print get_hotp_token(secret, intervals_no=int(tm // 60))

And next, for your reference, how Google-compliant generator is written. The only difference is that Dynadot (and PHP/Ruby/what else) uses ascii strings for their secret code and time counter, but all these so called "RFC compliant" generators work with byte-strings instead and any number supplied as string gets converted to it's integer representation then to byte representation.

To make below code generate Dynadot codes, comment 'counter_encoded' line and uncomment the other one. Also note Dynadot uses 60-seconds time period!

THAT IS what makes them different! Huh :)

"""An implementation of the RFC-6238 Time-Based One Time Password algorithm."""

import time
import hmac
import base64
import struct
import hashlib

PERIOD = 30

def make_hotp(secret, counter):
"""Generate an RFC-4226 HMAC-Based One Time Password."""
key = base64.b32decode(secret)

# compute the HMAC digest of the counter with the secret key

# when the secret is binary, counter must also be a binary string of 8 bytes.
counter_encoded = struct.pack(">q", counter)

# alternatively, when secret is string, no need to encode counter and pass it as is (as string)
#counter_encoded = str(int(counter)) # convert counter from float to int and to str

hmac_result = hmac.HMAC(key, counter_encoded, hashlib.sha1).digest()
#print " ".join(hex(ord(n)) for n in hmac_result)

# do HOTP dynamic truncation (see RFC4226 5.3)
offset = ord(hmac_result[-1]) & 0x0f
truncated_hash = hmac_result[offset:offset + 4]
code_bits, = struct.unpack(">L", truncated_hash)
htop = (code_bits & 0x7fffffff) % 1000000

# pad it out as necessary
return "%06d" % htop

def make_totp(secret, skew=0, timestamp=None):
"""Generate an RFC-6238 Time-Based One Time Password."""
timestamp = timestamp or time.time()
counter = timestamp // PERIOD
return make_hotp(secret, counter - skew)

if name == "main":

PERIOD = 60
secret = base64.b32encode("00000000")

print make_totp(secret)

Hope you enjoyed this post and can now have Dynadot 2-factor generators in any language of your choice :) God bless open-source! =)