authy-extractor
Version:
Extract 2FA tokens from Authy
179 lines (178 loc) • 8.35 kB
JavaScript
#!/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();