From 17307ff610454f24c9fd909a81a091a44c9e2ca8 Mon Sep 17 00:00:00 2001 From: Alex Zorin Date: Mon, 26 Aug 2019 09:29:37 +1000 Subject: [PATCH] Add support for "Authy Apps". For example, what Cloudflare and Humble Bundle used to use, and what Twitch.tv uses currently. The difference to the regular "authenticator tokens" seems to be that the tokens are issued on a per-device basis, which presumably makes them revocable. Since Authy is the authoritative issuer of these tokens, they are not encrypted in the API. The other difference is in the key length and the period (10 seconds rather than 30). Fixes #3. --- authy.go | 21 +++++++++++++ cmd/authy-export/authy-export.go | 26 ++++++++++++++++ objects.go | 51 ++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) 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 +}