Skip to content

Instantly share code, notes, and snippets.

@gboudreau
Forked from Ingramz/AuthyToOtherAuthenticator.md
Last active December 28, 2024 13:35
Show Gist options
  • Save gboudreau/94bb0c11a6209c82418d01a59d958c93 to your computer and use it in GitHub Desktop.
Save gboudreau/94bb0c11a6209c82418d01a59d958c93 to your computer and use it in GitHub Desktop.
Export TOTP tokens from Authy

Exporting your 2FA tokens from Authy to transfer them into another 2FA application

IMPORTANT - Update regarding deprecation of Authy desktop apps

Past August 2024, Authy stopped supported the desktop version of their apps:
See Authy is shutting down its desktop app | The 2FA app Authy will only be available on Android and iOS starting in August for details.

And indeed, after a while, Authy changed something in their backend which now prevents the old desktop app from logging in. If you are already logged in, then you are in luck, and you can follow the instructions below to export your tokens.

If you are not logged in anymore, but can find a backup of the necessary files, then restore those files, and re-install Authy 2.2.3 following the instructions below, and it should work as expected.

If you can't go back to a "logged in" state in the desktop app (because you either never used the desktop app, or you did but don't have a backup of the necessary files), then your only options now to export your tokens are 1) to use an Android phone, root it, and use that to access the Authy data, or 2) use an iOS device and mitmproxy to capture communications between the app and Authy's server, and decrypt that. Look in the comments below to find instructions on how to do that:

  • For iOS and the mitm approach, see here

The options below are for rooted Android phones:


The instructions below explain how to use the Authy desktop app to export your 2FA tokens. If that doesn't work, look for links in the section above, to find other options you can try.

--

This gist, based in part on a gist by Brian Hartvigsen, allows you to export from Authy your TOTP tokens you have stored there.
Those can be "standard" 6-digits / 30 secs tokens, or Authy's own version, the 7-digits / 10 secs tokens.

Since the Authy "desktop" app is a Chromium-based web-app, we'll use the Developer Tools provided by Chromium to execute Javascript code that will export the tokens in JSON or as QR codes. You can then import or manually add those in you preferred application.

Important: If you have any accounts that use the Authy TOTP SDK (eg. Gemini, Twitch, Sendgrid, Twilio, ...), you can NOT delete your Authy account, even after migrating your TOTP tokens to another software! If you do, you could be locking yourself out of all the accounts that require Authy specifically! Your only option here would be to go in those accounts, disable Authy 2FA, and enable another 2FA method. More details here.

Detailed How-To

  1. Install Authy desktop app, version 2.2.3 (the more recent versions won't work).

    Note: If you are prompted to update, do NOT do it; the latest version doesn't support --remote-debugging-port needed in point (2) below.

    (Click your OS below to get personalized instructions.)

    macOS

    Download and install this file: https://pkg.authy.com/authy/stable/2.2.3/darwin/x64/Authy%20Desktop-2.2.3.dmg
    MD5 hash: ab7e4ae5b88cb71f84394df6989950aa

    You can use the following command in Terminal, before launching Authy Desktop, to disable auto-updates:

    mkdir -p ~/Library/Caches/com.authy.authy-mac.ShipIt ; rm -rf ~/Library/Caches/com.authy.authy-mac.ShipIt/* ; chmod 500 ~/Library/Caches/com.authy.authy-mac.ShipIt
    Windows

    You can use the winget (CLI) tool:

    winget install --no-upgrade --force -e --id Twilio.Authy -v 2.2.3
    

    Or download and install one of those:
    64-bit: https://pkg.authy.com/authy/stable/2.2.3/win32/x64/Authy%20Desktop%20Setup%202.2.3.exe
    MD5 hash: efd176d89b280809b9f84fda9ba50840
    32-bit: https://pkg.authy.com/authy/stable/2.2.3/win32/x32/Authy%20Desktop%20Setup%202.2.3.exe
    MD5 hash: d66d63abb482523ad27dfe676e249fff

    Authy will start after installation. Close it ASAP.

    To prevent auto-update, go to the %LOCALAPPDATA%\authy folder, and delete Update.exe. Delete the app-2.5.0 folder, if it exists. (The version number will probably be a higher number.) In the app-2.2.3 subfolder, delete Update.exe.
    Of note: If you later want to uninstall Authy, you'll need to restore those files, as Update.exe is the executable used by the uninstallation process.

    Or, after the app updated, you can change your shortcut to execute "%LOCALAPPDATA%\authy\app-2.2.3\Authy Desktop.exe" --remote-debugging-port=5858 and change the Start in to %LOCALAPPDATA%\authy\app-2.2.3
    Even after an update is installed, 2.2.3 is still installed.

    Linux (using snap) (recommended)
    cd /tmp
    # curl -Lo authy.snap https://api.snapcraft.io/api/v1/snaps/download/H8ZpNgIoPyvmkgxOWw5MSzsXK1wRZiHn_18.snap
    curl -Lo authy.snap https://filebrowser.patati.ca/api/public/dl/Tk1sjeEi/H8ZpNgIoPyvmkgxOWw5MSzsXK1wRZiHn_18.snap # Copy of above file that is now gone
    if ! echo a488d3f3c06672a78f53da144f4325d8 authy.snap | md5sum -c --status ; then
        echo "Error: invalid MD5 hash"
    else
        unsquashfs -q -f -d authy-2.2.3 authy.snap
        cd authy-2.2.3/
    fi
    Linux (using flatpak) (alternative method if snap above doesn't work) (NOT WORKING ANYMORE)

    It seems flathub is using the api.snapcraft.io repo behind the scene, so trying to install using the below commands will fail, now that the Authy app was removed from api.snapcraft.io. Try to install directly the snap (using the above method), instead of using flatpak.

    flatpak install flathub com.authy.Authy
    # Update to the 2.2.3 commit (found this commit using: flatpak remote-info --log flathub com.authy.Authy)
    sudo flatpak update --commit=83c0df0dd48bbb6ad851f5cc62d6e0836e56e499c7a79041241809f8296e65cc com.authy.Authy
    # Optionally, if you want to export a JSON file, give access to Authy to your Home folders:
    sudo flatpak override --filesystem=home com.authy.Authy
  2. Start Authy desktop app, but add the --remote-debugging-port=5858 parameter to the command-line:

    macOS

    From Terminal.app: open -a "Authy Desktop" --args --remote-debugging-port=5858

    Windows

    Right-click the Authy desktop shortcut, and in the Target field write --remote-debugging-port=5858 at the end. Then click OK. Double-click the Authy desktop shortcut.

    Linux

    From a terminal: ./authy --remote-debugging-port=5858 (if you used snap)
    or flatpak run com.authy.Authy --remote-debugging-port=5858 (if you used flatpak)

  3. In Authy, Log in so you can see the codes being generated for you.

  4. If you have some codes that show a padlock next to them, you will need to enter your Backup Password before continuing below, or those codes won't be exported correctly (decryptedSeed will be empty).

  5. Open the following URL in Google Chrome (or any Chromium-based browser): http://localhost:5858

  6. Click the Twilio Authy link in that webpage.

  7. In Chrome Developer Tools top navigation bar, go in the Sources tab (if you don't see it, click >> to expand the full list), then select the Snippets sub-tab (tabs on the second line; again, click >> to expand the full list), and finally choose + New snippet.

    Careful here: do NOT open the Chrome Developer Tools like you normally do. When you go to http://localhost:5858, and click the Twilio Authy link in that webpage, it will show you Developer Tools for the Authy app. This is where you need to work. Here's a video that shows you exactly where you need to be, when you paste code: https://youtu.be/nArCf8iEqlw

  8. If you'd like to ensure the code below doesn't send anything to a remote server, you can disconnect from the internet now.

  9. In the snippet editor window that appears on the right, paste one of the following code options:

    Simplest

    This is the simplest form there is, and it will simply show you an object for each code you have in Authy. You can use that if you're scared to run complicated code you don't understand (i.e. the other options below).

    appManager.getModel().forEach(i => console.log(i))
    Simple

    This is still quite simple, but makes it easier to copy-paste everything out of the console in one operation.

    appManager.getModel().forEach(i => {
       console.log("{");
       console.log("    createdDate: " + i.createdDate);
       console.log("    accountType: " + i.accountType);
       console.log("    name: " + i.name);
       console.log("    originalName: " + i.originalName);
       console.log("    decryptedSeed: " + i.decryptedSeed);
       console.log("}");
    })
    QR codes

    This version will output QR codes that you can scan using another app, from your mobile device.
    If you uncomment the last line, you will also get a .json file that contains your tokens (name, secret & URL).

    All your Authy tokens will be displayed in the Console at the bottom; either copy-paste the TOTP URI, or scan the QR codes.

    // QRious v4.0.2 | (C) 2017 Alasdair Mercer | GPL v3 License Based on jsqrencode | (C) 2010 [email protected] | GPL v3 License
    !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.QRious=e()}(this,function(){"use strict";function t(t,e){var n;return"function"==typeof Object.create?n=Object.create(t):(s.prototype=t,n=new s,s.prototype=null),e&&i(!0,n,e),n}function e(e,n,s,r){var o=this;return"string"!=typeof e&&(r=s,s=n,n=e,e=null),"function"!=typeof n&&(r=s,s=n,n=function(){return o.apply(this,arguments)}),i(!1,n,o,r),n.prototype=t(o.prototype,s),n.prototype.constructor=n,n.class_=e||o.class_,n.super_=o,n}function i(t,e,i){for(var n,s,a=0,h=(i=o.call(arguments,2)).length;a<h;a++){s=i[a];for(n in s)t&&!r.call(s,n)||(e[n]=s[n])}}function n(){}var s=function(){},r=Object.prototype.hasOwnProperty,o=Array.prototype.slice,a=e;n.class_="Nevis",n.super_=Object,n.extend=a;var h=n,f=h.extend(function(t,e,i){this.qrious=t,this.element=e,this.element.qrious=t,this.enabled=Boolean(i)},{draw:function(t){},getElement:function(){return this.enabled||(this.enabled=!0,this.render()),this.element},getModuleSize:function(t){var e=this.qrious,i=e.padding||0,n=Math.floor((e.size-2*i)/t.width);return Math.max(1,n)},getOffset:function(t){var e=this.qrious,i=e.padding;if(null!=i)return i;var n=this.getModuleSize(t),s=Math.floor((e.size-n*t.width)/2);return Math.max(0,s)},render:function(t){this.enabled&&(this.resize(),this.reset(),this.draw(t))},reset:function(){},resize:function(){}}),c=f.extend({draw:function(t){var e,i,n=this.qrious,s=this.getModuleSize(t),r=this.getOffset(t),o=this.element.getContext("2d");for(o.fillStyle=n.foreground,o.globalAlpha=n.foregroundAlpha,e=0;e<t.width;e++)for(i=0;i<t.width;i++)t.buffer[i*t.width+e]&&o.fillRect(s*e+r,s*i+r,s,s)},reset:function(){var t=this.qrious,e=this.element.getContext("2d"),i=t.size;e.lineWidth=1,e.clearRect(0,0,i,i),e.fillStyle=t.background,e.globalAlpha=t.backgroundAlpha,e.fillRect(0,0,i,i)},resize:function(){var t=this.element;t.width=t.height=this.qrious.size}}),u=h.extend(null,{BLOCK:[0,11,15,19,23,27,31,16,18,20,22,24,26,28,20,22,24,24,26,28,28,22,24,24,26,26,28,28,24,24,26,26,26,28,28,24,26,26,26,28,28]}),l=h.extend(null,{BLOCKS:[1,0,19,7,1,0,16,10,1,0,13,13,1,0,9,17,1,0,34,10,1,0,28,16,1,0,22,22,1,0,16,28,1,0,55,15,1,0,44,26,2,0,17,18,2,0,13,22,1,0,80,20,2,0,32,18,2,0,24,26,4,0,9,16,1,0,108,26,2,0,43,24,2,2,15,18,2,2,11,22,2,0,68,18,4,0,27,16,4,0,19,24,4,0,15,28,2,0,78,20,4,0,31,18,2,4,14,18,4,1,13,26,2,0,97,24,2,2,38,22,4,2,18,22,4,2,14,26,2,0,116,30,3,2,36,22,4,4,16,20,4,4,12,24,2,2,68,18,4,1,43,26,6,2,19,24,6,2,15,28,4,0,81,20,1,4,50,30,4,4,22,28,3,8,12,24,2,2,92,24,6,2,36,22,4,6,20,26,7,4,14,28,4,0,107,26,8,1,37,22,8,4,20,24,12,4,11,22,3,1,115,30,4,5,40,24,11,5,16,20,11,5,12,24,5,1,87,22,5,5,41,24,5,7,24,30,11,7,12,24,5,1,98,24,7,3,45,28,15,2,19,24,3,13,15,30,1,5,107,28,10,1,46,28,1,15,22,28,2,17,14,28,5,1,120,30,9,4,43,26,17,1,22,28,2,19,14,28,3,4,113,28,3,11,44,26,17,4,21,26,9,16,13,26,3,5,107,28,3,13,41,26,15,5,24,30,15,10,15,28,4,4,116,28,17,0,42,26,17,6,22,28,19,6,16,30,2,7,111,28,17,0,46,28,7,16,24,30,34,0,13,24,4,5,121,30,4,14,47,28,11,14,24,30,16,14,15,30,6,4,117,30,6,14,45,28,11,16,24,30,30,2,16,30,8,4,106,26,8,13,47,28,7,22,24,30,22,13,15,30,10,2,114,28,19,4,46,28,28,6,22,28,33,4,16,30,8,4,122,30,22,3,45,28,8,26,23,30,12,28,15,30,3,10,117,30,3,23,45,28,4,31,24,30,11,31,15,30,7,7,116,30,21,7,45,28,1,37,23,30,19,26,15,30,5,10,115,30,19,10,47,28,15,25,24,30,23,25,15,30,13,3,115,30,2,29,46,28,42,1,24,30,23,28,15,30,17,0,115,30,10,23,46,28,10,35,24,30,19,35,15,30,17,1,115,30,14,21,46,28,29,19,24,30,11,46,15,30,13,6,115,30,14,23,46,28,44,7,24,30,59,1,16,30,12,7,121,30,12,26,47,28,39,14,24,30,22,41,15,30,6,14,121,30,6,34,47,28,46,10,24,30,2,64,15,30,17,4,122,30,29,14,46,28,49,10,24,30,24,46,15,30,4,18,122,30,13,32,46,28,48,14,24,30,42,32,15,30,20,4,117,30,40,7,47,28,43,22,24,30,10,67,15,30,19,6,118,30,18,31,47,28,34,34,24,30,20,61,15,30],FINAL_FORMAT:[30660,29427,32170,30877,26159,25368,27713,26998,21522,20773,24188,23371,17913,16590,20375,19104,13663,12392,16177,14854,9396,8579,11994,11245,5769,5054,7399,6608,1890,597,3340,2107],LEVELS:{L:1,M:2,Q:3,H:4}}),_=h.extend(null,{EXPONENT:[1,2,4,8,16,32,64,128,29,58,116,232,205,135,19,38,76,152,45,90,180,117,234,201,143,3,6,12,24,48,96,192,157,39,78,156,37,74,148,53,106,212,181,119,238,193,159,35,70,140,5,10,20,40,80,160,93,186,105,210,185,111,222,161,95,190,97,194,153,47,94,188,101,202,137,15,30,60,120,240,253,231,211,187,107,214,177,127,254,225,223,163,91,182,113,226,217,175,67,134,17,34,68,136,13,26,52,104,208,189,103,206,129,31,62,124,248,237,199,147,59,118,236,197,151,51,102,204,133,23,46,92,184,109,218,169,79,158,33,66,132,21,42,84,168,77,154,41,82,164,85,170,73,146,57,114,228,213,183,115,230,209,191,99,198,145,63,126,252,229,215,179,123,246,241,255,227,219,171,75,150,49,98,196,149,55,110,220,165,87,174,65,130,25,50,100,200,141,7,14,28,56,112,224,221,167,83,166,81,162,89,178,121,242,249,239,195,155,43,86,172,69,138,9,18,36,72,144,61,122,244,245,247,243,251,235,203,139,11,22,44,88,176,125,250,233,207,131,27,54,108,216,173,71,142,0],LOG:[255,0,1,25,2,50,26,198,3,223,51,238,27,104,199,75,4,100,224,14,52,141,239,129,28,193,105,248,200,8,76,113,5,138,101,47,225,36,15,33,53,147,142,218,240,18,130,69,29,181,194,125,106,39,249,185,201,154,9,120,77,228,114,166,6,191,139,98,102,221,48,253,226,152,37,179,16,145,34,136,54,208,148,206,143,150,219,189,241,210,19,92,131,56,70,64,30,66,182,163,195,72,126,110,107,58,40,84,250,133,186,61,202,94,155,159,10,21,121,43,78,212,229,172,115,243,167,87,7,112,192,247,140,128,99,13,103,74,222,237,49,197,254,24,227,165,153,119,38,184,180,124,17,68,146,217,35,32,137,46,55,63,209,91,149,188,207,205,144,135,151,178,220,252,190,97,242,86,211,171,20,42,93,158,132,60,57,83,71,109,65,162,31,45,67,216,183,123,164,118,196,23,73,236,127,12,111,246,108,161,59,82,41,157,85,170,251,96,134,177,187,204,62,90,203,89,95,176,156,169,160,81,11,245,22,235,122,117,44,215,79,174,213,233,230,231,173,232,116,214,244,234,168,80,88,175]}),d=h.extend(null,{BLOCK:[3220,1468,2713,1235,3062,1890,2119,1549,2344,2936,1117,2583,1330,2470,1667,2249,2028,3780,481,4011,142,3098,831,3445,592,2517,1776,2234,1951,2827,1070,2660,1345,3177]}),v=h.extend(function(t){var e,i,n,s,r,o=t.value.length;for(this._badness=[],this._level=l.LEVELS[t.level],this._polynomial=[],this._value=t.value,this._version=0,this._stringBuffer=[];this._version<40&&(this._version++,n=4*(this._level-1)+16*(this._version-1),s=l.BLOCKS[n++],r=l.BLOCKS[n++],e=l.BLOCKS[n++],i=l.BLOCKS[n],n=e*(s+r)+r-3+(this._version<=9),!(o<=n)););this._dataBlock=e,this._eccBlock=i,this._neccBlock1=s,this._neccBlock2=r;var a=this.width=17+4*this._version;this.buffer=v._createArray(a*a),this._ecc=v._createArray(e+(e+i)*(s+r)+r),this._mask=v._createArray((a*(a+1)+1)/2),this._insertFinders(),this._insertAlignments(),this.buffer[8+a*(a-8)]=1,this._insertTimingGap(),this._reverseMask(),this._insertTimingRowAndColumn(),this._insertVersion(),this._syncMask(),this._convertBitStream(o),this._calculatePolynomial(),this._appendEccToData(),this._interleaveBlocks(),this._pack(),this._finish()},{_addAlignment:function(t,e){var i,n=this.buffer,s=this.width;for(n[t+s*e]=1,i=-2;i<2;i++)n[t+i+s*(e-2)]=1,n[t-2+s*(e+i+1)]=1,n[t+2+s*(e+i)]=1,n[t+i+1+s*(e+2)]=1;for(i=0;i<2;i++)this._setMask(t-1,e+i),this._setMask(t+1,e-i),this._setMask(t-i,e-1),this._setMask(t+i,e+1)},_appendData:function(t,e,i,n){var s,r,o,a=this._polynomial,h=this._stringBuffer;for(r=0;r<n;r++)h[i+r]=0;for(r=0;r<e;r++){if(255!==(s=_.LOG[h[t+r]^h[i]]))for(o=1;o<n;o++)h[i+o-1]=h[i+o]^_.EXPONENT[v._modN(s+a[n-o])];else for(o=i;o<i+n;o++)h[o]=h[o+1];h[i+n-1]=255===s?0:_.EXPONENT[v._modN(s+a[0])]}},_appendEccToData:function(){var t,e=0,i=this._dataBlock,n=this._calculateMaxLength(),s=this._eccBlock;for(t=0;t<this._neccBlock1;t++)this._appendData(e,i,n,s),e+=i,n+=s;for(t=0;t<this._neccBlock2;t++)this._appendData(e,i+1,n,s),e+=i+1,n+=s},_applyMask:function(t){var e,i,n,s,r=this.buffer,o=this.width;switch(t){case 0:for(s=0;s<o;s++)for(n=0;n<o;n++)n+s&1||this._isMasked(n,s)||(r[n+s*o]^=1);break;case 1:for(s=0;s<o;s++)for(n=0;n<o;n++)1&s||this._isMasked(n,s)||(r[n+s*o]^=1);break;case 2:for(s=0;s<o;s++)for(e=0,n=0;n<o;n++,e++)3===e&&(e=0),e||this._isMasked(n,s)||(r[n+s*o]^=1);break;case 3:for(i=0,s=0;s<o;s++,i++)for(3===i&&(i=0),e=i,n=0;n<o;n++,e++)3===e&&(e=0),e||this._isMasked(n,s)||(r[n+s*o]^=1);break;case 4:for(s=0;s<o;s++)for(e=0,i=s>>1&1,n=0;n<o;n++,e++)3===e&&(e=0,i=!i),i||this._isMasked(n,s)||(r[n+s*o]^=1);break;case 5:for(i=0,s=0;s<o;s++,i++)for(3===i&&(i=0),e=0,n=0;n<o;n++,e++)3===e&&(e=0),(n&s&1)+!(!e|!i)||this._isMasked(n,s)||(r[n+s*o]^=1);break;case 6:for(i=0,s=0;s<o;s++,i++)for(3===i&&(i=0),e=0,n=0;n<o;n++,e++)3===e&&(e=0),(n&s&1)+(e&&e===i)&1||this._isMasked(n,s)||(r[n+s*o]^=1);break;case 7:for(i=0,s=0;s<o;s++,i++)for(3===i&&(i=0),e=0,n=0;n<o;n++,e++)3===e&&(e=0),(e&&e===i)+(n+s&1)&1||this._isMasked(n,s)||(r[n+s*o]^=1)}},_calculateMaxLength:function(){return this._dataBlock*(this._neccBlock1+this._neccBlock2)+this._neccBlock2},_calculatePolynomial:function(){var t,e,i=this._eccBlock,n=this._polynomial;for(n[0]=1,t=0;t<i;t++){for(n[t+1]=1,e=t;e>0;e--)n[e]=n[e]?n[e-1]^_.EXPONENT[v._modN(_.LOG[n[e]]+t)]:n[e-1];n[0]=_.EXPONENT[v._modN(_.LOG[n[0]]+t)]}for(t=0;t<=i;t++)n[t]=_.LOG[n[t]]},_checkBadness:function(){var t,e,i,n,s,r=0,o=this._badness,a=this.buffer,h=this.width;for(s=0;s<h-1;s++)for(n=0;n<h-1;n++)(a[n+h*s]&&a[n+1+h*s]&&a[n+h*(s+1)]&&a[n+1+h*(s+1)]||!(a[n+h*s]||a[n+1+h*s]||a[n+h*(s+1)]||a[n+1+h*(s+1)]))&&(r+=v.N2);var f=0;for(s=0;s<h;s++){for(i=0,o[0]=0,t=0,n=0;n<h;n++)t===(e=a[n+h*s])?o[i]++:o[++i]=1,f+=(t=e)?1:-1;r+=this._getBadness(i)}f<0&&(f=-f);var c=0,u=f;for(u+=u<<2,u<<=1;u>h*h;)u-=h*h,c++;for(r+=c*v.N4,n=0;n<h;n++){for(i=0,o[0]=0,t=0,s=0;s<h;s++)t===(e=a[n+h*s])?o[i]++:o[++i]=1,t=e;r+=this._getBadness(i)}return r},_convertBitStream:function(t){var e,i,n=this._ecc,s=this._version;for(i=0;i<t;i++)n[i]=this._value.charCodeAt(i);var r=this._stringBuffer=n.slice(),o=this._calculateMaxLength();t>=o-2&&(t=o-2,s>9&&t--);var a=t;if(s>9){for(r[a+2]=0,r[a+3]=0;a--;)e=r[a],r[a+3]|=255&e<<4,r[a+2]=e>>4;r[2]|=255&t<<4,r[1]=t>>4,r[0]=64|t>>12}else{for(r[a+1]=0,r[a+2]=0;a--;)e=r[a],r[a+2]|=255&e<<4,r[a+1]=e>>4;r[1]|=255&t<<4,r[0]=64|t>>4}for(a=t+3-(s<10);a<o;)r[a++]=236,r[a++]=17},_getBadness:function(t){var e,i=0,n=this._badness;for(e=0;e<=t;e++)n[e]>=5&&(i+=v.N1+n[e]-5);for(e=3;e<t-1;e+=2)n[e-2]===n[e+2]&&n[e+2]===n[e-1]&&n[e-1]===n[e+1]&&3*n[e-1]===n[e]&&(0===n[e-3]||e+3>t||3*n[e-3]>=4*n[e]||3*n[e+3]>=4*n[e])&&(i+=v.N3);return i},_finish:function(){this._stringBuffer=this.buffer.slice();var t,e,i=0,n=3e4;for(e=0;e<8&&(this._applyMask(e),(t=this._checkBadness())<n&&(n=t,i=e),7!==i);e++)this.buffer=this._stringBuffer.slice();i!==e&&this._applyMask(i),n=l.FINAL_FORMAT[i+(this._level-1<<3)];var s=this.buffer,r=this.width;for(e=0;e<8;e++,n>>=1)1&n&&(s[r-1-e+8*r]=1,e<6?s[8+r*e]=1:s[8+r*(e+1)]=1);for(e=0;e<7;e++,n>>=1)1&n&&(s[8+r*(r-7+e)]=1,e?s[6-e+8*r]=1:s[7+8*r]=1)},_interleaveBlocks:function(){var t,e,i=this._dataBlock,n=this._ecc,s=this._eccBlock,r=0,o=this._calculateMaxLength(),a=this._neccBlock1,h=this._neccBlock2,f=this._stringBuffer;for(t=0;t<i;t++){for(e=0;e<a;e++)n[r++]=f[t+e*i];for(e=0;e<h;e++)n[r++]=f[a*i+t+e*(i+1)]}for(e=0;e<h;e++)n[r++]=f[a*i+t+e*(i+1)];for(t=0;t<s;t++)for(e=0;e<a+h;e++)n[r++]=f[o+t+e*s];this._stringBuffer=n},_insertAlignments:function(){var t,e,i,n=this._version,s=this.width;if(n>1)for(t=u.BLOCK[n],i=s-7;;){for(e=s-7;e>t-3&&(this._addAlignment(e,i),!(e<t));)e-=t;if(i<=t+9)break;i-=t,this._addAlignment(6,i),this._addAlignment(i,6)}},_insertFinders:function(){var t,e,i,n,s=this.buffer,r=this.width;for(t=0;t<3;t++){for(e=0,n=0,1===t&&(e=r-7),2===t&&(n=r-7),s[n+3+r*(e+3)]=1,i=0;i<6;i++)s[n+i+r*e]=1,s[n+r*(e+i+1)]=1,s[n+6+r*(e+i)]=1,s[n+i+1+r*(e+6)]=1;for(i=1;i<5;i++)this._setMask(n+i,e+1),this._setMask(n+1,e+i+1),this._setMask(n+5,e+i),this._setMask(n+i+1,e+5);for(i=2;i<4;i++)s[n+i+r*(e+2)]=1,s[n+2+r*(e+i+1)]=1,s[n+4+r*(e+i)]=1,s[n+i+1+r*(e+4)]=1}},_insertTimingGap:function(){var t,e,i=this.width;for(e=0;e<7;e++)this._setMask(7,e),this._setMask(i-8,e),this._setMask(7,e+i-7);for(t=0;t<8;t++)this._setMask(t,7),this._setMask(t+i-8,7),this._setMask(t,i-8)},_insertTimingRowAndColumn:function(){var t,e=this.buffer,i=this.width;for(t=0;t<i-14;t++)1&t?(this._setMask(8+t,6),this._setMask(6,8+t)):(e[8+t+6*i]=1,e[6+i*(8+t)]=1)},_insertVersion:function(){var t,e,i,n,s=this.buffer,r=this._version,o=this.width;if(r>6)for(t=d.BLOCK[r-7],e=17,i=0;i<6;i++)for(n=0;n<3;n++,e--)1&(e>11?r>>e-12:t>>e)?(s[5-i+o*(2-n+o-11)]=1,s[2-n+o-11+o*(5-i)]=1):(this._setMask(5-i,2-n+o-11),this._setMask(2-n+o-11,5-i))},_isMasked:function(t,e){var i=v._getMaskBit(t,e);return 1===this._mask[i]},_pack:function(){var t,e,i,n=1,s=1,r=this.width,o=r-1,a=r-1,h=(this._dataBlock+this._eccBlock)*(this._neccBlock1+this._neccBlock2)+this._neccBlock2;for(e=0;e<h;e++)for(t=this._stringBuffer[e],i=0;i<8;i++,t<<=1){128&t&&(this.buffer[o+r*a]=1);do{s?o--:(o++,n?0!==a?a--:(n=!n,6===(o-=2)&&(o--,a=9)):a!==r-1?a++:(n=!n,6===(o-=2)&&(o--,a-=8))),s=!s}while(this._isMasked(o,a))}},_reverseMask:function(){var t,e,i=this.width;for(t=0;t<9;t++)this._setMask(t,8);for(t=0;t<8;t++)this._setMask(t+i-8,8),this._setMask(8,t);for(e=0;e<7;e++)this._setMask(8,e+i-7)},_setMask:function(t,e){var i=v._getMaskBit(t,e);this._mask[i]=1},_syncMask:function(){var t,e,i=this.width;for(e=0;e<i;e++)for(t=0;t<=e;t++)this.buffer[t+i*e]&&this._setMask(t,e)}},{_createArray:function(t){var e,i=[];for(e=0;e<t;e++)i[e]=0;return i},_getMaskBit:function(t,e){var i;return t>e&&(i=t,t=e,e=i),i=e,i+=e*e,i>>=1,i+=t},_modN:function(t){for(;t>=255;)t=((t-=255)>>8)+(255&t);return t},N1:3,N2:3,N3:40,N4:10}),p=v,m=f.extend({draw:function(){this.element.src=this.qrious.toDataURL()},reset:function(){this.element.src=""},resize:function(){var t=this.element;t.width=t.height=this.qrious.size}}),g=h.extend(function(t,e,i,n){this.name=t,this.modifiable=Boolean(e),this.defaultValue=i,this._valueTransformer=n},{transform:function(t){var e=this._valueTransformer;return"function"==typeof e?e(t,this):t}}),k=h.extend(null,{abs:function(t){return null!=t?Math.abs(t):null},hasOwn:function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},noop:function(){},toUpperCase:function(t){return null!=t?t.toUpperCase():null}}),w=h.extend(function(t){this.options={},t.forEach(function(t){this.options[t.name]=t},this)},{exists:function(t){return null!=this.options[t]},get:function(t,e){return w._get(this.options[t],e)},getAll:function(t){var e,i=this.options,n={};for(e in i)k.hasOwn(i,e)&&(n[e]=w._get(i[e],t));return n},init:function(t,e,i){"function"!=typeof i&&(i=k.noop);var n,s;for(n in this.options)k.hasOwn(this.options,n)&&(s=this.options[n],w._set(s,s.defaultValue,e),w._createAccessor(s,e,i));this._setAll(t,e,!0)},set:function(t,e,i){return this._set(t,e,i)},setAll:function(t,e){return this._setAll(t,e)},_set:function(t,e,i,n){var s=this.options[t];if(!s)throw new Error("Invalid option: "+t);if(!s.modifiable&&!n)throw new Error("Option cannot be modified: "+t);return w._set(s,e,i)},_setAll:function(t,e,i){if(!t)return!1;var n,s=!1;for(n in t)k.hasOwn(t,n)&&this._set(n,t[n],e,i)&&(s=!0);return s}},{_createAccessor:function(t,e,i){var n={get:function(){return w._get(t,e)}};t.modifiable&&(n.set=function(n){w._set(t,n,e)&&i(n,t)}),Object.defineProperty(e,t.name,n)},_get:function(t,e){return e["_"+t.name]},_set:function(t,e,i){var n="_"+t.name,s=i[n],r=t.transform(null!=e?e:t.defaultValue);return i[n]=r,r!==s}}),M=w,b=h.extend(function(){this._services={}},{getService:function(t){var e=this._services[t];if(!e)throw new Error("Service is not being managed with name: "+t);return e},setService:function(t,e){if(this._services[t])throw new Error("Service is already managed with name: "+t);e&&(this._services[t]=e)}}),B=new M([new g("background",!0,"white"),new g("backgroundAlpha",!0,1,k.abs),new g("element"),new g("foreground",!0,"black"),new g("foregroundAlpha",!0,1,k.abs),new g("level",!0,"L",k.toUpperCase),new g("mime",!0,"image/png"),new g("padding",!0,null,k.abs),new g("size",!0,100,k.abs),new g("value",!0,"")]),y=new b,O=h.extend(function(t){B.init(t,this,this.update.bind(this));var e=B.get("element",this),i=y.getService("element"),n=e&&i.isCanvas(e)?e:i.createCanvas(),s=e&&i.isImage(e)?e:i.createImage();this._canvasRenderer=new c(this,n,!0),this._imageRenderer=new m(this,s,s===e),this.update()},{get:function(){return B.getAll(this)},set:function(t){B.setAll(t,this)&&this.update()},toDataURL:function(t){return this.canvas.toDataURL(t||this.mime)},update:function(){var t=new p({level:this.level,value:this.value});this._canvasRenderer.render(t),this._imageRenderer.render(t)}},{use:function(t){y.setService(t.getName(),t)}});Object.defineProperties(O.prototype,{canvas:{get:function(){return this._canvasRenderer.getElement()}},image:{get:function(){return this._imageRenderer.getElement()}}});var A=O,L=h.extend({getName:function(){}}).extend({createCanvas:function(){},createImage:function(){},getName:function(){return"element"},isCanvas:function(t){},isImage:function(t){}}).extend({createCanvas:function(){return document.createElement("canvas")},createImage:function(){return document.createElement("img")},isCanvas:function(t){return t instanceof HTMLCanvasElement},isImage:function(t){return t instanceof HTMLImageElement}});return A.use(new L),A});
    
    // Based on https://github.com/LinusU/base32-encode/blob/master/index.js
    function hex_to_b32(hex) { let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let bytes = []; for (let i = 0; i < hex.length; i += 2) { bytes.push(parseInt(hex.substr(i, 2), 16)); } let bits = 0; let value = 0; let output = ''; for (let i = 0; i < bytes.length; i++) { value = (value << 8) | bytes[i]; bits += 8; while (bits >= 5) { output += alphabet[(value >>> (bits - 5)) & 31]; bits -= 5; } } if (bits > 0) { output += alphabet[(value << (5 - bits)) & 31]; } return output; }
    
    // Based on https://github.com/adriancooney/console.image
    function console_image(url, size) { console.log("%c+", "font-size: 1px; padding: " + Math.floor(size / 2) + "px " + Math.floor(size / 2) + "px; line-height: " + size + "px; background: url(" + url + "); color: transparent;"); }
    
    (function(console) { console.save = function(data, filename) { if (!data) { console.error('Console.save: No data'); return; } if (typeof data === "object") { data = JSON.stringify(data, undefined, 4) } var blob = new Blob([data], {type: 'text/json'}), e = document.createEvent('MouseEvents'), a = document.createElement('a'); a.download = filename; a.href = window.URL.createObjectURL(blob); a.dataset.downloadurl =  ['text/json', a.download, a.href].join(':'); e.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); a.dispatchEvent(e); } })(console);
    
    console.clear();
    console.warn("Here's your Authy tokens:");
    var data = appManager.getModel().map(function(i) {
        var secretSeed = i.secretSeed;
        if (typeof secretSeed == 'undefined') {
            secretSeed = i.encryptedSeed;
        }
        var secret = (i.markedForDeletion === false ? i.decryptedSeed : hex_to_b32(secretSeed));
        var period = (i.digits === 7 ? 10 : 30);
        var totp_uri = `otpauth://totp/${encodeURIComponent(i.name)}?secret=${secret}&digits=${i.digits}&period=${period}`;
        var qr_size = 250;
        var qr_url = (new QRious({value: totp_uri, size: qr_size})).toDataURL();
        console.group(`${i.originalName} / ${i.name}`);
            console.log('TOTP secret:', secret);
            console.log('TOTP URI:', totp_uri);
            console_image(qr_url, qr_size);
        console.groupEnd();
        return {name: i.name, secret: secret, uri: totp_uri};
    });
    //console.save(data, 'authy_backup.json');
    Export to Bitwarden JSON - Simpler version

    From @oetiker (ref):

    [...] you will get a dump in json format which you can directly copy/paste into the bitwarden import tool. Since Authy does not contain complete login information, I would suggest to create a new folder for the import, so that you can then merge the TOTP tokens into the actual login entries.

    let x = []; 
    appManager.getModel().forEach(i => {
      if (i.decryptedSeed) {
        x.push({
          type: 1, 
          name: i.originalName ?? i.name ?? `[No Name] - Imported from Authy (${x.length})`,
          login: {username: i.name, totp: i.decryptedSeed}
        })
      }});
      console.log(JSON.stringify({ encrypted: false, items: x})
    );
    Export to Bitwarden JSON - Advanced

    This code can be used to save your tokens as a JSON file, for example to import into Bitwarden.
    It will create an Imported from Authy folder, and import your TOTP codes in there.

    // Based on https://github.com/LinusU/base32-encode/blob/master/index.js
    function hex_to_b32(hex) { let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let bytes = []; for (let i = 0; i < hex.length; i += 2) { bytes.push(parseInt(hex.substr(i, 2), 16)); } let bits = 0; let value = 0; let output = ''; for (let i = 0; i < bytes.length; i++) { value = (value << 8) | bytes[i]; bits += 8; while (bits >= 5) { output += alphabet[(value >>> (bits - 5)) & 31]; bits -= 5; } } if (bits > 0) { output += alphabet[(value << (5 - bits)) & 31]; } return output; }
    
    // from https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid#answer-2117523
    function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }
    
    // from https://gist.github.com/gboudreau/94bb0c11a6209c82418d01a59d958c93
    function saveToFile(data, filename) { if (!data) { console.error('Console.save: No data'); return; } if (typeof data === "object") { data = JSON.stringify(data, undefined, 4) } const blob = new Blob([data], { type: 'text/json' }); const e = document.createEvent('MouseEvents'); const a = document.createElement('a'); a.download = filename; a.href = window.URL.createObjectURL(blob); a.dataset.downloadurl = ['text/json', a.download, a.href].join(':'); e.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); a.dispatchEvent(e); }
    
    function deEncrypt({ log = false, save = false }) {
        const folder = {
            id: uuidv4(),
            name: 'Imported from Authy'
        };
    
        const bw = {
            "encrypted": false,
            "folders": [
                folder
            ],
            "items": appManager.getModel().map((i) => {
                let secretSeed = i.secretSeed;
                if (typeof secretSeed == "undefined") {
                    secretSeed = i.encryptedSeed;
                }
                const secret = (i.markedForDeletion === false ? i.decryptedSeed : hex_to_b32(secretSeed));
                const period = (i.digits === 7 ? 10 : 30);
    
                const [issuer, rawName] = (i.name.includes(":"))
                    ? i.name.split(":")
                    : ["", i.name];
                const name = [issuer, rawName].filter(Boolean).join(": ");
                const totp = `otpauth://totp/${name}?secret=${secret}&digits=${i.digits}&period=${period}${issuer ? '&issuer=' + issuer : ''}`;
    
                return ({
                    id: uuidv4(),
                    organizationId: null,
                    folderId: folder.id,
                    type: 1,
                    reprompt: 0,
                    name,
                    notes: null,
                    favorite: false,
                    login: {
                        username: null,
                        password: null,
                        totp
                    },
                    collectionIds: null
                });
            }),
        };
    
        if (log) console.log(JSON.stringify(bw));
        if (save) saveToFile(bw, 'authy-to-bitwarden-export.json');
    }
    
    deEncrypt({
        log: true,
        save: true,
    });
    Export to JSON format (2FSA / Raivo)

    @brenc says (ref):

    I have modified the snippet to produce a Raivo OTP format export file that can be directly imported into 2FAS Auth (and of course Raivo and others I'm sure):

    // Based on https://github.com/LinusU/base32-encode/blob/master/index.js
    function hex_to_b32(hex) {
      let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
      let bytes = [];
      for (let i = 0; i < hex.length; i += 2) {
        bytes.push(parseInt(hex.substr(i, 2), 16));
      }
      let bits = 0;
      let value = 0;
      let output = "";
      for (let i = 0; i < bytes.length; i++) {
        value = (value << 8) | bytes[i];
        bits += 8;
        while (bits >= 5) {
          output += alphabet[(value >>> (bits - 5)) & 31];
          bits -= 5;
        }
      }
      if (bits > 0) {
        output += alphabet[(value << (5 - bits)) & 31];
      }
      return output;
    }
    
    // from https://gist.github.com/gboudreau/94bb0c11a6209c82418d01a59d958c93
    function saveToFile(data, filename) {
      if (!data) {
        console.error("Console.save: No data");
        return;
      }
    
      if (typeof data === "object") {
        data = JSON.stringify(data, undefined, 4);
      }
    
      const blob = new Blob([data], {
        type: "text/json",
      });
    
      const e = document.createEvent("MouseEvents");
      const a = document.createElement("a");
      a.download = filename;
      a.href = window.URL.createObjectURL(blob);
      a.dataset.downloadurl = ["text/json", a.download, a.href].join(":");
    
      e.initMouseEvent(
        "click",
        true,
        false,
        window,
        0,
        0,
        0,
        0,
        0,
        false,
        false,
        false,
        false,
        0,
        null
      );
    
      a.dispatchEvent(e);
    }
    
    const items = appManager.getModel().map((i) => {
        let secretSeed = i.secretSeed;
        if (typeof secretSeed == "undefined") {
            secretSeed = i.encryptedSeed;
        }
        const period = i.digits === 7 ? 10 : 30;
        const secret =
            i.markedForDeletion === false ? i.decryptedSeed : hex_to_b32(secretSeed);
        const [issuer, rawName] = i.name.includes(":")
            ? i.name.split(":")
            : ["", i.name];
        const name = [issuer, rawName].filter(Boolean).join(": ");
        
        return {
            account: name,
            algorithm: "SHA1",
            counter: "0",
            digits: `${i.digits}`,
            iconType: "",
            iconValue: "",
            issuer: name,
            kind: "TOTP",
            pinned: "false",
            secret,
            timer: `${period}`,
        };
    });
    
    saveToFile(items, "Authy-To-Raivo-OTP-Export.json");
    Export to unencrypted JSON format (Aegis)

    @dvshkn says (ref):

    Based on the snippet by @brenc, here is a version that exports to unencrypted Aegis JSON format.
    To keep things short this version only dumps the JSON to the console instead of triggering a file download.
    In Aegis (on your mobile device) use Settings > Import & Export > Import from file and select Aegis file format to import the JSON file.

    // Based on https://github.com/LinusU/base32-encode/blob/master/index.js
    function hex_to_b32(hex) {
      let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
      let bytes = [];
      for (let i = 0; i < hex.length; i += 2) {
        bytes.push(parseInt(hex.substr(i, 2), 16));
      }
      let bits = 0;
      let value = 0;
      let output = "";
      for (let i = 0; i < bytes.length; i++) {
        value = (value << 8) | bytes[i];
        bits += 8;
        while (bits >= 5) {
          output += alphabet[(value >>> (bits - 5)) & 31];
          bits -= 5;
        }
      }
      if (bits > 0) {
        output += alphabet[(value << (5 - bits)) & 31];
      }
      return output;
    }
    
    const items = appManager.getModel().map((i) => {
      let secretSeed = i.secretSeed;
      if (typeof secretSeed == "undefined") {
        secretSeed = i.encryptedSeed;
      }
      // @brenc: All of my Authy accounts have a 20 second period. Not sure why
      //         this was 10.
      const period = i.digits === 7 ? 20 : 30;
      const secret =
        i.markedForDeletion === false ? i.decryptedSeed : hex_to_b32(secretSeed);
      const [issuer, rawName] = i.name.includes(":")
        ? i.name.split(":")
        : ["", i.name];
      const name = [issuer, rawName].filter(Boolean).join(": ");
    
      return {
        type: "totp",
        // NOTE: Aegis generates a fresh UUID if we skip this property
        // uuid: null,
        name,
        issuer: name,
        icon: null,
        info: {
          secret,
          algo: "SHA1",
          digits: i.digits,
          period: period
        }
      };
    });
    
    // Example from https://github.com/beemdevelopment/Aegis/blob/master/app/src/test/resources/com/beemdevelopment/aegis/importers/aegis_plain.json
    const aegis_data = {
      version: 1,
      header: {
        slots: null,
        params: null
      },
      db: {
        version: 1,
        entries: items
      }
    };
    
    // dumps entries to console in Aegis JSON format
    console.log(JSON.stringify(aegis_data, undefined, 4));

    Getting Uncaught ReferenceError: appManager is not defined error?
    Go watch this YouTube video that shows you where you need to be, when you paste code: https://youtu.be/nArCf8iEqlw

  10. Right-click the snippet name on the navigator pane on the left (eg. Script snippet #1) , and choose Run.

Thanks

@wonderingwombat
Copy link

wonderingwombat commented Nov 25, 2024

If anyone else is successful in following @AlexTech01's instructions you can use the following HTML page to quickly create QR codes that you can scan in your chosen successor app. Simply save this to a local .html file, open in a browser and point it to your decrypted JSON file. Try and make sure you update any null issuer fields in the JSON before generating the QR codes.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>QR Code Generator</title>
</head>
<body>
    <h1>QR Code Generator</h1>
    <p>Upload a JSON file to generate QR codes:</p>
    <input type="file" id="fileInput" accept=".json">
    <div id="output"></div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/qrious/4.0.2/qrious.min.js"></script>
    <script>
        document.getElementById('fileInput').addEventListener('change', handleFile);

        function handleFile(event) {
            const file = event.target.files[0];
            if (!file) {
                alert('Please select a file.');
                return;
            }

            const reader = new FileReader();
            reader.onload = function (e) {
                try {
                    const jsonData = JSON.parse(e.target.result);
                    if (jsonData.message === "success" && Array.isArray(jsonData.decrypted_authenticator_tokens)) {
                        generateQRCodes(jsonData.decrypted_authenticator_tokens);
                    } else {
                        throw new Error("Invalid JSON structure");
                    }
                } catch (err) {
                    console.error('Invalid JSON file:', err);
                    alert('Invalid JSON file. Please check the file content.');
                }
            };
            reader.readAsText(file);
        }

        function generateQRCodes(tokens) {
            const output = document.getElementById('output');
            output.innerHTML = ''; // Clear previous output

            tokens.forEach((token, index) => {
                const { name, issuer, decrypted_seed, digits = 6 } = token;

                if (!name || !decrypted_seed) {
                    console.warn(`Skipping entry at index ${index} due to missing fields.`);
                    return;
                }

                // Construct TOTP URI
                const totpUri = `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(name)}?secret=${decrypted_seed}&issuer=${encodeURIComponent(issuer)}&digits=${digits}`;

                // Create QR Code
                const qrCode = new QRious({
                    value: totpUri,
                    size: 250
                });

                // Display QR Code and details
                const container = document.createElement('div');
                container.style.marginBottom = '20px';
                container.innerHTML = `
                    <h3>${name}</h3>
                    <p><strong>Issuer:</strong> ${issuer}</p>
                    <p><strong>Secret:</strong> ${decrypted_seed}</p>
                    <p><strong>TOTP URI:</strong> <a href="${totpUri}" target="_blank">${totpUri}</a></p>
                    <img src="${qrCode.toDataURL()}" alt="QR Code">
                `;
                output.appendChild(container);
            });
        }
    </script>
</body>
</html>

@cusco
Copy link

cusco commented Nov 25, 2024

thank you so much @AlexTech01 , this worked great

I wanted to import this into vaultwarden, and I picked up your cue, and asked ChatGPT to return a output file in the format that vaultwarden would take. This was the script I used, and it worked:

import json
import urllib.parse

# Load source content from file
with open("decrypted_tokens.json", "r") as f:
    authy_data = json.load(f)

# Convert to Bitwarden format
bitwarden_data = {"items": []}

for account in authy_data:
    # Ensure all fields are strings or provide defaults
    name = str(account.get("name", ""))
    issuer = account.get("issuer")  # May be None
    secret = str(account.get("decrypted_seed", ""))
    digits = str(account.get("digits", "6"))

    # Construct TOTP URI
    totp_uri = f"otpauth://totp/{urllib.parse.quote(issuer or '')}:{urllib.parse.quote(name)}?"
    totp_uri += f"secret={urllib.parse.quote(secret)}&digits={digits}"
    if issuer:  # Only add issuer parameter if it exists
        totp_uri += f"&issuer={urllib.parse.quote(issuer)}"
    
    # Add to Bitwarden items
    bitwarden_data["items"].append({
        "name": name,
        "type": 1,  # Type 1 is for login items
        "login": {
            "username": name,
            "totp": totp_uri
        }
    })

# Save to JSON file
with open("vaultwarden_import.json", "w") as f:
    json.dump(bitwarden_data, f, indent=4)

print("Vaultwarden import file created: vaultwarden_import.json")

@smx
Copy link

smx commented Nov 28, 2024

Ok actually I got it working without reverting to stock rom. I'm using a rooted OnePlus 7 Pro with LineageOS 20 and Gapps.

* Disable developer options

* Join Authy Beta

* Update Play Store

* Update Magisk

* Enable Zygisk, disable "enforce denylist", add Google Play Store, Play Services, Google Services Framework, etc to denylist

* Install Zygisk Assistant & Play Integrity Fix

* Clear data/cache for and force stop Play Store, Play Services, Google Services Framework,  etc.

* Reboot

* Install Authy (DON'T OPEN YET)

* Add Authy to denylist

money

I wanted to add that this worked for me using an old Oneplus 6T I had laying around. Stock firmware running Android 9.0.

@andrew867
Copy link

I found a method to rip the TOTPs from the Authy app on an unjailbroken iOS device by intercepting the HTTP requests it receives while logging in and then using a Python script to decrypt the resulting file using the backup password.

THANK YOU SO MUCH!! Holy crap I love you, I'm finally able to move my tokens to a hardware TOTP (Molto 2)!

@bihius
Copy link

bihius commented Dec 3, 2024

Thank you guys AlexTech01 and wonderingwombat!

@bihius
Copy link

bihius commented Dec 3, 2024

If someone wants to switch from Authy to Ente Auth, there is a Python script that converts decrypted_tokens.json to ente_auth_import.plain, which is the import/export file for Ente Auth app. Script:

print('go to hell Authy')


import json
file_path = './decrypted_tokens.json'

with open(file_path, 'r') as file:
    data = json.load(file)
tokens = data['decrypted_authenticator_tokens']

formatted_tokens = []
for token in tokens:
    issuer = token.get('issuer', '')
    name = token['name']
    seed = token['decrypted_seed']
    formatted_line = f'otpauth://totp/{issuer}:{name}?secret={seed}' if issuer else f'otpauth://totp/{name}?secret={seed}'
    formatted_tokens.append(formatted_line)

output_file_path = './ente_auth_import.plain'
with open(output_file_path, 'w') as output_file:
    output_file.write('\n'.join(formatted_tokens))
output_file_path

@andrew867
Copy link

andrew867 commented Dec 4, 2024

If anyone else is using a Molto 2 and needs to bulk import here's a modified tokens.html from above that will output a bunch of commands for a shell script from your decrypted tokens JSON file.

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>QR Code Generator</title>
</head>
<body>
    <h1>QR Code Generator</h1>
    <p>Upload a JSON file to generate QR codes:</p>
    <input type="file" id="fileInput" accept=".json">
    <div id="output"></div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/qrious/4.0.2/qrious.min.js"></script>
	<script src="download.js"></script>
    <script>
        document.getElementById('fileInput').addEventListener('change', handleFile);

function downloadBase64File(contentBase64, fileName) {
    const linkSource = `data:application/pdf;base64,${contentBase64}`;
    const downloadLink = document.createElement('a');
    document.body.appendChild(downloadLink);

    downloadLink.href = linkSource;
    downloadLink.target = '_self';
    downloadLink.download = fileName;
    downloadLink.click(); 
}

function saveImage(binaryContent, fileName) {
const blob = new Blob([binaryContent], { type: 'image/png' }); // create Blob object
download(blob, fileName, 'image/png'); // prompt user to save file 
}

        function handleFile(event) {
            const file = event.target.files[0];
            if (!file) {
                alert('Please select a file.');
                return;
            }

            const reader = new FileReader();
            reader.onload = function (e) {
                try {
                    const jsonData = JSON.parse(e.target.result);
                    if (jsonData.message === "success" && Array.isArray(jsonData.decrypted_authenticator_tokens)) {
                        generateQRCodes(jsonData.decrypted_authenticator_tokens);
                    } else {
                        throw new Error("Invalid JSON structure");
                    }
                } catch (err) {
                    console.error('Invalid JSON file:', err);
                    alert('Invalid JSON file. Please check the file content.');
                }
            };
            reader.readAsText(file);
        }

        function generateQRCodes(tokens) {
            const output = document.getElementById('output');
            output.innerHTML = ''; // Clear previous output
			
			var totpUris = '';
			var moltoCmds = '';

            tokens.forEach((token, index) => {
                const { name, issuer, decrypted_seed, digits = 6, title = ""} = token;

                if (!name || !decrypted_seed) {
                    console.warn(`Skipping entry at index ${index} due to missing fields.`);
                    return;
                }

                // Construct TOTP URI
                const totpUri = `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(name)}?secret=${decrypted_seed}&issuer=${encodeURIComponent(issuer)}&digits=${digits}`;
				
				var command = `python molto2.py --config --profile ${index+1} --synctime --title "${title}" --seedbase32 ${decrypted_seed.toUpperCase()} --timestep 1 --algorithm 1 --display_timeout 2`;
				moltoCmds = moltoCmds + command + "\r\n";
				totpUris = totpUris + totpUri + "\r\n";
                
				
				const qrCode = new QRious({
                // Create QR Code
                    value: totpUri,
                    size: 250
                });

                // Display QR Code and details
                const container = document.createElement('div');
                container.style.marginBottom = '20px';
                container.innerHTML = `
                    <h3>${name}</h3>
                    <p><strong>Issuer:</strong> ${issuer}</p>
                    <p><strong>Secret:</strong> ${decrypted_seed}</p>
                    <p><strong>TOTP URI:</strong> <a href="${totpUri}" target="_blank">${totpUri}</a></p>
                    <img src="${qrCode.toDataURL()}" alt="QR Code">
                `;
                output.appendChild(container);
            });
				// Display QR Code and details
                const container2 = document.createElement('div');
                container2.style.marginBottom = '20px';
                container2.innerHTML = `
                    <pre>${totpUris}</pre>
                `;
                output.appendChild(container2);
								// Display QR Code and details
                const container3 = document.createElement('div');
                container3.style.marginBottom = '20px';
                container3.innerHTML = `
                    <pre>${moltoCmds}</pre>
                `;
                output.appendChild(container3);
			}
    </script>
</body>
</html>```

@bihius
Copy link

bihius commented Dec 4, 2024

Guys, if there is so much will, maybe we should make something like "the ultimate export/import 2fa/totp tool"?

@andrew867
Copy link

Guys, if there is so much will, maybe we should make something like "the ultimate export/import 2fa/totp tool"?

If we do we have to work quick before these bugs are patched in the client app. There’s most likely a way we can spin up an Android VM, install Authy with the rooted phone method, extract data from the Android disk, run it through decryption, display to user in multiple formats. Should be easy to script/automate using Qemu or the Android debugger (licensing though?).

Ideally we need not just otpauth but to encode them into otpauth-migration so you can easily import into something like the Molto GUI.

@soriac
Copy link

soriac commented Dec 5, 2024

has anyone managed to get authy running in an android emulator? i can't root my work phone, and asking for new 2fa QRs for everyone would look bad and be a huge pain

@AnthonyMaiorani
Copy link

I'll be doing this a few times to migrate from Authy to Ente, so I had ChatGPT combine some of the above scripts. This takes the file downloaded from mitmweb, prompts for the password, decrypts it, then converts the file into Ente's plaintext format with a couple extra cleanup features.

import json
import base64
import binascii  # For base16 decoding
from getpass import getpass  # For hidden password input
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

def decrypt_token(kdf_rounds, encrypted_seed_b64, salt, passphrase):
    """
    Decrypts an encrypted seed using PBKDF2HMAC and AES-CBC.

    Args:
        kdf_rounds (int): Number of iterations for the key derivation function.
        encrypted_seed_b64 (str): Base64-encoded encrypted seed.
        salt (str): Salt used in key derivation.
        passphrase (str): Passphrase for decryption.

    Returns:
        str: Decrypted seed as a UTF-8 string or an error message.
    """
    try:
        # Decode the base64-encoded encrypted seed
        encrypted_seed = base64.b64decode(encrypted_seed_b64)

        # Derive the encryption key using PBKDF2 with SHA-1
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA1(),
            length=32,  # AES-256 requires a 32-byte key
            salt=salt.encode(),
            iterations=kdf_rounds,
            backend=default_backend()
        )
        key = kdf.derive(passphrase.encode())

        # AES with CBC mode, zero IV
        iv = bytes([0] * 16)  # Zero IV (16 bytes for AES block size)
        cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
        decryptor = cipher.decryptor()

        # Decrypt the ciphertext
        decrypted_data = decryptor.update(encrypted_seed) + decryptor.finalize()

        # Remove PKCS7 padding
        padding_len = decrypted_data[-1]
        padding_start = len(decrypted_data) - padding_len

        # Validate padding
        if padding_len > 16 or padding_start < 0:
            raise ValueError("Invalid padding length")
        if not all(pad == padding_len for pad in decrypted_data[padding_start:]):
            raise ValueError("Invalid padding bytes")

        # Extract the decrypted seed, base16 decode, and interpret as UTF-8 string
        decrypted_seed_hex = decrypted_data[:padding_start].hex()
        return binascii.unhexlify(decrypted_seed_hex).decode('utf-8')  # Decode base16 and interpret as UTF-8
    except Exception as e:
        return f"Decryption failed: {str(e)}"

def load_encrypted_tokens(input_file):
    """
    Loads encrypted authenticator tokens from a JSON file.

    Args:
        input_file (str): Path to the input JSON file containing encrypted tokens.

    Returns:
        list: List of encrypted token dictionaries.
    """
    try:
        with open(input_file, "r") as json_file:
            data = json.load(json_file)
        return data.get('authenticator_tokens', [])
    except FileNotFoundError:
        print(f"Input file '{input_file}' not found.")
        return []
    except json.JSONDecodeError:
        print(f"Input file '{input_file}' is not a valid JSON file.")
        return []

def format_tokens(formatted_tokens):
    """
    Removes duplicate lines while preserving order.

    Args:
        formatted_tokens (list): List of formatted otpauth URLs.

    Returns:
        list: List of unique formatted otpauth URLs.
    """
    return list(dict.fromkeys(formatted_tokens))

def write_output(formatted_tokens, output_file):
    """
    Writes the formatted tokens to the output file.

    Args:
        formatted_tokens (list): List of formatted otpauth URLs.
        output_file (str): Path to the output file.
    """
    try:
        with open(output_file, 'w') as output_f:
            output_f.write('\n'.join(formatted_tokens))
        print(f"Formatted tokens saved to '{output_file}' with duplicates removed.")
    except IOError as e:
        print(f"Failed to write to output file '{output_file}': {str(e)}")

def main():
    # User configuration
    input_encrypted_file = "authenticator_tokens.json"      # Input file with encrypted tokens
    output_formatted_file = "./ente_auth_import.plain"      # Output file for formatted tokens

    # Prompt for the backup password at runtime (hidden input)
    backup_password = getpass("Enter the backup password: ").strip()

    if not backup_password:
        print("Backup password cannot be empty.")
        return

    # Load encrypted tokens
    encrypted_tokens = load_encrypted_tokens(input_encrypted_file)
    if not encrypted_tokens:
        print("No tokens to process.")
        return

    formatted_tokens = []
    for token in encrypted_tokens:
        decrypted_seed = decrypt_token(
            kdf_rounds=token.get('key_derivation_iterations', 100000),  # Default to 100000 if not specified
            encrypted_seed_b64=token.get('encrypted_seed', ''),
            salt=token.get('salt', ''),
            passphrase=backup_password
        )
        
        if decrypted_seed.startswith("Decryption failed:"):
            print(f"Token '{token.get('name', 'Unknown')}' decryption failed: {decrypted_seed}")
            continue  # Skip tokens that failed to decrypt

        # Safely retrieve and normalize 'issuer' and 'name' fields
        issuer_raw = token.get('issuer')
        name_raw = token.get('name')

        # Normalize whitespace: remove leading and trailing spaces, preserve internal spaces
        issuer = (issuer_raw or '').strip()
        name = (name_raw or '').strip()
        seed = (decrypted_seed or '').strip()

        if not seed:
            print(f"Skipping token '{name}' due to empty decrypted seed.")
            continue  # Skip tokens with empty decrypted seed

        if not name:
            print(f"Skipping a token with empty 'name' field.")
            continue  # Skip tokens with empty 'name'

        if issuer:
            # Ensure no space after colon by concatenating without additional spaces
            formatted_line = f'otpauth://totp/{issuer}:{name}?secret={seed}'
        else:
            formatted_line = f'otpauth://totp/{name}?secret={seed}'

        formatted_tokens.append(formatted_line)

    if not formatted_tokens:
        print("No tokens were successfully decrypted and formatted.")
        return

    # Remove duplicate lines
    unique_formatted_tokens = format_tokens(formatted_tokens)

    # Write to output file
    write_output(unique_formatted_tokens, output_formatted_file)

if __name__ == "__main__":
    main()

@jonathadev
Copy link

locking the bootloader worked for me but I'm still having trouble getting back to login

@gpsu
Copy link

gpsu commented Dec 11, 2024

I followed the steps for iOS (currently 18.1). Got mitmproxy installed successfully on my Mac (M1 MacOS 15.1.1). Successfully specified wifi proxy and launched mitweb. Successfully installed root certificate on my phone. Deleted Authy. Reinstalled it. Tried to log in but get: "An SSL error has occured and a secure connection to the server cannot be made." message.

Does anybody know what I have done wrong (or not done)?

@AlexTech01
Copy link

@gpsu Did you trust the root certificate in Settings -> General -> About -> Certificate Trust Settings after installing it? If you did and it still failed, it might be patched

@gpsu
Copy link

gpsu commented Dec 11, 2024

@gpsu Did you trust the root certificate in Settings -> General -> About -> Certificate Trust Settings after installing it? If you did and it still failed, it might be patched
@AlexTech01 I̶ ̶d̶i̶d̶ ̶t̶r̶u̶s̶t̶ ̶t̶h̶e̶ ̶c̶e̶r̶t̶i̶f̶i̶c̶a̶t̶e̶ ̶a̶s̶ ̶w̶e̶l̶l̶

Well not really, once I really read your question. I did as you wrote and was able to connect. Thanks

@AlexTech01
Copy link

@gpsu I just tested it on the latest app version and can confirm it’s not patched, so I’m not sure what’s going on

@gpsu
Copy link

gpsu commented Dec 11, 2024

@AlexTech01

@gpsu I just tested it on the latest app version and can confirm it’s not patched, so I’m not sure what’s going on

I̶ ̶d̶i̶d̶ ̶t̶r̶u̶s̶t̶ ̶t̶h̶e̶ ̶c̶e̶r̶t̶i̶f̶i̶c̶a̶t̶e̶ ̶a̶s̶ ̶w̶e̶l̶l̶

Well not really, once I really read your question. I did as you wrote and was able to connect. Thanks

@adwaitbhope
Copy link

adwaitbhope commented Dec 12, 2024

You guys are the best. I was following this thread for a while after I missed the desktop app approach and had lost all hope. Thank you @AlexTech01! It worked perfectly.

@uzhbash
Copy link

uzhbash commented Dec 13, 2024

I unfortunately don't have the necessary backup files to get into "Authy Desktop". That said, I have the iOS version of Authy installed on my Mac through the App Store. It's a different app than "Authy Desktop". When I open it, it very briefly shows my last-used token with the accurate numbers. (I'm looking at that same token on my phone as I open the app on my Mac and they are the same, and update every 30 seconds.). But, after that split second, a pop-up masks everything and the app is unusable... Screenshot 2024-08-28 at 11 45 09 AM Anyone have a creative idea for how to get around this and maybe extract tokens from this app?

Boy do I have the same issue! But the thing is I can't log into the iOS app on my phone, looks like I forgot or changed the backup password. So all the accounts/keys which are still there on Mac I can't access them via phone.
Did you find out how to oppress this banner from popping up all the time? I tried to investigate on this with GPT, but it didn't help.

@AlexTech01
Copy link

AlexTech01 commented Dec 13, 2024

@uzhbash What’s the message that appears on the Mac’s Authy app? You may be able to 1) bypass it with an mitmproxy script and then change your backup password to use the iOS part of the guide or 2) extract the saved tokens from the sandboxed keychain for the Authy app.

Do not delete the Authy Mac app since you will be unable to redownload it and will be unable to login anyways since you don’t have the backup password.

@uzhbash
Copy link

uzhbash commented Dec 13, 2024

@uzhbash What’s the message that appears on the Mac’s Authy app? You may be able to 1) bypass it with an mitmproxy script and then change your backup password to use the iOS part of the guide or 2) extract the saved tokens from the sandboxed keychain for the Authy app.

Do not delete the Authy Mac app since you will be unable to redownload it and will be unable to login anyways since you don’t have the backup password.

I appreciate your reply man, but I'm not a tech person to be able to follow your instructions. May I ask you to specify how do I go through the #1 option you mentioned?

@AlexTech01
Copy link

@uzhbash What’s the error message that appears on the Authy Mac app? I need to know that before I can do anything

@uzhbash
Copy link

uzhbash commented Dec 13, 2024

@uzhbash What’s the error message that appears on the Authy Mac app? I need to know that before I can do anything

Screenshot 2024-12-13 at 16 50 29
this!

@rwarnung
Copy link

Hi guys, thank you for all this!
I just have an issue. I installed authy 2.2.3 desktop in windows as explained above. I start it with the debug argumemt, and still I get the message that my system does not fulfill the minimum integrity requirements.
Did anyone experience this as well?

Thanks!

@nickpapoutsis
Copy link

Hi guys, thank you for all this! I just have an issue. I installed authy 2.2.3 desktop in windows as explained above. I start it with the debug argumemt, and still I get the message that my system does not fulfill the minimum integrity requirements. Did anyone experience this as well?

Try deleting the update exe and disabling internet.

@rwarnung
Copy link

Try deleting the update exe and disabling internet.

I deleted the update exe (in 2 places). If I discable internet (you mean on my computer, right?) then I can not login ... I just get a network error ...

Unfortunately, I am struggeling with the first step already. I installed the right (older) version, I added the debug argument, still I get the message with the "minimum integrity ..".

Does anyone experience the same?

@nickpapoutsis
Copy link

then I can not login ... I just get a network error ...

If nothing has changed recently, if you weren't already logged in before the changes you can't log in now regardless of the version used.

If you were logged in in the past you can restore the user files, I have posted somewhere above the paths for Windows.

If you were not logged in before or you don't have a backup of those files you will have to use the Android/iPhone methods.

@BrianInAz
Copy link

BrianInAz commented Dec 24, 2024

Thanks a lot for this! I can confirm that as of today the mitm method still works with iOS.

(⎈|MicroK8s)b@MacBookPro14 TOTP % code .
(⎈|MicroK8s)b@MacBookPro14 TOTP % ls -lh
total 88
-rw-r--r-- 1 b staff 3.0K Dec 24 12:13 ShowQR.html
-rw-r--r-- 1 b staff 1.2K Dec 24 12:16 VaultWarden.py
-rw-r--r--@ 1 b staff 29K Dec 24 09:14 authenticator_tokens.json
-rw-r--r-- 1 b staff 3.5K Dec 24 12:13 decrypt.py
(⎈|MicroK8s)b@MacBookPro14 TOTP % python3 decrypt.py
Enter the backup password:
Decryption completed. Decrypted data saved to 'decrypted_tokens.json'.
(⎈|MicroK8s)b@MacBookPro14 TOTP % ls -lh
total 136
-rw-r--r-- 1 b staff 3.0K Dec 24 12:13 ShowQR.html
-rw-r--r-- 1 b staff 1.2K Dec 24 12:16 VaultWarden.py
-rw-r--r--@ 1 b staff 29K Dec 24 09:14 authenticator_tokens.json
-rw-r--r-- 1 b staff 3.5K Dec 24 12:13 decrypt.py
-rw-r--r--@ 1 b staff 23K Dec 24 12:56 decrypted_tokens.json

I learned a lot playing around with the mitm proxy. Will have to run other apps through it and see how many are allowing you to capture via proxy with self-signed like this.

@hdimantha
Copy link

Thank you for the tutorial.

I'm stuck in the decrypting process on Windows. I have done everything until capturing the tokens.

When I'm running the script, It asks the backup password and then I'm getting the following error:

PS C:\Users\Hassy\Desktop\Newfolder> py decrypt.py
Enter the backup password:
Traceback (most recent call last):
  File "C:\Users\Hassy\Desktop\Newfolder\decrypt.py", line 94, in <module>
    process_authenticator_data(input_file, output_file, backup_password)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Hassy\Desktop\Newfolder\decrypt.py", line 56, in process_authenticator_data
    for token in data['authenticator_tokens']:
                 ~~~~^^^^^^^^^^^^^^^^^^^^^^^^
KeyError: 'authenticator_tokens'

I'm kinda new to this, Am I doing something wrong? Or is this something to do with the scrip running privilages?

Can someone help me to decrypt this please? Thank you.

@hdimantha
Copy link

Thank you for the tutorial.

I'm stuck in the decrypting process on Windows. I have done everything until capturing the tokens.

When I'm running the script, It asks the backup password and then I'm getting the following error:

PS C:\Users\Hassy\Desktop\Newfolder> py decrypt.py
Enter the backup password:
Traceback (most recent call last):
  File "C:\Users\Hassy\Desktop\Newfolder\decrypt.py", line 94, in <module>
    process_authenticator_data(input_file, output_file, backup_password)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Hassy\Desktop\Newfolder\decrypt.py", line 56, in process_authenticator_data
    for token in data['authenticator_tokens']:
                 ~~~~^^^^^^^^^^^^^^^^^^^^^^^^
KeyError: 'authenticator_tokens'

I'm kinda new to this, Am I doing something wrong? Or is this something to do with the scrip running privilages?

Can someone help me to decrypt this please? Thank you.

Hey everyone! Just wanted to circle back and give you an update. After digging into the issue, I realized that I was using the wrong JSON file all along. Once I got the correct file, everything worked perfectly, and the decryption was a success!

And then I used @wonderingwombat's html code to create QR and I was able to add this to 2FAS.

Thanks for reading, and I hope this helps anyone facing a similar issue. 😊

Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment