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 "Authy Apps".
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alexzorin committed Aug 25, 2019
1 parent eac435b commit 17307ff
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 0 deletions.
21 changes: 21 additions & 0 deletions authy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
26 changes: 26 additions & 0 deletions cmd/authy-export/authy-export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
51 changes: 51 additions & 0 deletions objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package authy
import (
"crypto/rsa"
"crypto/x509"
"encoding/base32"
"encoding/hex"
"encoding/pem"
"errors"
Expand Down Expand Up @@ -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
}

0 comments on commit 17307ff

Please sign in to comment.