├── .eslintignore ├── bin ├── dev.cmd ├── run.cmd ├── run └── dev ├── .vscode └── settings.json ├── .prettierrc ├── src ├── index.ts └── commands │ └── export.ts ├── test ├── tsconfig.json ├── helpers │ └── init.js └── commands │ └── hello │ └── index.test.ts ├── .mocharc.json ├── .editorconfig ├── tsconfig.json ├── .github └── dependabot.yml ├── LICENSE ├── README.md ├── package.json └── .gitignore /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\dev" %* -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["Authy"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "semi": false 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Command, Flags, run } from '@oclif/core' 2 | 3 | export { run } from '@oclif/core' 4 | 5 | run(['export']) 6 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [{ "path": ".." }] 7 | } 8 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core') 4 | 5 | oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle')) 6 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": ["test/helpers/init.js", "ts-node/register"], 3 | "watch-extensions": ["ts"], 4 | "recursive": true, 5 | "reporter": "spec", 6 | "timeout": 60000 7 | } 8 | -------------------------------------------------------------------------------- /test/helpers/init.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json') 3 | process.env.NODE_ENV = 'development' 4 | 5 | global.oclif = global.oclif || {} 6 | global.oclif.columns = 80 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /test/commands/hello/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@oclif/test' 2 | 3 | describe('help', () => { 4 | test 5 | .stdout() 6 | .command(['help']) 7 | .it('runs help cmd', (ctx) => { 8 | expect(ctx.stdout).to.contain( 9 | 'utility to export TOTP secrets from a running authy desktop' 10 | ) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "declaration": true, 5 | "importHelpers": true, 6 | "module": "commonjs", 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | "strict": true, 10 | "target": "es2019", 11 | "esModuleInterop": true, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["src/**/*"] 15 | } 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | versioning-strategy: increase 5 | directory: '/' 6 | schedule: 7 | interval: 'monthly' 8 | labels: 9 | - 'dependencies' 10 | open-pull-requests-limit: 100 11 | pull-request-branch-name: 12 | separator: '-' 13 | ignore: 14 | - dependency-name: 'fs-extra' 15 | - dependency-name: '*' 16 | update-types: ['version-update:semver-major'] 17 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core') 4 | 5 | const path = require('path') 6 | const project = path.join(__dirname, '..', 'tsconfig.json') 7 | 8 | // In dev mode -> use ts-node and dev plugins 9 | process.env.NODE_ENV = 'development' 10 | 11 | require('ts-node').register({project}) 12 | 13 | // In dev mode, always show stack traces 14 | oclif.settings.debug = true; 15 | 16 | // Start the CLI 17 | oclif.run().then(oclif.flush).catch(oclif.Errors.handle) 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Authier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # authy-desktop-export 2 | 3 | authy-desktop-export utility. Can be imported into [authier](https://www.authier.pm/) password manager. 4 | 5 | 6 | 7 | - [authy-desktop-export](#authy-desktop-export) 8 | - [Prerequisites](#prerequisites) 9 | - [Install](#install) 10 | - [Usage](#usage) 11 | 12 | 13 | ## Prerequisites 14 | 15 | 1. Install [Authy desktop app](https://authy.com/download/) - The following steps will work on Linux, Mac and Windows. 16 | 2. Open Authy and log in, so you can see the codes being generated for you 17 | Note: Authy has a backup password feature that is enabled on some accounts; it's ones that have a padlock icon next to them. For those accounts, you might need to enter the backup password to be able to export them. 18 | 3. Restart Authy desktop app, but add the `--remote-debugging-port=5858` parameter to the command-line: 19 | 20 | - On Windows: right-click the Authy desktop shortcut, and in the Target field write `--remote-debugging-port=5858` at the end. Then click OK. Double-click the Authy desktop shortcut. 21 | - On Mac: from Terminal.app: `open -a "Authy Desktop" --args --remote-debugging-port=5858` 22 | - On Linux, from a terminal: `authy --remote-debugging-port=5858` 23 | 24 | ## Install 25 | 26 | ```sh-session 27 | $ npm install -g authy-desktop-export 28 | ``` 29 | 30 | ## Usage 31 | 32 | Make sure your authy desktop is running with the debugging port open and run: 33 | 34 | ``` 35 | authy-desktop-export export my-authy_export.json 36 | ``` 37 | 38 | You should see your exported json file in the folder where you ran the command. 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "authy-desktop-export", 3 | "version": "1.0.1", 4 | "description": "utility to export TOTP secrets from a running authy desktop", 5 | "author": "Authier @authierpm", 6 | "bin": { 7 | "authy-desktop-export": "./bin/run" 8 | }, 9 | "homepage": "https://github.com/authier-pm/authy-desktop-export", 10 | "license": "MIT", 11 | "main": "dist/index.js", 12 | "repository": "authier-pm/authy-desktop-export", 13 | "files": [ 14 | "/bin", 15 | "/dist", 16 | "/npm-shrinkwrap.json", 17 | "/oclif.manifest.json" 18 | ], 19 | "dependencies": { 20 | "@oclif/core": "^1.16.5", 21 | "@oclif/plugin-help": "^5.1.15", 22 | "chrome-remote-interface": "^0.31.3" 23 | }, 24 | "devDependencies": { 25 | "@oclif/test": "^2.2.3", 26 | "@types/chai": "^4.3.3", 27 | "@types/chrome-remote-interface": "^0.31.4", 28 | "@types/mocha": "^9.1.1", 29 | "@types/node": "^18.8.4", 30 | "chai": "^4.3.6", 31 | "eslint": "^8.25.0", 32 | "eslint-config-oclif": "^4.0.0", 33 | "eslint-config-oclif-typescript": "^1.0.3", 34 | "husky": "^8.0.1", 35 | "mocha": "^10.0.0", 36 | "oclif": "^3.2.16", 37 | "prettier": "^2.7.1", 38 | "pretty-quick": "^3.1.3", 39 | "shx": "^0.3.4", 40 | "ts-node": "^10.9.1", 41 | "tslib": "^2.4.0", 42 | "typescript": "^4.8.4" 43 | }, 44 | "oclif": { 45 | "bin": "authy-desktop-export", 46 | "dirname": "authy-desktop-export", 47 | "commands": "./dist/commands", 48 | "plugins": [ 49 | "@oclif/plugin-help" 50 | ], 51 | "topicSeparator": " " 52 | }, 53 | "scripts": { 54 | "build": "shx rm -rf dist && tsc -b", 55 | "watch": "tsc -b -w", 56 | "lint": "eslint . --ext .ts --config .eslintrc", 57 | "postpack": "shx rm -f oclif.manifest.json", 58 | "prepack": "yarn build && oclif manifest && oclif readme", 59 | "test": "mocha --forbid-only \"test/**/*.test.ts\"", 60 | "version": "oclif readme && git add README.md" 61 | }, 62 | "engines": { 63 | "node": ">=12.0.0" 64 | }, 65 | "bugs": "https://github.com/authier-pm/authy-desktop-export/issues", 66 | "keywords": [ 67 | "authy" 68 | ], 69 | "types": "dist/index.d.ts", 70 | "husky": { 71 | "hooks": { 72 | "pre-commit": "pretty-quick --staged" 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | *-debug.log 106 | *-error.log 107 | /.nyc_output 108 | /dist 109 | /lib 110 | /package-lock.json 111 | /tmp 112 | node_modules 113 | oclif.manifest.json 114 | 115 | au-export.json -------------------------------------------------------------------------------- /src/commands/export.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/core' 2 | import CDP from 'chrome-remote-interface' 3 | import fs from 'fs' 4 | 5 | const options = { 6 | host: '127.0.0.1', 7 | port: 5858 8 | } 9 | 10 | export default class ExportFromAuthy extends Command { 11 | static description = 12 | 'utility to export TOTP secrets from a running authy desktop' 13 | 14 | static examples = [`$ authy-desktop-export`] 15 | 16 | static args = [ 17 | { name: 'fileName', description: 'Export name', required: false } 18 | ] 19 | 20 | async run(): Promise { 21 | const { args, flags } = await this.parse(ExportFromAuthy) 22 | let client 23 | 24 | // connect to endpoint 25 | client = await CDP(options) 26 | console.log('Connected to running authy!') 27 | 28 | const res = await client.Runtime.evaluate({ 29 | // the script is inspired from https://gist.github.com/gboudreau/94bb0c11a6209c82418d01a59d958c93 30 | expression: ` 31 | // Based on https://github.com/LinusU/base32-encode/blob/master/index.js 32 | 33 | const hex_to_b32 = (hex) => { 34 | let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; 35 | let bytes = []; 36 | 37 | for (let i = 0; i < hex.length; i += 2) { 38 | bytes.push(parseInt(hex.substr(i, 2), 16)); 39 | } 40 | 41 | let bits = 0; 42 | let value = 0; 43 | let output = ""; 44 | 45 | for (let i = 0; i < bytes.length; i++) { 46 | value = (value << 8) | bytes[i]; 47 | bits += 8; 48 | 49 | while (bits >= 5) { 50 | output += alphabet[(value >>> (bits - 5)) & 31]; 51 | bits -= 5; 52 | } 53 | } 54 | 55 | if (bits > 0) { 56 | output += alphabet[(value << (5 - bits)) & 31]; 57 | } 58 | 59 | return output; 60 | }; 61 | 62 | JSON.stringify(appManager.getModel().map(function(i) { 63 | var secret = (i.markedForDeletion === false ? i.decryptedSeed : hex_to_b32(i.secretSeed)); 64 | var period = (i.digits === 7 ? 10 : 30); 65 | return {...i, secret} 66 | }))` 67 | }) 68 | 69 | if (res.result.subtype === 'error') { 70 | console.error(res.result.description) 71 | throw new Error('Error in authy desktop script injection') 72 | } 73 | const fileName = args.fileName || 'au-export.json' 74 | await fs.promises.writeFile(fileName, res.result.value) 75 | 76 | console.log(`✅ Exported to ${fileName}!`) 77 | await client.close() 78 | } 79 | } 80 | --------------------------------------------------------------------------------