Maybe you just want to back them up for when something goes wrong, or maybe you want to set up a new two-factor authentication app on a platform that Authy doesn’t support (cough Windows Phone cough). Whatever your reasons, if you want to export your TOTP secret keys from Authy, their apps or support guys won’t be much help to you.

The trick, that I just used to install all my existing TOTP secrets in the Microsoft Authenticator app, is to change one of their app of which we have the source, namely their Chrome app, to show us what we want.

So I opened gaedmjdfmmahhbjefcbgaolhhanlaolb/2.2.0_0/js/app.js from my Chrome Extensions folder (~/Library/Application Support/Google/Chrome/Default/Extensions on a Mac, ~/.config/google-chrome/Default/Extensions on Linux) in my favorite text editor (TextMate), and used JavaScript > Reformat Selection to be able to see what was happening in there. I then found that the shared secrets I was after was stored in GoogleAuthApp.decryptedSeed. I was looking for the decrypted version, because I didn’t want to have to understand where the encrypted values were stored, and how I could decrypt them myself; their Chrome app could decrypt them already, so all I needed was to add something in there that would somehow output them.

So I added a getter for the shared secret, in GoogleAuthApp (now obfuscated as d), like this:

1
2
3
4
5
  }, d.prototype.getOtp = function() {
return this.isEncrypted() ? "------" : this.otpGenerator.getOtp(this.decryptedSeed)
+ }, d.prototype.getSharedSecret = function() {
+ return this.isEncrypted() ? "?" : this.decryptedSeed
}, d.prototype.isDecrypted = function() {

Then I modified TokensView.prototype.updateTokens to output this info, along with the human-readable name of the entry, in a link to a QR code that I can scan in any TOTP client (compatible with Google Authenticator), like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  }, TokensView.prototype.updateTokens = function(tokenRows) {
var e, r, o, i, s, a;
for (s = [], o = 0, i = t.length; o < i; o++) {
a = t[o], r = f(a), e = n.get().find(r.attr("data-token-id")), r.find(".tokenCode").html(e.formatToken()(e.getOtp())), s.push(r.find(".tokenCodeString").html(e.getOtp()));
+ try {
+ var $sharedSecretBlock = r.find(".sharedSecret");
+ if ($sharedSecretBlock.length == 0) {
+ r.append('<div class="sharedSecret" style="float:right;font-size:13px;padding-right:8px">');
+ $sharedSecretBlock = r.find(".sharedSecret");
+ }
+ var qrQata = "otpauth://totp/" + encodeURIComponent(e.getName(e)) + "?secret=" + encodeURIComponent(e.getSharedSecret(e));
+ $sharedSecretBlock.html('<a href="https://chart.googleapis.com/chart?chs=256x256&cht=qr&chl=' + encodeURIComponent(qrQata) + '" target="_blank">Show Secret QR');
+ } catch(err) {}
}
return s

I noticed TokensView.updateTokens is only called after the tokens expire, so at most 30 seconds after you decrypted your data by entering your password, but eh, good enough.

So, if you want to do that too, you can try to do the changes I detailed above, or just replace your gaedmjdfmmahhbjefcbgaolhhanlaolb/js/app.js with my version (based on version 2.2.0 of their Chrome app), and wait between 0 and 30 seconds to see the links appear:

Authy-Chrome-app-with-QR-codes

Click them one by one, scan them with your new client, and voilà! You’re ready to rock two-factor logins on your new device.

Of note: CloudFlare doesn’t show a QR code in my screenshot above because it uses “Authy two-factor authentication”, which is not compatible with Google Authenticator. There’s just no point in exporting those out of Authy, since they are not usable anywhere else…