Files
authy/authy.go
Alex Zorin 17307ff610 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.
2019-08-26 09:29:37 +10:00

192 lines
6.4 KiB
Go

package authy
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
const (
baseURL = "https://api.authy.com/json/"
// This is copied from app.js of AuthyChrome
apiKey = "37b312a3d682b823c439522e1fd31c82"
)
// Client provides API interaction with the Authy API.
// See NewClient()
type Client struct {
httpCl http.Client
UserAgent string
APIKey string
// This doesn't seem to be a real nonce nor a signature, since
// it actually appears to be random bytes that get re-used between
// requests
nonce []byte
}
// NewClient creates a new Authy API client.
func NewClient() (Client, error) {
nonce, err := randomBytes(32)
if err != nil {
return Client{}, err
}
return Client{
httpCl: http.Client{},
UserAgent: "authy (https://github.com/alexzorin/authy)",
APIKey: apiKey,
nonce: nonce,
}, nil
}
func (c Client) doRequest(ctx context.Context, method, url string, body io.Reader, dest interface{}) error {
req, err := http.NewRequest(method, baseURL+url, body)
if err != nil {
return err
}
if ctx == nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
}
req = req.WithContext(ctx)
req.Header.Set("user-agent", c.UserAgent)
resp, err := c.httpCl.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var r io.Reader = resp.Body
if os.Getenv("AUTHY_DEBUG") == "1" {
var debugBuf bytes.Buffer
r = io.TeeReader(resp.Body, &debugBuf)
defer func() {
fmt.Fprintf(os.Stderr, "[AUTHY_DEBUG] Sent request to: %s, got response: %s\n",
req.URL.String(), debugBuf.String())
}()
}
return json.NewDecoder(r).Decode(&dest)
}
// QueryUser fetches the status of an Authy user account.
func (c Client) QueryUser(ctx context.Context, countryCallingCode int, phone string) (UserStatus, error) {
var us UserStatus
return us, c.doRequest(ctx, http.MethodGet, fmt.Sprintf("users/%d-%s/status", countryCallingCode, phone),
nil, &us)
}
// RequestDeviceRegistration begins a new device registration for an Authy User account,
// via the nominated mechanism.
func (c Client) RequestDeviceRegistration(ctx context.Context, userID uint64, via ViaMethod) (StartDeviceRegistrationResponse, error) {
form := url.Values{}
form.Set("api_key", c.APIKey)
form.Set("via", string(via))
form.Set("device_app", "authy")
form.Set("signature", hex.EncodeToString(c.nonce))
var resp StartDeviceRegistrationResponse
return resp, c.doRequest(ctx, http.MethodPost, fmt.Sprintf("users/%d/devices/registration/start", userID),
strings.NewReader(form.Encode()), &resp)
}
// CheckDeviceRegistration fetches the status of the device registration request (requestID) for the
// nominated Authy User ID (userID). This should be polled with a timeout.
func (c Client) CheckDeviceRegistration(ctx context.Context, userID uint64, requestID string) (DeviceRegistrationStatus, error) {
form := url.Values{}
form.Set("api_key", c.APIKey)
form.Set("signature", hex.EncodeToString(c.nonce))
var resp DeviceRegistrationStatus
return resp, c.doRequest(ctx, http.MethodGet,
fmt.Sprintf("users/%d/devices/registration/%s/status?%s", userID, requestID, form.Encode()), nil, &resp)
}
// CompleteDeviceRegistration completes the device registration process for the nominated Authy User ID
// (userID) and PIN (from the DeviceRegistrationStatus)
func (c Client) CompleteDeviceRegistration(ctx context.Context, userID uint64, pin string) (CompleteDeviceRegistrationResponse, error) {
form := url.Values{}
form.Set("api_key", c.APIKey)
form.Set("pin", pin)
form.Set("device_app", "authy")
var resp CompleteDeviceRegistrationResponse
return resp, c.doRequest(ctx, http.MethodPost,
fmt.Sprintf("users/%d/devices/registration/complete", userID), strings.NewReader(form.Encode()), &resp)
}
// QueryDevicePrivateKey fetches the PKCS#1 private key for the nominated device ID, using the
// known device secret TOTP seed from CompleteDeviceRegistrationResponse.
func (c Client) QueryDevicePrivateKey(ctx context.Context, deviceID uint64, deviceSeed string) (DevicePrivateKeyResponse, error) {
// We need to generate 3 OTPs using the device seed in order to get access to the device private key
codes, err := generateTOTPCodes(deviceSeed, totpDigits, totpTimeStep, false)
if err != nil {
return DevicePrivateKeyResponse{}, 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])
var resp DevicePrivateKeyResponse
return resp, c.doRequest(ctx, http.MethodGet,
fmt.Sprintf("devices/%d/rsa_key?%s", deviceID, form.Encode()), nil, &resp)
}
// QueryAuthenticatorTokens fetches the encrypted TOTP tokens for userID, authenticating
// using the deviceSeed (hex-encoded).
func (c Client) QueryAuthenticatorTokens(ctx context.Context, userID uint64, deviceID uint64, deviceSeed string) (AuthenticatorTokensResponse, error) {
codes, err := generateTOTPCodes(deviceSeed, totpDigits, totpTimeStep, false)
if err != nil {
return AuthenticatorTokensResponse{}, 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("apps", "")
var resp AuthenticatorTokensResponse
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)
}