Skip to content

Instantly share code, notes, and snippets.

@dnicolson
Created August 1, 2025 11:59
Show Gist options
  • Save dnicolson/71878557d80aef4459066513a538ecc2 to your computer and use it in GitHub Desktop.
Save dnicolson/71878557d80aef4459066513a538ecc2 to your computer and use it in GitHub Desktop.
Extract Authy OTP accounts and Keychain secrets from an encrypted iOS backup
// Usage:
// npm i otplib fs-extra irestore simple-plist prompt-sync cli-table3 tmp
// node authy-extractor.js <iOS backup path>
const { authenticator } = require('otplib');
const fs = require('fs-extra');
const IRestore = require('irestore');
const path = require('path');
const plist = require('simple-plist');
const prompt = require('prompt-sync')({ sigint: true });
const Table = require('cli-table3');
const tmp = require('tmp');
async function decryptBackup(backupPath, tempDir) {
const password = prompt('Enter password: ', { echo: '*' });
const iRestore = new IRestore(backupPath, password);
await iRestore.restore('AppDomainGroup-group.authy.128FD4', path.join(tempDir.name, 'Authy'));
await iRestore.dumpKeys(path.join(tempDir.name, 'keys.json'));
}
function getAccounts(tempDir) {
const authyPlistPath = path.join(tempDir.name, 'Authy/Library/Preferences/group.authy.128FD4.plist');
const authyPlist = plist.readFileSync(authyPlistPath);
const genericPasswords = JSON.parse(fs.readFileSync(path.join(tempDir.name, 'keys.json')))['General'];
return Object.entries(authyPlist)
.filter(([id]) => !isNaN(Number(id)))
.map(([id, value]) => ({
id,
name: value.name,
secret: Buffer.from(genericPasswords.filter(password => password.svce === `Google${id}App`)[0]['v_Data'], 'base64').toString('utf-8'),
original_issuer: value.original_issuer,
digits: Number(value.digits),
}));
}
function renderTable(accounts) {
const table = new Table({
head: ['ID', 'Name', 'Original Issuer', 'Digits', 'Secret', 'OTP Code'],
});
for (const account of accounts) {
const { id, name, original_issuer, digits, secret } = account;
const otp = authenticator.generate(secret);
table.push([id, name, original_issuer, digits, secret, otp]);
}
return table.toString();
}
async function main() {
const backupPath = process.argv[2];
const tempDir = tmp.dirSync({ unsafeCleanup: true });
await decryptBackup(backupPath, tempDir);
const accounts = getAccounts(tempDir);
console.log(renderTable(accounts));
tempDir.removeCallback();
}
main();
@CasperNielsen
Copy link

Hi,
I hope you can help me with this error.
I’ve tried different paths, but I keep getting the same error every time. 😕
All the dependencies were installed without any issues.

I’m using Node/NPM installed via Homebrew.

➜ ~ node authy-extractor.js /Users/Capser/Library/Application\ Support/MobileSync/Backup/00008101-001A55541841401E-20230715-190049
Enter password: **************
node:internal/process/promises:392
new UnhandledPromiseRejection(reason);
^
UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "pty error, please try again.".
at throwUnhandledRejectionsMode (node:internal/process/promises:392:7)
at processPromiseRejections (node:internal/process/promises:475:17)
at process.processTicksAndRejections (node:internal/process/task_queues:106:32) {
code: 'ERR_UNHANDLED_REJECTION'
}
Node.js v24.5.0
➜ ~

@dnicolson
Copy link
Author

What version of npm are you using? It's possible irestore cannot be found, what do you see with the which irestore command?

@CasperNielsen
Copy link

➜ ~ npm --version
11.5.1
➜ ~ which irestore
/opt/homebrew/bin/irestore
➜ ~

@dnicolson
Copy link
Author

Is the backup encrypted?

What happens with this command:
irestore <iOS backup path> dumpkeys > keys.json

@CasperNielsen
Copy link

Found the issue:
The terminal didn’t have Full Disk Access to the protected areas.

To fix it:
Go to System Settings → Privacy & Security → Full Disk Access and enable access for terminal app (e.g. Terminal, iTerm).

Thanks for the quick help, now it's working :)

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