First up, a note: exporting your Authy tokens presents a security risk, especially if you save them to a file. Anyone that is able to view the tokens can then create the codes needed to access the associated accounts. So… use your head and be safe. With that out of the way:
I’ve recently become very frustrated at the major second-factor authentication (2FA) apps, particularly around the lack of features needed to manage a large number of tokens.
In the security industry, the guideline has long been “use a second factor” mechanism — giving rise to mantras like “something you have and something you know”, which is really the point. However, currently, nearly nobody uses 2FA strictly in that manner: most folks view 2FA as “a second password” mechanism. One they choose, one is based on whatever (generally a seed that’s based on time — the first ‘T’ in TOTP). While that defeats some of the intent behind 2FA, in practice that’s actually probably just fine.
So: as an industry we push vendors and developers to require 2FA. Good news! Many of them are starting to. Herein comes the problem: users now have to manage a lot of tokens. In fact, they have to manage at least one for each app they use.
So what’s the big deal? NONE of the major apps used for 2FA (Authy, 1Password, Duo, Google Authenticator, LastPass, etc.) really provide a useful way to manage a large number of accounts:
- Users can’t rename them based on what they want to call them (most of the apps use a combination of the Seed Provider name + username to refer to them. That may be OK with a small number of accounts, but wait until you have 15 “Google” accounts and need to figure out which one is which but you can only see the first 2 or 3 chars of what comes after “Google” on the screen…)
- Tagging doesn’t exist in this world
- Neither does sorting, for most of them
- In some of them, you can’t delete accounts without having to wait at least 24 hours.
- There’s no such thing as export
Now, some of those have good security reasons behind them (well… OK. Just one, really. The one that most applies to this post: lack of export). Why can’t you export from these apps? Simple: allowing export breaks the 2FA model of “something you have, something you know”. If you can get a dump of seeds, that becomes two things you know, since the device is no longer required. Except… for day to day use, nearly everyone is totally fine with 2FA just being 2 pieces of information that you know. Not to mention: what happens if a software company goes away? Do I lose access to my accounts because I can’t access the tokens anymore? Pretty much zero app developers allow someone that has already configured 2FA to display a scannable QR code (or even just get the seed) once the initial configuration is complete (which, again, is in harmony with good “something you have” mode, but contrary to “good user interface” requirements).
So… if you are one of those that wants to dump your tokens out of an existing app you are using — and if that app happens to be Authy — you are in luck! It turns out you can do this. It just isn’t super straight foward.
- Open the Authy application, such that you are viewing the accounts you have saved
- Open your browser extensions manager. In Chrome, this can be done by going to chrome://extensions
- In the extensions manager, click “Developer Mode” on the top right
- Find the Authy application (note: this is different than the Authy extension. It should be at the bottom of the page in the “Chrome Apps” section)
- You should see a bit that says “Inspect Views”. It may say “background page, 1 more”. Click the link, until you see “main.html”.
- Click “main.html”, and the Chrome developer tools window should open
- Open the Console (this is the Javascript Console)
- Paste the codeblock below into the console window
- Enjoy your list of accounts. (I recommend doing something like right clicking on the console and “save as” to export the data to a file — but please understand you do this at your own risk, as anyone that is able to access the file can then create a 2FA token for the accounts in the list).
Here’s the code to paste in step 8 (hat tip to gboudreau and nmurthy):
/* base32 / / Copyright (c) 2011, Chris Umbel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ var charTable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' function quintetCount (buff) { var quintets = Math.floor(buff.length / 5) return buff.length % 5 === 0 ? quintets : quintets + 1 } const encode = function (plain) { var i = 0 var j = 0 var shiftIndex = 0 var digit = 0 var encoded = new Array(quintetCount(plain) * 8) /* byte by byte isn't as pretty as quintet by quintet but tests a bit faster. will have to revisit. */ while (i < plain.length) { var current = plain[i]; if (shiftIndex > 3) { digit = current & (0xff >> shiftIndex) shiftIndex = (shiftIndex + 5) % 8 digit = (digit << shiftIndex) | ((i + 1 < plain.length) ? plain[i + 1] : 0) >> (8 - shiftIndex) i++ } else { digit = (current >> (8 - (shiftIndex + 5))) & 0x1f shiftIndex = (shiftIndex + 5) % 8 if (shiftIndex === 0) i++ } encoded[j] = charTable.charCodeAt(digit); j++ } for (i = j; i < encoded.length; i++) { encoded[i] = 0x3d // '='.charCodeAt(0) } return encoded.join('') } /* base32 end */ var hexToInt = function (str) { var result = [] for (var i = 0; i < str.length; i += 2) { result.push(parseInt(str.substr(i, 2), 16)) } return result } function hexToB32 (str) { return encode(hexToInt(str)) } const getTotps = function () { var totps = [] console.warn("Here's your Authy tokens:") appManager.getModel().forEach(function (i) { var secret = (i.markedForDeletion === false || !i.secretSeed) ? i.decryptedSeed : hexToB32(i.secretSeed) console.group(i.name) console.log('TOTP Secret: ' + secret) totps.push({ name: i.name, secret: secret }) console.groupEnd() }) console.log(JSON.stringify(totps)) return totps } getTotps()