diff --git a/authy.go b/authy.go index 18513f0..364997d 100644 --- a/authy.go +++ b/authy.go @@ -168,3 +168,24 @@ func (c Client) QueryAuthenticatorTokens(ctx context.Context, userID uint64, dev return resp, c.doRequest(ctx, http.MethodGet, fmt.Sprintf("users/%d/authenticator_tokens?%s", userID, form.Encode()), nil, &resp) } + +// QueryAuthenticatorApps fetches the encrypted Authy App tokens for userID, +// authenticating using the deviceSeed (hex-encoded). +func (c Client) QueryAuthenticatorApps(ctx context.Context, userID uint64, deviceID uint64, deviceSeed string) (AuthenticatorAppsResponse, error) { + codes, err := generateTOTPCodes(deviceSeed, totpDigits, totpTimeStep, false) + if err != nil { + return AuthenticatorAppsResponse{}, fmt.Errorf("Failed to generate TOTP codes: %v", err) + } + + form := url.Values{} + form.Set("api_key", apiKey) + form.Set("device_id", strconv.FormatUint(deviceID, 10)) + form.Set("otp1", codes[0]) + form.Set("otp2", codes[1]) + form.Set("otp3", codes[2]) + form.Set("locale", "en-GB") + + var resp AuthenticatorAppsResponse + return resp, c.doRequest(ctx, http.MethodPost, + fmt.Sprintf("users/%d/devices/%d/apps/sync", userID, deviceID), strings.NewReader(form.Encode()), &resp) +} diff --git a/cmd/authy-export/authy-export.go b/cmd/authy-export/authy-export.go index 8697480..df4c504 100644 --- a/cmd/authy-export/authy-export.go +++ b/cmd/authy-export/authy-export.go @@ -49,6 +49,15 @@ func main() { log.Fatalf("Couldn't create API client: %v", err) } + // 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) + } + // Fetch the actual tokens now tokensResponse, err := cl.QueryAuthenticatorTokens(nil, regr.UserID, regr.DeviceID, regr.Seed) if err != nil { @@ -85,6 +94,23 @@ func main() { } 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) + } + 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()) + } } func newInteractiveDeviceRegistration() (deviceRegistration, error) { diff --git a/objects.go b/objects.go index eff004a..1b22f44 100644 --- a/objects.go +++ b/objects.go @@ -3,6 +3,7 @@ package authy import ( "crypto/rsa" "crypto/x509" + "encoding/base32" "encoding/hex" "encoding/pem" "errors" @@ -194,3 +195,53 @@ func (t AuthenticatorToken) Description() string { } return "Token-" + t.UniqueID } + +// AuthenticatorAppsResponse is the response from: +// https://api.authy.com/json/users/{User_ID}/devices/{Device_ID}/apps/sync +type AuthenticatorAppsResponse struct { + // Display to user + Message string `json:"message"` + + // Active encrypted authenticator apps + AuthenticatorApps []AuthenticatorApp `json:"apps"` + + // Recently deleted, but not removed encrypted authenticator apps + Deleted []AuthenticatorApp `json:"deleted"` + + // Whether this request succeeded + Success bool `json:"success"` +} + +// AuthenticatorApp is embedded in AuthenticatorAppsResponse +type AuthenticatorApp struct { + ID string `json:"_id"` + + // Display name of the token + Name string `json:"name"` + + SerialID int `json:"serial_id"` + + Version int `json:"version"` + + AssetsGroup string `json:"assets_group"` + + AuthyID uint64 `json:"authy_id"` + + // The Device Secret Seed (hex-encoded). It is the TOTP + // secret that protects the authenticated endpoints. + SecretSeed string `json:"secret_seed"` + + // How many digits in the TOTP + Digits int `json:"digits"` +} + +// Token produces the base32-encoded TOTP token backing +// this app. It has a period of 10. +func (a AuthenticatorApp) Token() (string, error) { + decoded, err := hex.DecodeString(a.SecretSeed) + if err != nil { + return "", err + } + encoder := base32.StdEncoding.WithPadding(base32.NoPadding) + return encoder.EncodeToString(decoded), nil +}