Skip to content
This repository was archived by the owner on Dec 10, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,21 @@

After discontinuation of Authy Desktop app, it is no longer possible to neither access nor export your 2FA tokens on desktop.

This script makes it possible to **partially** (around 90% in my case) extract your tokens from cached data of **Authy iOS app installed on macOS**.
This script makes it possible to **partially** (around 90% in my case) extract your tokens from cached data of the **Authy iOS app**, on macOS or iOS.

I wrote it for myself in about an hour, so it's far from perfect, but should work. Feel free to improve it :-)

There are likely much better ways to do this, but I found it utterly boring to deeper reverse-engineer the app.
The previous author wrote it for themselves in about an hour, so it's far from perfect, but should work. I improved it slightly by having it output in BitWarden format, and adding support for some changes to the Authy internal files.

## Step 1 – Locate cached keychain

What we are looking for is an encrypted keychain file in iOS Storage Container.

You can use this command to find the right Container, but it requires a
Full Disk Access permission for your Terminal app:

```shell
find ~/Library/Containers/ \
-maxdepth 1 -type d \
-regex ".*/[A-F0-9]\{8\}-[A-F0-9]\{4\}-[A-F0-9]\{4\}-[A-F0-9]\{4\}-[A-F0-9]\{12\}" \
-exec find {} -type f -path "*/Data/Library/Caches/com.authy/fsCachedData/*" \; | \
xargs grep -l "authenticator_tokens"
You can use this command to find the right Container, using Python:
```py
import os
for root, dirs, files in os.walk("/Users/MYUSERNAME/Library/Containers"):
for d in dirs:
if "fsCachedData" in os.path.join(root, d) and "com.authy" in os.path.join(root, d):
print(os.path.join(root, d))
```

Otherwise, you can search for the keychain file manually:
Expand All @@ -30,6 +26,24 @@ Otherwise, you can search for the keychain file manually:
3. In each directory, look for `Data/Library/Caches/com.authy/fsCachedData` folder.
4. If you find such a folder, look for a file with JSON content and `authenticator_tokens` key in it.

If you are doing this on a live (jailbroken) iOS device:

1. Launch Filza
2. Press the Star button on the bottom
3. Press "Apps Manager"
4. Select "Authy"
5. Go to `Library > Caches > com.authy > fsCachedData`
6. Press "Edit"
7. Select all files
8. Press "More"
9. Press "Create Zip"
10. Press on the "i" next to the zip file
11. Press the Share icon in the upper right
12. Press "QuickLook"
13. Press the Share icon in the upper right
14. Airdrop to your Mac
15. Inspect the files for JSON with `authenticator_tokens`

## Step 2 – Decrypt keychain

**First, audit the `decrypt.mjs` script contents.**
Expand All @@ -42,4 +56,4 @@ Then, run the script with the path to the keychain file as an argument:
cat ~/Library/Containers/.../00000000-0000-0000-0000-000000000000 | BACKUP_KEY="your-cool-bACKup-KEY" node decrypt.mjs
```

You should now see your Authy tokens decrypted.
You should now see your Authy tokens decrypted in the console, and a new file called `authyout.json`, which you can import into BitWarden of KeePassXC.
49 changes: 45 additions & 4 deletions decrypt.mjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { readFileSync } from "fs";
import { createDecipheriv, pbkdf2Sync } from "crypto";
import fs from 'fs';
import { v4 as uuidv4, v6 as uuidv6 } from 'uuid';

// Read the JSON from stdin, then filter out non-TOTP tokens
const contents = JSON.parse(
readFileSync(0, "utf8")
).authenticator_tokens.filter((t) => t.encrypted_seed !== undefined);

// The IV is static and equals 16 NULL bytes
const IV = Buffer.from("00000000000000000000000000000000", "hex");
let IV = Buffer.from("00000000000000000000000000000000", "hex");
const defaultIV = "00000000000000000000000000000000";

// Obtain your backup key from the environment variable
const backupKey = process.env.BACKUP_KEY;
Expand All @@ -18,9 +21,10 @@ const backupKey = process.env.BACKUP_KEY;
* @param {String} salt Account salt
* @returns {String} Decrypted seed
*/
function decryptSync(seed, salt) {
function decryptSync(seed, salt, iiv) {
// Authy uses PBKDF2 with PKCS5 padding and 100,000 iterations of SHA1.
// Here, we derive the key from the backup key and the account's salt
IV = Buffer.from(iiv, "hex");
const key = pbkdf2Sync(backupKey, salt, 100000, 32, "sha1");

// Then, we decrypt the seed using AES-256-CBC with the derived key and static IV
Expand All @@ -35,9 +39,46 @@ function decryptSync(seed, salt) {
return decrypted.toString("utf8");
}

// Iterate over each token and decrypt the seed phrase, then output it to stdout
// BitWarden format header
let tobewritten = {
"encrypted": false,
"folders": [
{
"id": uuidv4(),
"name": "Extracted from Authy"
}
],
"items": []
}

// Iterate over each token and decrypt the seed phrase, then output it to stdout, as well as the BitWarden JSON format
// along with the token's name and original name
for (const token of contents) {
const decrypted = decryptSync(token.encrypted_seed, token.salt);

if (token.unique_iv === undefined) {
token.unique_iv = defaultIV;
}

const decrypted = decryptSync(token.encrypted_seed, token.salt, token.unique_iv);
console.log(`${token.name} (${token.original_name})\t ${decrypted}`);
fs.appendFileSync('authyout.txt', `${token.name} (${token.original_name})\t ${decrypted}\n`);
tobewritten.items.push({
"id": uuidv4(),
"organizationId": null,
"folderId": tobewritten.folders[0].id,
"type": 1,
"reprompt": 0,
"name": token.name,
"notes": token.original_name,
"favorite": false,
"login": {
"username": null,
"password": null,
"totp": `otpauth://totp/${token.name}?secret=${decrypted}&digits=${token.digits}&period=30`
},
"collectionIds": null
})
}

// Write the BitWarden JSON to a file
fs.writeFileSync('authyout.json', JSON.stringify(tobewritten, null, 2));