UNPKG

authy-extractor

Version:

Extract 2FA tokens from Authy

179 lines (178 loc) 8.35 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const axios_1 = __importDefault(require("axios")); const inquirer_1 = __importDefault(require("inquirer")); const uuid_1 = require("uuid"); const fs_1 = require("fs"); const api = __importStar(require("./api")); const constants_1 = __importDefault(require("./constants")); const decrypt_1 = __importDefault(require("./decrypt")); const otp_1 = __importDefault(require("./otp")); const utils_1 = require("./utils"); // Inject axios defaults axios_1.default.defaults.baseURL = constants_1.default.API_BASE_URL; axios_1.default.defaults.headers.common['X-User-Agent'] = constants_1.default.USER_AGENT; axios_1.default.defaults.headers.common['X-Authy-Device-App'] = constants_1.default.DEVICE_APP; axios_1.default.defaults.params = { api_key: constants_1.default.API_KEY, locale: constants_1.default.LOCALE, }; // Inject new RequestID for every request axios_1.default.interceptors.request.use((config) => { // eslint-disable-next-line no-param-reassign config.headers['X-Authy-Request-ID'] = uuid_1.v4(); return config; }); // Calculate moving factor correction with every request let movingFactorCorrection = 0; axios_1.default.interceptors.response.use((response) => { // SyncTimeSync.prototype.syncTime const serverTime = new Date(response.headers.date).getTime(); movingFactorCorrection = Math.round((serverTime - new Date().getTime()) / 1000); return response; }); function run() { return __awaiter(this, void 0, void 0, function* () { try { const { phoneNumber } = yield inquirer_1.default.prompt([ { type: 'input', name: 'phoneNumber', message: 'Phone number followed by country code, eg. 48-700100100', validate: (value) => value.match(/^[0-9]+-[0-9]+$/) !== null || 'Please enter valid phone number', }, ]); const deviceStatus = yield api.getDeviceStatus(phoneNumber); if (deviceStatus.message !== 'active' || !deviceStatus.authy_id) { throw new Error('Account must be active to perform a backup'); } const { authMethod } = yield inquirer_1.default.prompt([ { type: 'list', name: 'authMethod', message: 'Authentication method', choices: [ { name: 'SMS', value: 'sms' }, { name: 'Call', value: 'call', disabled: 'Currently unsupported' }, { name: 'Other device', value: 'push', disabled: 'Currently unsupported' }, ], }, ]); yield api.createNewDeviceRequest(deviceStatus.authy_id, authMethod, constants_1.default.DEVICE_APP, constants_1.default.DEVICE_NAME); const { pin } = yield inquirer_1.default.prompt([ { type: 'password', name: 'pin', message: 'PIN code', mask: '*', validate: (value) => value.match(/^[0-9]+$/) !== null || 'Please enter valid PIN code', }, ]); const { device } = yield api.registerNewDevice(deviceStatus.authy_id, pin, constants_1.default.DEVICE_APP, constants_1.default.DEVICE_NAME); const otps = otp_1.default(device.secret_seed, constants_1.default.OTP_LENGTH, constants_1.default.OTP_TIME_STEP, movingFactorCorrection); const { authenticator_tokens: tokens } = yield api.syncAuthenticatorApps(deviceStatus.authy_id, device.id, otps); const { password } = yield inquirer_1.default.prompt([ { type: 'password', name: 'password', mask: '*', message: 'Master password (needed to unlock the backup)', }, ]); const decryptedTokens = decrypt_1.default(tokens, password); const { formats } = yield inquirer_1.default.prompt([ { type: 'checkbox', message: 'Export formats', name: 'formats', choices: [ { name: 'CSV', value: 'csv' }, { name: 'JSON', value: 'json' }, { name: 'QR codes', value: 'qr' }, ], validate: (value) => value.length > 0 || 'You must choose at least one export format', }, ]); const { output } = yield inquirer_1.default.prompt([ { type: 'input', message: 'Output directory path', name: 'output', validate: (value) => value.length > 0 || 'You must enter output directory path', }, ]); // Check if output directory is empty or create if does not exist try { const outputFiles = yield fs_1.promises.readdir(output); if (outputFiles.length > 0) throw new Error('Output directory is not empty'); } catch (err) { if (err.code === 'ENOENT') { yield fs_1.promises.mkdir(output, { recursive: true }); } else { throw err; } } if (formats.includes('csv')) { yield utils_1.exportCSV(decryptedTokens, output); } if (formats.includes('json')) { yield utils_1.exportJSON(decryptedTokens, output); } if (formats.includes('qr')) { const { correction } = yield inquirer_1.default.prompt([ { type: 'list', message: 'QR code error correction level', name: 'correction', choices: [ { name: 'L (low, ~7%)', key: 'L' }, { name: 'M (medium, ~15%)', key: 'M' }, { name: 'Q (quartile, ~25%', key: 'Q' }, { name: 'H (high, ~30%)', key: 'H' }, ], }, ]); yield utils_1.exportQR(decryptedTokens, output, correction); } } catch (err) { console.error(err.message); } }); } run();