3e0763463f
Add the vkpay package: verify a VK Mini Apps payment ("голоса") callback
signature — MD5 (mandated by VK's payment protocol) of the sig-excluded
parameters, sorted by name and concatenated key=value, with the app secret
appended; case-insensitive over the hex digest. Unit-tested (valid / tampered /
wrong-secret / missing). First piece of the VK rail; the two-phase callback
handler, the VK order branch and the client bridge follow.
75 lines
1.7 KiB
Go
75 lines
1.7 KiB
Go
package vkpay
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"maps"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// vkSig computes a valid VK signature the way VK does, for the fixtures (sig excluded, sorted
|
|
// key=value concatenation, secret appended, MD5 hex).
|
|
func vkSig(params map[string]string, secret string) string {
|
|
keys := make([]string, 0, len(params))
|
|
for k := range params {
|
|
if k == "sig" {
|
|
continue
|
|
}
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
var b strings.Builder
|
|
for _, k := range keys {
|
|
b.WriteString(k + "=" + params[k])
|
|
}
|
|
b.WriteString(secret)
|
|
sum := md5.Sum([]byte(b.String()))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func clone(m map[string]string) map[string]string {
|
|
out := make(map[string]string, len(m))
|
|
maps.Copy(out, m)
|
|
return out
|
|
}
|
|
|
|
func TestVerify(t *testing.T) {
|
|
const secret = "app_secret"
|
|
base := map[string]string{
|
|
"notification_type": "order_status_change",
|
|
"app_id": "123",
|
|
"user_id": "456",
|
|
"receiver_id": "456",
|
|
"order_id": "789",
|
|
"date": "1700000000",
|
|
"status": "chargeable",
|
|
"item": "019f47a0-6f9e-7000-8000-000000000000",
|
|
"item_price": "10",
|
|
}
|
|
valid := clone(base)
|
|
// VK sends the digest uppercase; the verifier must accept it case-insensitively.
|
|
valid["sig"] = strings.ToUpper(vkSig(base, secret))
|
|
|
|
if !Verify(valid, secret) {
|
|
t.Fatal("valid VK signature rejected")
|
|
}
|
|
|
|
tampered := clone(valid)
|
|
tampered["item_price"] = "1"
|
|
if Verify(tampered, secret) {
|
|
t.Error("accepted a tampered amount")
|
|
}
|
|
|
|
if Verify(valid, "wrong-secret") {
|
|
t.Error("accepted under the wrong secret")
|
|
}
|
|
|
|
noSig := clone(valid)
|
|
delete(noSig, "sig")
|
|
if Verify(noSig, secret) {
|
|
t.Error("accepted a callback with no signature")
|
|
}
|
|
}
|