// Package vkauth verifies VK Mini App launch parameters in-process. VK signs the // launch query string with the app's protected ("secure") key; the gateway holds that // secret (GATEWAY_VK_APP_SECRET) and validates the `sign` itself rather than calling a // side-service, because the check is a pure offline HMAC with no VK API round-trip // (unlike the Telegram validator, which lives in a separate process to isolate the bot // token). See docs/ARCHITECTURE.md §12. package vkauth import ( "crypto/hmac" "crypto/sha256" "crypto/subtle" "encoding/base64" "errors" "net/url" "strings" ) // ErrInvalid is returned when launch params fail signature verification, are // malformed, carry no signed vk_* parameters, or lack the vk_user_id. var ErrInvalid = errors.New("vkauth: invalid vk launch params") // Identity is the user extracted from verified VK launch params. ExternalID is the // vk_user_id used as the identities external_id; Language is the vk_language hint that // seeds a brand-new account's preferred language. type Identity struct { ExternalID string Language string } // Verify checks the `sign` of a VK Mini App launch query string against secret and // returns the launching user's identity. Per VK's documented algorithm the signature // is HMAC-SHA256 over the vk_*-prefixed parameters — sorted by key and serialized as a // URL-encoded query string (url.Values.Encode mirrors VK's reference serialization for // the constrained launch-parameter charset) — under the app secret, then base64url // without padding; the comparison is constant-time. // // VK launch params carry no built-in expiry (unlike Telegram's auth_date), so freshness // is deliberately not enforced here: the gateway mints its own short-lived session, and // a replay only re-authenticates the same vk_user_id. func Verify(params, secret string) (Identity, error) { values, err := url.ParseQuery(params) if err != nil { return Identity{}, ErrInvalid } sign := values.Get("sign") if sign == "" { return Identity{}, ErrInvalid } // Only vk_*-prefixed parameters are signed; any other query parameter the client // appended is outside the signature and must be excluded from the check. signed := url.Values{} for k, v := range values { if strings.HasPrefix(k, "vk_") { signed[k] = v } } if len(signed) == 0 { return Identity{}, ErrInvalid } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(signed.Encode())) want := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) if subtle.ConstantTimeCompare([]byte(want), []byte(sign)) != 1 { return Identity{}, ErrInvalid } externalID := values.Get("vk_user_id") if externalID == "" { return Identity{}, ErrInvalid } return Identity{ExternalID: externalID, Language: values.Get("vk_language")}, nil }