diff --git a/README.md b/README.md index ee379da..3a6e9ad 100644 --- a/README.md +++ b/README.md @@ -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). @@ -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. diff --git a/cmd/authy-export/authy-export.go b/cmd/authy-export/authy-export.go index afc2cd0..4fcc001 100644 --- a/cmd/authy-export/authy-export.go +++ b/cmd/authy-export/authy-export.go @@ -4,6 +4,7 @@ import ( "bufio" "encoding/json" "errors" + "flag" "fmt" "log" "net/url" @@ -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()) } }