Skip to content

About

Migrate OTP Auth (iOS) backups to 2FAS or Aegis by decrypting them into an Aegis JSON vault

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

otpauth-to-2fas

Migrate from OTP Auth (iOS) to 2FAS, by way of an Aegis JSON vault.

2FAS cannot read OTP Auth's encrypted .otpauthdb backup, but it can import an unencrypted Aegis vault. This script decrypts the backup and writes that vault, in one step, with no QR codes in between. The same file also imports into Aegis itself, Bitwarden Authenticator (Android) and cotp, so nothing here is tied to 2FAS.

Decryption is done by wenceslas-sanchez/decrypt-otpauth, which this script imports as a library, pinned to a commit. Everything here is the conversion layer on top. macOS and Linux only.

Usage

Clone the repository (the lock file has to sit next to the script) and run it with uv. Dependencies are declared inline (PEP 723) and locked in otpauth_to_2fas.py.lock, so there is nothing to install beyond uv itself:

uv run --locked otpauth_to_2fas.py -p Accounts.otpauthdb -o aegis_plain.json

You will be prompted for the password you set when exporting the backup from OTP Auth (Settings -> Backup).

The first run fetches the dependencies, and a Python 3.13 or newer if none is installed. To convert with the network off, fetch first and then run offline:

uv sync --locked --script otpauth_to_2fas.py
uv run --offline --locked otpauth_to_2fas.py -p Accounts.otpauthdb -o aegis_plain.json

Without uv (needs Python 3.13 or newer, and git):

python3.13 -m venv .venv && . .venv/bin/activate
pip install "git+https://github.com/wenceslas-sanchez/decrypt-otpauth@f99357ac8524c4b4b8e85e8abdd76aeececc9295"
python otpauth_to_2fas.py -p Accounts.otpauthdb -o aegis_plain.json

Options

Flag Effect
-p, --path the encrypted .otpauthdb backup (required)
-o, --output output path, or - for stdout (default aegis_plain.json)
--uris FILE also write one otpauth:// URI per line, for apps that import URIs
--steam write 5-digit, 30-second SHA1 accounts whose issuer or name says "Steam" as Aegis type steam
--folder-notes record the OTP Auth folder name in each entry's note (Aegis shows it, 2FAS mostly ignores it)
--force replace existing output files

Output files are created with mode 0600. Either every output file is written or none is. With --force the previous files are removed first and then recreated, so the result never keeps an old mode, a symlink, or a hard link. An output path may not be the backup itself.

Exit codes:

Code Meaning
0 every account was converted
1 nothing was written to disk: unreadable backup, wrong password, or an output that was refused or failed
2 bad command line
3 some accounts were skipped; the output is written and the skipped accounts are listed
130 cancelled with Ctrl-C

Importing

  • 2FAS: Settings -> Import Tokens (Import/Export in older versions) -> Aegis, then pick the JSON. See the 2FAS import guide.
  • Aegis: Settings -> Import & Export -> Import from file -> Aegis.

2FAS reports how many tokens it found and how many it will import. Compare that with the count the script printed, and verify a few codes against the old app before deleting anything.

Cleaning up

The output is every one of your OTP secrets in cleartext.

  • Write it somewhere that is neither synced nor backed up. On a Mac, ~/Desktop and ~/Documents are in iCloud Drive when Desktop & Documents sync is on, and Time Machine snapshots the rest.
  • Use -o FILE rather than redirecting -o - into a file: the shell creates a redirected file with your umask, not with mode 0600. With -o - on a terminal the secrets also land in the scrollback; clear it.
  • Move it to the phone by AirDrop or a USB cable rather than mail or a cloud drive.
  • Delete it on the Mac and on the phone, including the Files app's Recently Deleted folder. Overwrite-style tools (shred, rm -P) do not reliably destroy old blocks on APFS or SSDs; full-disk encryption (FileVault, LUKS) is the real protection.
  • Keep the encrypted .otpauthdb as your fallback, not the JSON.

Notes and caveats

  • Accounts that no target app could use are skipped and listed on stderr: unknown OTP type, MD5 algorithm (Aegis and 2FAS for iOS silently treat it as SHA1, which generates wrong codes, and 2FAS for Android drops it), empty secret, non-positive period, digit count outside 1 to 10, negative HOTP counter.
  • 2FAS only imports 5 to 8 digits and periods of 10, 30, 60 or 90 seconds, and keeps one entry per secret. The summary marks entries that hit these limits with (!); re-add those by hand.
  • 2FAS has a reported Aegis import bug (twofas/2fas-android#130) affecting entries that share a secret, or whose names contain brand strings. Count your entries after importing.
  • OTP Auth stores no issuer on some accounts. Those get an empty issuer in both the JSON and the --uris file. 2FAS names the service after the issuer, so such entries show up under the account name only.
  • Steam tokens are 5-digit TOTP with a custom alphabet. --steam writes them as Aegis type steam, which both 2FAS and Aegis accept; check the code against the Steam app. In the --uris file they stay otpauth://totp/ with digits=5 and the account's own issuer.
  • The upstream reader validates the OTP type and algorithm codes while it decodes the backup, so an unknown code in any single account makes the whole backup unreadable. The error names the offending value.
  • The summary printed to stderr lists issuers and account names, never secrets.

Tests

uv run --locked tests/test_otpauth_to_2fas.py

The tests build real encrypted .otpauthdb files, so every case runs end to end through the upstream decryptor. tests/data/Accounts.otpauthdb is the test export shipped by decrypt-otpauth (password hello).

License

GPL-3.0, matching the upstream decryption library this script imports. Copyright (C) 2026 Wonjoon Seol.

About

Migrate OTP Auth (iOS) backups to 2FAS or Aegis by decrypting them into an Aegis JSON vault

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages