Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

Commit

Permalink
Add support for saving and reloading encrypted backup file (#26)
Browse files Browse the repository at this point in the history
* Add support for saving and reloading encrypted backup file

Added two command line arguments (with default values the behavior is unchanged):
- `-save` with a filepath in order to save the encrypted backup file as json.
  In this case the decryption and console display of the
  tokens are disabled so that backup password is not required.
- `-load` in order to load an encrypted backup file instead of
  fetching it from the server

* Apply suggestions from code review

- Don't add a new struct in the interface just for local usage, use unnamed struct instead
- Check error code
- Apply gofmt

Co-authored-by: alexzorin <alex@zorin.au>

Co-authored-by: alexzorin <alex@zorin.au>
  • Loading branch information
cyril42e and alexzorin authored Jan 17, 2023
1 parent ac5e26a commit c4f25fd
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 72 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Please be careful. You can get your Authy account suspended very easily by using
### authy-export
This program will enrol itself as an additional device on your Authy account and export all of your TOTP tokens in [Key URI Format](https://github.com/google/google-authenticator/wiki/Key-Uri-Format).

It is also able to save the TOTP database in a JSON file encrypted with your Authy backup password, which can be used for backup purposes, and to read it back in order to decrypt it.

**Installation**

Pre-built binaries are available from the [releases page](https://github.com/alexzorin/authy/releases). (Windows binaries have been removed because of continual false positive virus complaints, sorry).
Expand All @@ -34,6 +36,7 @@ go install github.com/alexzorin/authy/...@latest
4. If the device registration is successful, the program will save its authentication credential (a random value) to `$HOME/authy-go.json` for further uses. **Make sure to delete this file and de-register the device after you're finished.**
5. If the program is able to fetch your TOTP encrypted database, it will prompt you for your Authy backup password. This is required to decrypt the TOTP secrets for the next step.
6. The program will dump all of your TOTP tokens in URI format, which you can use to import to other applications.
7. Alternatively, you can save the TOTP encrypted database to a file with the `--save` option, and reload it later with the `--load` option in order to decrypt it and dump the tokens.

If you [notice any missing TOTP tokens](https://github.com/alexzorin/authy/issues/1#issuecomment-516187701), please try toggling "Authenticator Backups" in your Authy settings, to force your backup to be resynchronized.

Expand Down
184 changes: 112 additions & 72 deletions cmd/authy-export/authy-export.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"net/url"
Expand All @@ -27,93 +28,132 @@ type deviceRegistration struct {
}

func main() {
// If we don't already have a registered device, prompt the user for one
regr, err := loadExistingDeviceRegistration()
if err == nil {
log.Println("Found existing device registration")
} else if os.IsNotExist(err) {
log.Println("No existing device registration found, will perform registration now")
regr, err = newInteractiveDeviceRegistration()
savePtr := flag.String("save", "", "Save encrypted tokens to this JSON file")
loadPtr := flag.String("load", "", "Load tokens from this JSON file instead of the server")
flag.Parse()

var resp struct {
Tokens authy.AuthenticatorTokensResponse `json:"tokens"`
Apps authy.AuthenticatorAppsResponse `json:"apps"`
}
if *loadPtr != "" {
// Get tokens from the json file
f, err := os.Open(*loadPtr)
if err != nil {
log.Fatalf("Device registration failed: %v", err)
log.Fatalf("Failed to read the file: %v", err)
}
} else if err != nil {
log.Fatalf("Could not check for existing device registration: %v", err)
}
defer f.Close()

// By now we have a valid user and device ID
log.Printf("Authy User ID %d, Device ID %d", regr.UserID, regr.DeviceID)
err = json.NewDecoder(f).Decode(&resp)
if err != nil {
log.Fatalf("Failed to decode the file: %v", err)
}
} else {
// Get tokens from the server
// If we don't already have a registered device, prompt the user for one
regr, err := loadExistingDeviceRegistration()
if err == nil {
log.Println("Found existing device registration")
} else if os.IsNotExist(err) {
log.Println("No existing device registration found, will perform registration now")
regr, err = newInteractiveDeviceRegistration()
if err != nil {
log.Fatalf("Device registration failed: %v", err)
}
} else if err != nil {
log.Fatalf("Could not check for existing device registration: %v", err)
}

cl, err := authy.NewClient()
if err != nil {
log.Fatalf("Couldn't create API client: %v", err)
}
// By now we have a valid user and device ID
log.Printf("Authy User ID %d, Device ID %d", regr.UserID, regr.DeviceID)

// Fetch the apps
appsResponse, err := cl.QueryAuthenticatorApps(nil, regr.UserID, regr.DeviceID, regr.Seed)
if err != nil {
log.Fatalf("Could not fetch authenticator apps: %v", err)
}
if !appsResponse.Success {
log.Fatalf("Failed to fetch authenticator apps: %+v", appsResponse)
}
cl, err := authy.NewClient()
if err != nil {
log.Fatalf("Couldn't create API client: %v", err)
}

// Fetch the actual tokens now
tokensResponse, err := cl.QueryAuthenticatorTokens(nil, regr.UserID, regr.DeviceID, regr.Seed)
if err != nil {
log.Fatalf("Could not fetch authenticator tokens: %v", err)
}
if !tokensResponse.Success {
log.Fatalf("Failed to fetch authenticator tokens: %+v", tokensResponse)
}
// Fetch the apps
resp.Apps, err = cl.QueryAuthenticatorApps(nil, regr.UserID, regr.DeviceID, regr.Seed)
if err != nil {
log.Fatalf("Could not fetch authenticator apps: %v", err)
}
if !resp.Apps.Success {
log.Fatalf("Failed to fetch authenticator apps: %+v", resp.Apps)
}

// We'll need the prompt the user to give the decryption password
pp := []byte(os.Getenv("AUTHY_EXPORT_PASSWORD"))
if len(pp) == 0 {
log.Printf("Please provide your Authy TOTP backup password: ")
pp, err = terminal.ReadPassword(int(os.Stdin.Fd()))
// Fetch the actual tokens now
resp.Tokens, err = cl.QueryAuthenticatorTokens(nil, regr.UserID, regr.DeviceID, regr.Seed)
if err != nil {
log.Fatalf("Failed to read the password: %v", err)
log.Fatalf("Could not fetch authenticator tokens: %v", err)
}
if !resp.Tokens.Success {
log.Fatalf("Failed to fetch authenticator tokens: %+v", resp.Tokens)
}
}

// Print out in https://github.com/google/google-authenticator/wiki/Key-Uri-Format format
log.Println("Here are your authenticator tokens:\n")
for _, tok := range tokensResponse.AuthenticatorTokens {
decrypted, err := tok.Decrypt(string(pp))
if *savePtr != "" {
// Save encrypted tokens to json file
f, err := os.OpenFile(*savePtr, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
log.Printf("Failed to decrypt token %s: %v", tok.Description(), err)
continue
log.Fatalf("Creating backup file failed: %v", err)
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", "\t")
if err := enc.Encode(resp); err != nil {
log.Fatalf("Encoding backup file failed: %v", err)
}
} else {
// Display decrypted tokens to the terminal
// We'll need the prompt the user to give the decryption password
pp := []byte(os.Getenv("AUTHY_EXPORT_PASSWORD"))
if len(pp) == 0 {
log.Printf("Please provide your Authy TOTP backup password: ")
var err error
pp, err = terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
log.Fatalf("Failed to read the password: %v", err)
}
}

// Print out in https://github.com/google/google-authenticator/wiki/Key-Uri-Format format
log.Println("Here are your authenticator tokens:\n")
for _, tok := range resp.Tokens.AuthenticatorTokens {
decrypted, err := tok.Decrypt(string(pp))
if err != nil {
log.Printf("Failed to decrypt token %s: %v", tok.Description(), err)
continue
}

params := url.Values{}
params.Set("secret", decrypted)
params.Set("digits", strconv.Itoa(tok.Digits))
u := url.URL{
Scheme: "otpauth",
Host: "totp",
Path: tok.Description(),
RawQuery: params.Encode(),
params := url.Values{}
params.Set("secret", decrypted)
params.Set("digits", strconv.Itoa(tok.Digits))
u := url.URL{
Scheme: "otpauth",
Host: "totp",
Path: tok.Description(),
RawQuery: params.Encode(),
}
fmt.Println(u.String())
}
for _, app := range resp.Apps.AuthenticatorApps {
tok, err := app.Token()
if err != nil {
log.Printf("Failed to decode app %s: %v", app.Name, err)
continue
}
params := url.Values{}
params.Set("secret", tok)
params.Set("digits", strconv.Itoa(app.Digits))
params.Set("period", "10")
u := url.URL{
Scheme: "otpauth",
Host: "totp",
Path: app.Name,
RawQuery: params.Encode(),
}
fmt.Println(u.String())
}
fmt.Println(u.String())
}
for _, app := range appsResponse.AuthenticatorApps {
tok, err := app.Token()
if err != nil {
log.Printf("Failed to decode app %s: %v", app.Name, err)
continue
}
params := url.Values{}
params.Set("secret", tok)
params.Set("digits", strconv.Itoa(app.Digits))
params.Set("period", "10")
u := url.URL{
Scheme: "otpauth",
Host: "totp",
Path: app.Name,
RawQuery: params.Encode(),
}
fmt.Println(u.String())
}
}

Expand Down

0 comments on commit c4f25fd

Please sign in to comment.