Skip to content

Instantly share code, notes, and snippets.

@gboudreau
Forked from Ingramz/AuthyToOtherAuthenticator.md
Last active October 26, 2025 07:53
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, try this (newest option), or see here (original technique).

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

@PlagueFencer
Copy link

They do respond to GDPR requests but you have to be clear and demanding about what you want because they don't want to do it. Twilio / Authy are a horrible, evil company for trapping users like this.

At first they didn't give me the key export. I replied again demanding it and they said they would but because this worked, I told them to forget it because of the security risk. If it's not needed, it's better to not have them export and email it.

I have also send the GDPR request, hoping they will respond to that.

Sent them a GDPR request 1 month ago, and they just completely ignored me. Since deadline for the GDPR requests is 1 month, I already escalated this to the Finnish Data Protection Ombudsman. We'll see what DPO will do with this.

@ramonsmits
Copy link

At first they didn't give me the key export. I replied again demanding it and they said they would but because this worked, I told them to forget it because of the security risk. If it's not needed, it's better to not have them export and email it.

Does that mean the keys are unencrypted in their backend? My thought was that these are encrypted based on a shared device key.

If that is not securely stored that would be a huge security risk and reasons to pretty much immediately renew any 2FA key stored in Authy.

@AlexTech01
Copy link

AlexTech01 commented Sep 25, 2025

@ramonsmits Your keys are encrypted in their backend using your backup password (which you set when turning on cloud backup of your keys).

@Miniontoby
Copy link

I am trying to export my Authy stuff, but like:

  • I cannot root my phone
  • I have tried the 2.2.3 of the Windows Desktop app, but the appManager.getModel() is empty. (probably messed it up myself by not making a backup of the data)
  • I've tried running a proxy on my phone, but it doesn't want to load or I'm doing something wrong. Might have to try my mom's tablet, as that device has proven to work with proxies in the past.
  • I've tried getting the stuff from the API, with no luck

@yalexxandre
Copy link

yalexxandre commented Oct 6, 2025

I wanted to share my recent journey of a few hours migrating away from Authy on my rooted Android device, hoping it might help others in a similar situation. The solution came from a very unexpected place after many failures.

My Setup:

  • Phone: Samsung Galaxy S23
  • OS: crDroid (Android custom ROM)
  • Root: Sukisu Ultra (a KernelSU fork)

My goal was to migrate all my 2FA accounts from Authy to an open-source authenticator, initially targeting Aegis.

Attempt 1: Direct Import into Aegis

My first steps were to try the most direct methods. First, I tried Aegis's built-in "Import from Authy" feature. This prompted for my Authy backup password, but it failed every time, claiming the password was incorrect, even though I was certain it was right.

Next, using a root file explorer (MiXplorer), I navigated to /data/data/com.authy.authy/shared_prefs/, copied the com.authy.storage.tokens.authenticator.xml file to another folder, and then tried to have Aegis import this copied file. This also failed, but this time with a specific cryptographic error: com.beemdevelopment.aegis.importers.DatabaseImporterException: javax.crypto.BadPaddingException: error:1e000065:Cipher functions:OPENSSL_internal:BAD_DECRYPT.

Attempt 2: android-otp-extractor

Since the direct import methods failed, I moved on to community tools. My next attempt was with the puddly/android-otp-extractor script. However, it also failed, but with a different error: KeyError: 'secretSeed'. It seemed unable to find the secret keys where it expected them to be.

Attempt 3: Frida

As a last resort for automated extraction, I tried the more advanced method using Frida. This is where I hit a major stability issue. Every time I tried to launch the frida-server on my device with root privileges (either via adb root or su), my phone would crash and reboot after showing a syscall timeout error. I tried multiple versions of frida-server, but the result was always the same. It seems there's an instability between Frida and my specific Sukisu Ultra/crDroid setup.

The Breakthrough: Manual File Inspection

At this point, I was about to give up. As a final check, I decided to manually open the same com.authy.storage.tokens.authenticator.xml file that Aegis had failed to import.

When I opened it, I was shocked. For almost all of my accounts, alongside an encryptedSecret field, there was another field: decryptedSecret. This field contained my actual TOTP secret keys in plaintext (Base32).

My Hypothesis:
Some time ago, I had issues with the Authy app and had removed my backup password from within the app settings. I believe this action put the app into an unusual state where it locally decrypted most of the secrets and stored them in this new decryptedSecret field. This would explain why direct imports failed (they likely couldn't handle this non-standard format) and why android-otp-extractor failed (it was looking for secretSeed, not decryptedSecret). (Note: One of my accounts, for NVIDIA, did not have this field and remained encrypted, so I had to reset that one manually).

Conclusion:
I was able to successfully migrate by manually copying each plaintext key from the decryptedSecret field in the XML file into my new authenticator app.

If you are running into issues with automated scripts or direct imports (especially cryptographic or password errors), and you have ever disabled your backup password in the past, I highly recommend using a root file explorer to inspect your com.authy.storage.tokens.authenticator.xml file. The treasure might be sitting right there in plain sight.

Hope this helps someone!

@nick22985
Copy link

I have created https://github.com/nick22985/authy-decryptor you specify the input file and output file / schema and it will take your GDPR or mit proxy file and decrypt it and convert it to the schema you want it in. Feel free to add suggestions and stuff for additional schemas if it is not their or isnt working. I only use ente so the other ones I have implemented were from what I could find online for it so they may not work.

@seanmurdoch01
Copy link

I have created https://github.com/nick22985/authy-decryptor you specify the input file and output file / schema and it will take your GDPR or mit proxy file and decrypt it and convert it to the schema you want it in. Feel free to add suggestions and stuff for additional schemas if it is not their or isnt working. I only use ente so the other ones I have implemented were from what I could find online for it so they may not work.

Hi nick22985, I downloaded the authy-decrypt-windows-x64-0.3.0.exe, but says "this app can't run on your pc". I have tried on Windwos 10 and 11.

@nick22985
Copy link

I have created github.com/nick22985/authy-decryptor you specify the input file and output file / schema and it will take your GDPR or mit proxy file and decrypt it and convert it to the schema you want it in. Feel free to add suggestions and stuff for additional schemas if it is not their or isnt working. I only use ente so the other ones I have implemented were from what I could find online for it so they may not work.

Hi nick22985, I downloaded the authy-decrypt-windows-x64-0.3.0.exe, but says "this app can't run on your pc". I have tried on Windwos 10 and 11.

thanks,
hmm apparently the windows exe did not compile correctly. Ill figure out why and release a updated version of them.

If you want to use it right now you should be able to use the node version published to npm:

  1. downlload node: https://nodejs.org/en/download
  2. in terminal a new terminal (have to close and open a new one after installing node type: npm install -g @nick22985/authy-decryptor
  3. then you should be able to run this command in your terminal: authy-decryptor -i ./authy2facodes.csv -o ./secrets.json --schema ente

@seanmurdoch01
Copy link

I have created github.com/nick22985/authy-decryptor you specify the input file and output file / schema and it will take your GDPR or mit proxy file and decrypt it and convert it to the schema you want it in. Feel free to add suggestions and stuff for additional schemas if it is not their or isnt working. I only use ente so the other ones I have implemented were from what I could find online for it so they may not work.

Hi nick22985, I downloaded the authy-decrypt-windows-x64-0.3.0.exe, but says "this app can't run on your pc". I have tried on Windwos 10 and 11.

thanks, hmm apparently the windows exe did not compile correctly. Ill figure out why and release a updated version of them.

If you want to use it right now you should be able to use the node version published to npm:

  1. downlload node: https://nodejs.org/en/download
  2. in terminal a new terminal (have to close and open a new one after installing node type: npm install -g @nick22985/authy-decryptor
  3. then you should be able to run this command in your terminal: authy-decryptor -i ./authy2facodes.csv -o ./secrets.json --schema ente

Thank you so much Nick. It worked. Used proxyman on IOs device to captcha the url and was able to download the json file and decode it.

@nick22985
Copy link

I have created github.com/nick22985/authy-decryptor you specify the input file and output file / schema and it will take your GDPR or mit proxy file and decrypt it and convert it to the schema you want it in. Feel free to add suggestions and stuff for additional schemas if it is not their or isnt working. I only use ente so the other ones I have implemented were from what I could find online for it so they may not work.

Hi nick22985, I downloaded the authy-decrypt-windows-x64-0.3.0.exe, but says "this app can't run on your pc". I have tried on Windwos 10 and 11.

thanks, hmm apparently the windows exe did not compile correctly. Ill figure out why and release a updated version of them.
If you want to use it right now you should be able to use the node version published to npm:

  1. downlload node: nodejs.org/en/download
  2. in terminal a new terminal (have to close and open a new one after installing node type: npm install -g @nick22985/authy-decryptor
  3. then you should be able to run this command in your terminal: authy-decryptor -i ./authy2facodes.csv -o ./secrets.json --schema ente

Thank you so much Nick. It worked. Used proxyman on IOs device to captcha the url and was able to download the json file and decode it.

cool glad you got it working.

@Mattie112
Copy link

Mattie112 commented Oct 21, 2025

I also got my tokens from Authy by doing a GDPR request. However the files here did not really help me (got "invalid padding lengths"). Screwed around a bit with PHP and got it working. Also putting it in a format I could import in Aegis.

The CSV I got contained the following columns: name,encrypted_seed,salt,iv.

The output is a text file in the otp format (plain text).

<?php

error_reporting(E_ALL);
ini_set('display_errors', '1');

// Define the CSV file path
define('CSV_FILE_PATH', 'encrypted_tokens.csv');

$password = "your password here";
$iteration_counts = 100000;

$csv_data = [];

if (!file_exists(CSV_FILE_PATH)) {
    die("Error: File not found at " . CSV_FILE_PATH);
}

if (($handle = fopen(CSV_FILE_PATH, "r")) !== false) {
    fgetcsv($handle);

    while (($data = fgetcsv($handle)) !== false) {
        $tmp = [];
        $tmp[] = $data[0];
        $tmp[] = $data[1];
        $tmp[] = $data[2];
        $tmp[] = $data[3];
        $csv_data[] = $tmp;
    }

    fclose($handle);
} else {
    die("Error: Unable to open file");
}

echo "Found " . count($csv_data) . " rows" . PHP_EOL . PHP_EOL;

foreach ($csv_data as $row) {
    list($name, $encrypted_seed, $salt, $iv) = $row;

    echo "Processing: " . $name . PHP_EOL;

    $key = hash_pbkdf2('sha1', $password, $salt, $iteration_counts, 32, true);
    $encrypted_data = base64_decode($encrypted_seed);
    $iv_bin = hex2bin($iv);
    $decrypted_seed = openssl_decrypt($encrypted_data, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv_bin);

    if ($decrypted_seed !== false && $decrypted_seed !== '') {
        echo "  SUCCESS (iterations: $iteration_counts): " . $decrypted_seed . PHP_EOL;
        file_put_contents("totp.txt", sprintf("otpauth://totp/%s?secret=%s&digits=6&period=30\n", $name, $decrypted_seed), FILE_APPEND);
        break;
    }

    if ($decrypted_seed === false || $decrypted_seed === '') {
        echo "  FAILED: Could not decrypt" . PHP_EOL;
    }

    echo PHP_EOL;
}

Perhaps someone can use it :) Input is a file in the same directory encrypted_tokens.csv, set your password on line 9

@LucasArruda
Copy link

@Mattie112 that's an amazing idea. I got most of my tokens but I am still missing some recent. Going to do the same.

Is the password just for making the aegis file encrypted?

@NoSubstitute
Copy link

NoSubstitute commented Oct 25, 2025

  • @AlexTech01 I used your guide and it worked great, but for one thing.

  • I added it as an issue.

  • It adjusted the name of my posts, which I don't want.

  • I have 147 codes, and it's going to be a pain to adjust them all manually.

Nevermind, I found the setting in the script, which I hadn't adjusted. Sorry.

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