Skip to content

Instantly share code, notes, and snippets.

@gboudreau
Forked from Ingramz/AuthyToOtherAuthenticator.md
Last active November 28, 2024 06:48
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

@Bizzaro
Copy link

Bizzaro commented Nov 5, 2024

Hey everyone, I'm stoked to share that I finally managed to move my tokens from Authy to 2FAS using a rooted Android phone (take that, Authy!). It took me a few weeks and a bunch of different phones, but I cracked the code on that pesky "The device does not meet the minimum integrity requirements" message.

Here's what I figured out:

* Authy checks if developer mode is on

* It also runs a Google Play Integrity check

* Fixing Google Play Integrity with Magisk doesn't seem to work on newer phones (Android 12/14)

* Authy doesn't care about your app list - even if you have Magisk installed

* Not sure if Authy checks for custom ROMs, but I'd stick with stock to be safe

Here's how I did it, building on tips from others in this gist - big thanks to everyone who contributed!

1. Got my hands on a Nexus 6P running Android 8 (newer phones like Samsung S22 and Xiaomi 12 didn't work with this method)

2. Unlocked the bootloader and flashed Android 8 stock ROM

3. Installed Magisk

4. Set up PlayIntegrityFix and Shamiko

5. Turned on Zygisk

6. Rebooted

7. Made sure Shamiko was running

8. Downloaded Authy from the Play Store

9. Logged in (should work now)

10. Unlocked all tokens with the master password

11. Grabbed Aegis from the Play Store

12. Imported Authy tokens to Aegis (thanks Aegis!)

13. Exported Aegis vault (chose unencrypted)

14. Transferred the JSON to my main phone

15. Imported the JSON into 2FAS

16. Success!

Good luck to everyone trying this out!

Thanks for this, it helped a lot. Repeated on rooted Pixel 2 running Android 11. Done in under 30mins.

Some gotchas that tied me up (coming back to Android after 10 years on iOS is... disorienting 🤣)-

Android CLI tools on Ubuntu:

sudo apt-get -y google-android-platform-tools-installer

Arch

yay -S android-sdk-platform-tools
sudo ln -s /opt/android-sdk/platform-tools/adb /usr/bin/adb
sudo ln -s /opt/android-sdk/platform-tools/fastboot /usr/bin/fastboot

https://github.com/topjohnwu/Magisk/releases

adb devices
adb reboot bootloader
fastboot devices
fastboot flashing unlock (this will wipe your phone)
adb install Magisk-v28.0.apk

Links to the 2 .zip files that you install inside the Modules tab in Magisk:

To patch boot.img for the Pixel 2, make sure you get the Factory Image and not the Full OTA Image.
https://developers.google.com/android/images#walleye

Use adb push and adb pull commands for easy access to phone storage. Tab completion also works for filenames using adb pull

adb push boot.img /sdcard/Download

Open Magisk, click Install and browse to the boot.img we loaded

adb pull /sdcard/Download/magisk_patched-28000_N2bQM.img .
adb reboot bootloader
fastboot devices
fastboot flash boot magisk_patched-28000_N2bQM.img
fastboot reboot
adb push PlayIntegrityFix_v17.9.zip /sdcard/Download
adb push Shamiko-v1.1.1-357-release.zip /sdcard/Download

Install the two modules.


Also if you're getting a javax.crypto.BadPaddingException: Decryption error in Aegis when you're trying to decrypt the Authy file with encrypted backup files enabled, scroll through the list of codes and make sure none of them are locked. If one of them is still locked, delete the code and then close both Authy and Aegis, and re-try the import. I had a very old 2FA account that didn't sync properly for some reason in Authy and it prevented the import into Aegis.

I ran YASNAC - SafetyNet Checker just in-case before trying Authy for the first time.

Obligatory fuck Authy 🏴‍☠️🏴‍☠️🏴‍☠️

I tried on an old Nexus 6 before I found this thread but I gave up quick, it should work though...

Nexus 6 with stock ROM (Android 7.1.1) + Magisk doesn't work because we need Android 8+ for the root detection bypass modules. Magisk kept crashing during module install so I tried with adb shell and su magisk --install-module ZIP to surface the error message.

Spent 3 hours but 🎉🎉🎉 confirmed working on Nexus 6 with Lineage OS 18.1 (Android 11), Magisk and two detection bypass modules. Just in case someone was wondering if it would work 😉 It should really only take an hour if you go straight to trying with LOS 18.1.

https://xdaforums.com/t/official-lineageos-18-1-for-the-nexus-6.4255327/

@MonkeySaint
Copy link

  • Fixing Google Play Integrity with Magisk doesn't seem to work on newer phones (Android 12/14)

You can do it on modern android versions, but pif doesn't work you need trickeystore installed and having your own keybox. All authy checks is if your device meets strong integrity (which requires tricky store or a locked bootloader). If authy could detect root then google would be able to too. You may also be able to just lock your bootloader and use pif. That should theoretically work but I haven't tested it. Also just fyi shamiko actually breaks pif right now. Don't use shamiko if you want to make it work on a modern device.

@cusco
Copy link

cusco commented Nov 7, 2024

hello all,

for those of us that have no android phone, is there an emulator that can pass integrity check?

I tried WSA with no success...

@marcussacana
Copy link

  • Fixing Google Play Integrity with Magisk doesn't seem to work on newer phones (Android 12/14)

You can do it on modern android versions, but pif doesn't work you need trickeystore installed and having your own keybox. All authy checks is if your device meets strong integrity (which requires tricky store or a locked bootloader). If authy could detect root then google would be able to too. You may also be able to just lock your bootloader and use pif. That should theoretically work but I haven't tested it. Also just fyi shamiko actually breaks pif right now. Don't use shamiko if you want to make it work on a modern device.

lock bootloader? need to take care to don't softbrick the phone.

@easly1989
Copy link

I was trying to follow this guide, but I've an non-rooted phone (and I don't want to root it). Also It seems that authy desktop is no longer a thing.
Can someone tell me how can I export my totp library so that I can import it to Ente or any other 2fa app?

@Menchen
Copy link

Menchen commented Nov 13, 2024

@easly1989 Without root or jailbreak, your best chance is manually migrating each account…

@easly1989
Copy link

@easly1989 Without root or jailbreak, your best chance is manually migrating each account…

thats a shame....

@jaikumarm
Copy link

Leave here for those that have jailbroken iOS, the secrets in base32 are stored in the keychain and can be extracted with https://github.com/ptoomey3/Keychain-Dumper (I have compiled from source, changed entitlement to same access as authy as resign on device with ldid) combined with preference plist as seen in https://gist.github.com/gboudreau/94bb0c11a6209c82418d01a59d958c93?permalink_comment_id=5137446#gistcomment-5137446 found in filza and manually clean up I have been able to export all ~30 TOTP..

Tested with iOS 16 and dopamine. Hope this helps someone... Although I'm not sure if resetting all account would be faster... (~5h including research)

can you provide more details on how you did this, I have jailbroken ios, but the keychain dumper does not seem to working..

@Enissay
Copy link

Enissay commented Nov 16, 2024

After running ./authy --remote-debugging-port=5858 I am unable to login as I keep getting The device does not meet the minimum integrity requirements

@Menchen
Copy link

Menchen commented Nov 16, 2024

Leave here for those that have jailbroken iOS, the secrets in base32 are stored in the keychain and can be extracted with https://github.com/ptoomey3/Keychain-Dumper (I have compiled from source, changed entitlement to same access as authy as resign on device with ldid) combined with preference plist as seen in https://gist.github.com/gboudreau/94bb0c11a6209c82418d01a59d958c93?permalink_comment_id=5137446#gistcomment-5137446 found in filza and manually clean up I have been able to export all ~30 TOTP..
Tested with iOS 16 and dopamine. Hope this helps someone... Although I'm not sure if resetting all account would be faster... (~5h including research)

can you provide more details on how you did this, I have jailbroken ios, but the keychain dumper does not seem to working..

@jaikumarm, almost missed your message.

  1. The first thing you need is the keychain group of your authy, I got mine using AppIndex and cheking the app's enititlement, should be under keychain-access-groups, and like XXXXXXXXXX.com.authy.XXXXXXXXX.
  2. Next, clone https://github.com/ptoomey3/Keychain-Dumper and modify entitlements.xml <string>groupName or *</string> to your keychain group name.
  3. You need the keychain_dumper binary, either compile yourself using a Mac and XCode or get from GitHub release, I'm not sure which is the target version, though.
  4. Get ssh onto the phone, the default user is mobile, once in, switch to the root user sudo su
  5. scp or Filza both keychain_dumper and entitlements.xml to same folder (Due to rootless jailbreak, go to /var/jb/ if you have write problem even with root user).
  6. run ldid -Sentitlements.xml keychain_dumper to resign with new entitlement, install ldid if missing.
  7. chmod +x ./keychain_dumper Give permission.
  8. With the phone UNLOCKED!!!, dump with ./keychain_dumper -g > dumppass. There should be one entry for each password.
  9. Next we need to look up the Authy's account ID to the account name, In the dump content, the Service field is the account ID and Keychain Data is the secret key in base32 ready to import into any authenticator.
  10. (Optional?) You could just import the key into Ente auth and match the code in Authy to recover the account name, or you could look up the config file of Authy using Filza by going to Authy's AppGruop (AppIndex can give you the path), (Your app group)/Library/Preferences/group.authy.XXXXXX.plist, The numeric entries should match with Service field in dump.

If under 50 accounts, DO NOT attempt to write a script to parse the plist and dump to create a QR code, based on my personal experience, it's not worth the hassle (I would even recommend skipping step 10 and just match the account and edit manually).

@jaikumarm
Copy link

@Menchen , thank you so much for providing such detailed steps. it was super helpful.

I really only ran into one issue, since I was using ios 15.8.3 on an iPhone SE (1st Gen) the keychain_dumper version 1.2.0 did not work; it failed with the lib mismatch issue.. I tracked it to issue 71 and followed workaround to try v1.1.0, that worked!!

I followed the rest of steps and I now have a dumppass and group.authy.XXXXXX.plist on my mac.

Now unfortunately I do have 60+ accounts :( For now am going to try to add a few manually and see how it goes, but once am confident, I might chase into rabbit hole to try and script it to import into Ente with matching account names, please let me know if you have any pointers from your experience

@Menchen
Copy link

Menchen commented Nov 16, 2024

@jaikumarm, I did it by first converting dumppass to more machine-readable using regex(yeah... not rgeat idea) to a dictionary format like {serviceid:secret}, then I converted plist to json using python (builtin plist library to python dict and serialize to json), then I used jq to format it by

cat pp.json | jq '[.[] | objects | select(.name != null) | {name:.name,id:._id,oname:.original_name,aname:.account_name,oi:.original_issuer}] | INDEX(.id)'

Then by iterating plist json, create a string with the following format:

otpauth://totp/${encodeURIComponent(i.name)}?secret=${secret}&digits=6&period=30

for example, from Yubico: otpauth://totp/Example:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Example

Then you just need to create a QR code of that string and display it one at the time by using input() as pause.

I used https://pypi.org/project/qrcode/ as qr library, it allows printing qr code as ascii.

This is the for loop I used, even after jq and regex I had to manually modify the dictionary using pop to delete entries (Twitch and duplicates):

for i in j2:
    print(i)
    k = f'otpauth://totp/{j2[str(i)]["aname"] +":"+ j2[str(i)]["name"]}?secret={j[i]}&digits=6&period=30'
    qr = qrcode.QRCode()
    qr.add_data(k)
    qr.print_ascii()
    print(k)
    input('...')

j2 is the plist json after jq, j is the dumppass dict of {id:secret} format, I think.

I would strongly recommend matching with Authy after importing all, also it would be helpful to someone else if you can document it better, due to trial and error, my attempt is so messy that I can't share it easily due to sensitive information.

Good luck.


Some random code fragment that I used:

import plistlib
f = open('./group.authy.XXXXXX.plist','rb')
p = plistlib.load(f) # load plist as dict

Regex to convert to "dict" like string... (I should just create a dict in python... )

The input string is like, I filtered using grep I think:

cat passdump.txt | grep -e '^Service: Google' -e 'Keychain Data:'
Service: GoogleXXXXApp

Keychain Data: XXXX

Service: GoogleXXXXXApp

Keychain Data: XXXXXXX

Service: GoogleXXXApp

Keychain Data: XXXXXXXXXXXX
subst = "'\\1':'\\2',"
regex = r"Service: Google([0-9]+)App\nKeychain Data: (.+)$"
result = re.sub(regex, subst, mystr, 0, re.MULTILINE)
if result:
    print (result)

Please have no hesitation to ask for more detail.

@jaikumarm
Copy link

@Menchen thanks again for the pointers, I was able to get all of the TOTPs migrated into Ente..
In anycase, here is the script I ended up writing.. hope it helps someone.. I may release it in a repo if there is interest.

import datetime
import plistlib

authy_plist_filename = "./secrets/group.authy.XXXXXX.plist"
authy_dumppass_filename = "./secrets/dumppass"
simple_totp_filename = "./secrets/simple_totp.txt"


def read_dumppass_file(authy_dumppass_filename):
	# read dumppass file and return the data in a dictionary list
	with open(authy_dumppass_filename, 'r') as dumppass_file:
		dumppass_lines = dumppass_file.readlines()
		dumppass_data = []
		for i in range(0, len(dumppass_lines)):
			if dumppass_lines[i].startswith("Service:"):
				service = dumppass_lines[i].split(":")[1].strip()

				if service.startswith("Google"):
					service_id = service.split("Google")[1].split("App")[0]
				elif service.startswith("AuthyApp"):
					#print ("service: %s" % service)
					service_id = service.split("AuthyApp")[1].split("Data")[0]
				else:
					#print ("service: %s" % service)
					#print ("Service ID does not match with known format")
					service_id = None
				#print ("Service ID: %s" % service_id)

				account = dumppass_lines[i+1].split(":")[1].strip()
				keychain_data = dumppass_lines[i+9].split(":")[1].strip()
				dumppass_data.append({
					"service": service,
					"service_id": service_id,
					"account": account,
					"keychain_data": keychain_data
				})
	return dumppass_data 


def read_authy_plist_file(authy_plist_filename):
	# read the authy plist file and return the data in a dictionary list
	with open(authy_plist_filename, 'rb') as authy_plist_file:
		authy_pl = plistlib.load(authy_plist_file)
	
		authy_data = []
		for key in authy_pl:
			# if key has value starting with ASSET_MD5_FOR then skip it
			if key.startswith("ASSET_MD5_FOR"):
				continue
			#print ("Key: %s Value: %s" % (key, authy_pl[key]))
			authy_data.append({
					"key": key,
					"value": authy_pl[key]
				})
			#print("Key: %s Value: %s" % (key, authy_pl[key]))
	
	return authy_data

def	matched_authy_and_dumpass(authy_pl_data, dumppass_data_list):
	# compare the data from the authy plist file and the dumppass file
	# and return the matched data in a dictionary list
	matched_data = []
	for authy_data in authy_pl_data:
		for dumppass_data in dumppass_data_list:

			if authy_data["key"] == dumppass_data["service_id"]:
				#print("Match Found: %s" % authy_data["key"])
				matched_data.append({
					"service_id": authy_data["key"],
					"service": dumppass_data["service"],
					"account": dumppass_data["account"],
					"keychain_data": dumppass_data["keychain_data"],
					"account_type": authy_data["value"]["account_type"] if "account_type" in authy_data["value"] else None,
					"original_name": authy_data["value"]["original_name"] if "original_name" in authy_data["value"] else None,
					"name": authy_data["value"]["name"] if "name" in authy_data["value"] else None,
					"type": authy_data["value"]["type"] if "type" in authy_data["value"] else None,
					"digits": authy_data["value"]["digits"] if "digits" in authy_data["value"] else None,
				})
				break
	
	return matched_data
			
def export_to_file_in_totp(matched_data):
	# write the matched data to a file
	with open(simple_totp_filename, 'w') as simple_totp_filen:
		# totp format
		# otpauth://totp/{account_name}?secret={secret_key}&digits={digits}&period=30
		for data in matched_data:
			if data["type"] == "AuthyApp":
				print ("Service %s is type AuthyApp, TOPT needs to migrated mnaually, skipping" % data["name"])
				continue
			base_str = "otpauth://" + "totp/" + data["name"] + "?secret=" + data["keychain_data"] + "&digits=" + str(data["digits"]) + "&period=30"
			#print(base_str)
			simple_totp_filen.write(base_str + "\n")

	# open the file and count the number of keys written to the file
	with open(simple_totp_filename, 'r') as simple_totp_filen:
		lines = simple_totp_filen.readlines()
		print("len of keys written : %d" % len(lines))

	return

if __name__ == "__main__":
	authy_pl_data = read_authy_plist_file(authy_plist_filename)
	print("len of authy_pl_data: %d" % len(authy_pl_data))
	#print("authy_pl_data: ", authy_pl_data)
	dumppass_data = read_dumppass_file(authy_dumppass_filename)
	print("len of dumppass_data: %d" % len(dumppass_data))
	#print("dumppass_data: ", authy_pl_data)
	matched_data = matched_authy_and_dumpass(authy_pl_data, dumppass_data)
	print("len of matched_data: %d" % len(matched_data))
	#print("Matched Data: %s" % matched_data)
	export_to_file_in_totp(matched_data)

@seupedro
Copy link

seupedro commented Nov 24, 2024

Just let you know guys: Android Virtual Devices (AVD) like Genymotion or Android Emulator doesn't work. It cannot pass on safetynet check. no matter what.

@AlexTech01
Copy link

AlexTech01 commented Nov 24, 2024

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.

Step 1: Dumping encrypted TOTPs

First, you need to install mitmproxy (https://mitmproxy.org) on your computer and then start it for the domain api.authy.com using the mitmweb --allow-hosts "api.authy.com" command. Next, connect your iOS device to mitmproxy by going to Settings -> Wi-Fi -> (your network) -> Configure Proxy, then enter your computer's local IP for the address and 8080 for the port. You then need to go to mitm.it in Safari while connected to the proxy and install the root certificate for iOS. (Note that this certificate is specific to your mitmproxy instance and is not shared.) After installing it, you will need to go to Settings -> General -> About -> Certificate Trust Settings to enable trust for the certificate.

After that, keep the proxy on, and open the Authy app while logged out. If you are logged in to the app already, you will need to delete and reinstall the app from the App Store before continuing. Sign in to the Authy app like normal, and wait until the app shows you a list of your TOTPs and asks you for your backup password. At this point, go to the mitmweb interface and search for "authenticator_tokens" (switch to the Flow List tab if you don't see a search bar), then look through the resulting requests until you find one with the response tab showing JSON looking like this:

{ "authenticator_tokens": [ { "account_type": "example", "digits": 6, "encrypted_seed": "something", "issuer": "Example.com", "key_derivation_iterations": 100000, "logo": "example", "name": "Example.com", "original_name": "Example.com", "password_timestamp": 12345678, "salt": "something", "unique_id": "123456", "unique_iv": null }, ...

Obviously, yours will show a bunch of real info about the TOTPs you have in your Authy account. Once you find this request, click "Download" on the Flow tab with it selected and it should download an "authenticator_tokens" file to your device. Rename this file to authenticator_tokens.json before exiting out of mitmweb (hit Ctrl+C on the terminal window running it) and disconnecting from the proxy on your device's settings.

Step 2: Decrypting TOTPs with backup password

I had ChatGPT write a Python script for me that takes the authenticator_tokens.json file and decrypts the TOTPs using your backup password. In order to use it, you must have the "cryptography" package installed from pip and you must have an "authenticator_tokens.json" file in the same directory you're running the script from. To use it, run python3 decrypt.py with everything set up as mentioned, then enter your backup password at the prompt, and you should have a "decrypted_tokens.json" file saved once it's done. I have tested this method and was able to successfully dump and decrypt my TOTP tokens and import them to iCloud Passwords without any issue.

Compatibility note

Note that this method could be patched by Twilio in an app update so that Authy ignores self-signed certificates you trust in Settings, and that this doesn't work on unrooted Android due to apps on Android not trusting certificates from the user store by default and rooting being needed to add certificates to the system store.

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):
    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 process_authenticator_data(input_file, output_file, backup_password):
    with open(input_file, "r") as json_file:
        data = json.load(json_file)

    decrypted_tokens = []
    for token in data['authenticator_tokens']:
        decrypted_seed = decrypt_token(
            kdf_rounds=token['key_derivation_iterations'],
            encrypted_seed_b64=token['encrypted_seed'],
            salt=token['salt'],
            passphrase=backup_password
        )
        decrypted_token = {
            "account_type": token["account_type"],
            "name": token["name"],
            "issuer": token["issuer"],
            "decrypted_seed": decrypted_seed,  # Store as UTF-8 string
            "digits": token["digits"],
            "logo": token["logo"],
            "unique_id": token["unique_id"]
        }
        decrypted_tokens.append(decrypted_token)

    output_data = {
        "message": "success",
        "decrypted_authenticator_tokens": decrypted_tokens,
        "success": True
    }

    with open(output_file, "w") as output_json_file:
        json.dump(output_data, output_json_file, indent=4)

    print(f"Decryption completed. Decrypted data saved to '{output_file}'.")


# User configuration
input_file = "authenticator_tokens.json"  # Replace with your input file
output_file = "decrypted_tokens.json"  # Replace with your desired output file

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

# Process the file
process_authenticator_data(input_file, output_file, backup_password)

@Enissay
Copy link

Enissay commented Nov 25, 2024

and that this doesn't work on unrooted Android

That's a pity... I'll be waiting for someone to bring a miracle solution so I can migrate my 50+ codes out of Authy :x

@AlexTech01
Copy link

and that this doesn't work on unrooted Android

That's a pity... I'll be waiting for someone to bring a miracle solution so I can migrate my 50+ codes out of Authy :x

Unfortunately I don't have a method to export it with an unrooted Android device and don't really see a possibility for one, my recommendation is to either borrow a friend's iPhone or manually change 2FA settings on all 50+ websites

@Enissay
Copy link

Enissay commented Nov 25, 2024

or manually change 2FA settings on all 50+ websites

After being angry, I finally accepted my destiny as this is likely what I will endup doing... Will have, I will kick it off "next year" ;-)
Unless someone comes up with a miracle hack... I'll keep watching this thread until then xD

@wonderingwombat
Copy link

@AlexTech01 Thanks so much! Your instructions worked perfectly. The only thing I will add is that I had to authenticate my new device using an existing device. Trying to authenticate using SMS gave me something related to an "Attestation" error.

@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.

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