mirror of
https://github.com/token2/authy.git
synced 2026-03-13 11:10:55 -07:00
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.
192 lines
6.4 KiB
Go
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)
|
|
}
|