> For the complete documentation index, see [llms.txt](https://apidoc.tokenpay.me/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://apidoc.tokenpay.me/ms-my/penerangan/penjanaan-tandatangan.md).

# Penjanaan tandatangan

{% hint style="info" %}
PETUA (TIP)

Pedagang boleh mengikuti langkah-langkah di bawah untuk menjana tandatangan permintaan, dan platform akan mengesahkan tandatangan selepas menerima permintaan. Jika pengesahan tandatangan gagal, permintaan akan ditolak dan [kod status](/ms-my/kod-ralat/kod-status.md) (status code) yang sesuai akan dikembalikan.
{% endhint %}

### Penyediaan (Preparation)

Pedagang perlu mendaftar dengan nombor pedagang dan mencipta aplikasi pembayaran melalui pejabat belakang pedagang untuk mendapatkan `APP_ID` dan `APP_SECRECT`.

### Syarat (Condition)

Semua antara muka kaedah `POST` dalam dokumen ini perlu mengesahkan tandatangan, dan antara muka lain tidak perlu disahkan buat masa ini.

### Penjanaan (Generation)

Rentetan tandatangan mempunyai empat baris, satu parameter untuk setiap baris. Baris berakhir dengan `\n` (pemisah baris, nilai pengekodan ASCII ialah 0x0A), dan jangan tambah `\n` pada baris terakhir. Jika parameter itu sendiri berakhir dengan `\n`, anda juga perlu menambah `\n`.

```
URL\n
Request timestamp\n
Request random string\n
Request message body
```

Mari kita ambil [pertanyaan pesanan](/ms-my/pesanan/pertanyaan-pesanan.md) (order inquiry) sebagai contoh.

Langkah pertama ialah mendapatkan URL mutlak permintaan dan membuang bahagian nama domain untuk mendapatkan URL tandatangan yang mengambil bahagian. Jika terdapat parameter pertanyaan dalam permintaan, URL harus dilampirkan dengan '? ' dan rentetan pertanyaan yang sepadan.

```
/v1/transaction/query
```

Langkah kedua ialah mendapatkan cap masa (milisaat) semasa sistem apabila permintaan dimulakan. Iaitu, jumlah saat dari 00:00:00 GMT pada 1 Januari 1970 hingga sekarang, sebagai cap masa permintaan. Platform akan menolak untuk memproses permintaan yang dibuat lama dahulu, sila pastikan masa sistem pedagang itu sendiri tepat.

```
1554208460
```

Langkah ketiga ialah menjana `rentetan rawak (random string)` 32-bit.

```
593BEC0C930BF1AFEB40B4A08C8FB242
```

Langkah keempat ialah mendapatkan isi mesej permintaan dalam permintaan (request body).

```json
{
    "app_id": "8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd",
    "mch_id": "12345678",
    "transaction_id": "e98b30294xxxxxxxxxxxx97a9d9e09ce",
    "out_trade_no": "fb72xxxx-xxxx-xxxx-xxxx-xxxx8a7b52cb"
}
```

Langkah kelima ialah membina rentetan tandatangan permintaan mengikut peraturan sebelumnya seperti berikut:

```html
/v1/transaction/query\n
1554208460\n
593BEC0C930BF1AFEB40B4A08C8FB242\n
{"app_id":"8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd","mch_id":"12345678","transaction_id":"e98b30294xxxxxxxxxxxx97a9d9e09ce","out_trade_no":"fb72xxxx-xxxx-xxxx-xxxx-xxxx8a7b52cb"}
```

### Penyulitan (Encryption)

Ambil proses penyulitan javascript sebagai contoh:

```json
let cryptoJs = require("crypto-js");
let key = CryptoJS.enc.Utf8.parse("9db664697xxxxxxxxxxxx2d27a3c925c") //the secret key of APP
//AES encryption
let ciphertext = cryptoJs.AES.encrypt(Signature string, key, {
  mode: CryptoJS.mode.ECB,
  padding: CryptoJS.pad.Pkcs7,
}).toString();
```

Contoh selepas penyulitan:

```
QCwHvoBM9TJ2wokF8hhaoS34P0nkJpYMisBUizpOj5q/77I6+KFPVvFUCaaUiu+KFctisJFU1DfJdCHrLpJIx9CirX5ku3L9TMGihFcEG8MGoh2dwDvunH8JgJOVV9ClSkpXqjad4flSuYMoxPOZqPHr+ktOLZ3pPzs12BMqmbZVNIe+oOezTZsQ8xxxxRgOJzwU/AbouZSl2xto7DcYCjvNSnw7BkuzBFgTfxVXB3+R7e+1SpdeJajuCKGKvYMVTe7slS5j/4LQ4vcr1QqOPhpoemsOV92tPhgQ0iGw3GKpLIEOoDAwy2+ojzP5XERh
```

### Menggabungkan (Concatenate)

Format sambungan maklumat tandatangan adalah seperti berikut:\
app\_id=`APP_ID`, mch\_id=`ID Pedagang`, nonce\_str=`Rentetan rawak yang dijana dalam langkah 3`, timestamp=`Cap masa yang dijana dalam langkah 2`, signature=`Rentetan tandatangan kriptografi`.

### Pengepala HTTP (HTTP header)

API dokumen melepasi tandatangan melalui pengepala HTTP `Authorization`. `Authorization` terdiri daripada dua bahagian: `jenis pengesahan` dan `maklumat tandatangan`. `Jenis pengesahan` hanya menyokong `TTPAY-AES-256-ECB` buat masa ini.

```
Authorization: authentication type  signature information
```

Pengepala kebenaran seperti berikut: (Nota: Contoh kerana penetapan jenis mungkin mengandungi pemisah baris, data sebenar hendaklah berada pada satu baris)

```
Authorization: TTPAY-AES-256-ECB app_id=8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd,mch_id=12345678,nonce_str=593BEC0C930BF1AFEB40B4A08C8FB242,timestamp=1554208460,signature=QCwHvoBM9TJ2wokF8hhaoS34P0nkJpYMisBUizpOj5q/77I6+KFPVvFUCaaUiu+KFctisJFU1DfJdCHrLpJIx9CirX5ku3L9TMGihFcEG8MGoh2dwDvunH8JgJOVV9ClSkpXqjad4flSuYMoxPOZqPHr+ktOLZ3pPzs12BMqmbZVNIe+oOezTZsQ8xxxxRgOJzwU/AbouZSl2xto7DcYCjvNSnw7BkuzBFgTfxVXB3+R7e+1SpdeJajuCKGKvYMVTe7slS5j/4LQ4vcr1QqOPhpoemsOV92tPhgQ0iGw3GKpLIEOoDAwy2+ojzP5XERh"
```

Akhir sekali, kita boleh mencipta permintaan HTTP yang mengandungi tandatangan.

```sh
$ curl https://api.tokenpay.me/v1/transaction/query -H "Content-Type: application/json" -H 'Authorization: TTPAY-AES-256-ECB
 app_id=8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd,mch_id=12345678,nonce_str=593BEC0C930BF1AFEB40B4A08C8FB242,timestamp=1554208460,signature=QCwHvoBM9TJ2wokF8hhaoS34P0nkJpYMisBUizpOj5q/77I6+KFPVvFUCaaUiu+KFctisJFU1DfJdCHrLpJIx9CirX5ku3L9TMGihFcEG8MGoh2dwDvunH8JgJOVV9ClSkpXqjad4flSuYMoxPOZqPHr+ktOLZ3pPzs12BMqmbZVNIe+oOezTZsQ8xxxxRgOJzwU/AbouZSl2xto7DcYCjvNSnw7BkuzBFgTfxVXB3+R7e+1SpdeJajuCKGKvYMVTe7slS5j/4LQ4vcr1QqOPhpoemsOV92tPhgQ0iGw3GKpLIEOoDAwy2+ojzP5XERh' -X POST -d '{"out_trade_no": "fb72xxxx-xxxx-xxxx-xxxx-xxxx8a7b52cb", "transaction_id":"e98b30294xxxxxxxxxxxx97a9d9e09ce", "app_id":"8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd", "mch_id":"12345678" }'
```

### Kod demo (Demo code)

{% tabs %}
{% tab title="Java" %}

```java
package com.example.http;

import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.sun.istack.internal.NotNull;
import okhttp3.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Base64;

public class HttpApplication {
    private static String SECRET = "AES";
    private static String CIPHER_ALGORITHM = "AES/ECB/PKCS7Padding";
    private static String schema = "TTPAY-AES-256-ECB ";

    public static void main(String[] args) throws Exception {
        String key = "xxxxxxxx";    // AppSecret
        String reqURL = "/v1/transaction/query";    // Request interface

        OkHttpClient client = new OkHttpClient();
        JSONObject params = new JSONObject();

        try {
            params.put("app_id", "xxxxxxxx");           // Application ID
            params.put("mch_id", "xxxxxxxx");           // Merchat ID
            params.put("transaction_id", "xxxxxxxx");   // Platform order number
            params.put("out_trade_no", "xxxxxxxx");     // Merchat order number
        } catch (JSONException e) {
            e.printStackTrace();
        }

        Security.addProvider(new BouncyCastleProvider());

        // Signature
        String auth = auth(reqURL, params, key);

        RequestBody body = RequestBody.create(MediaType.parse("application/json;charset=utf-8"), params.toString());
        Request request = new Request.Builder()
        .header("Authorization", auth)
        .url(reqURL)
        .post(body)
        .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                System.out.println("http request error");
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                System.out.println(response.body().string());
            }
        });
    }

    /**
    * Signature
    * @param reqURL Request URL
    * @param raw Message body
    * @param key AppSecret
    * @return Signature information
    */
    public static String auth(String reqURL, JSONObject raw, String key) throws Exception {
        HttpUrl parseReqURL = HttpUrl.parse(reqURL);
        String url = parseReqURL.encodedPath();
        String appID = raw.getString("app_id");
        String mchID = raw.getString("mch_id");
        long currentTimeMillis = System.currentTimeMillis();
        String timestamp = String.valueOf(currentTimeMillis);
        String nonceStr = randomString(32);
        String message = url + "\n" + timestamp + "\n" + nonceStr + "\n" + raw.toString();
        String aes256ECBPkcs7PaddingEncrypt = aes256ECBPkcs7PaddingEncrypt(message, key);

        return schema + "app_id=" + appID + ",mch_id=" + mchID + ",nonce_str=" + nonceStr + ",timestamp=" + timestamp + ",signature=" + aes256ECBPkcs7PaddingEncrypt;
    }

    /**
    * Generate random string
    * @param len String length
    * @return Random string
    */
    public static String randomString(Integer ...len) {
        int e = len.length <= 0 ? 32 : len[0];
        String str = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
        int strLen = str.length();
        StringBuilder stringBuilder = new StringBuilder();

        for (int i = 0; i < e; i++) {
            double random = Math.random();
            int v = (int) Math.floor(random * strLen);
            char charAt = str.charAt(v);

            stringBuilder.append(charAt);
        }

        return stringBuilder.toString();
    }

    /**
    * AES encryption
    * @param str String
    * @param key Secret key
    * @return Cryptographic string
    * @throws Exception Abnormal information
    */
    public static String aes256ECBPkcs7PaddingEncrypt(String str, String key) throws Exception {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);

        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyBytes, SECRET));

        byte[] doFinal = cipher.doFinal(str.getBytes(StandardCharsets.UTF_8));

        return new String(Base64.getEncoder().encode(doFinal));
    }

    /**
    * AES encryption
    * @param str String
    * @param key Secret key
    * @return Decryption string
    * @throws Exception Abnormal information
    */
    public static String aes256ECBPkcs7PaddingDecrypt(String str, String key) throws Exception {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);

        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, SECRET));

        byte[] doFinal = cipher.doFinal(Base64.getDecoder().decode(str));

        return new String(doFinal);
    }
}
```

{% endtab %}

{% tab title="Go" %}

```go
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"time"

	"github.com/forgoer/openssl"
)

const (
	SignatureMessageFormat = "%s\n%d\n%s\n%s" // The original format of the digital signature
	HeaderAuthorizationFormat = "%s app_id=%s,mch_id=%s,nonce_str=%s,timestamp=%d,signature=%s"
)

type QueryReq struct {
	AppID         string `json:"app_id"`
	MchID         string `json:"mch_id"`
	TransactionID string `json:"transaction_id,omitempty"`
	OutTradeNo    string `json:"out_trade_no,omitempty"`
}

func main() {

	appSecret := "**********"           // AppSecret
	reqPath := "/v1/transaction/query"  // Request interface path
	queryReq := QueryReq{
		AppID:         "**********",    // Application ID
		MchID:         "**********",    // Merchat ID
		TransactionID: "**********",    // Platform order number
		OutTradeNo:    "**********",    // Merchat order number
	}

	b, _ := json.Marshal(queryReq)

    // Signature
	authorization, err := GenerateAuthorizationHeader(queryReq.AppID, queryReq.MchID, reqPath, string(b), appSecret)
	if err != nil {
		panic(err)
	}

    // Request interface
	url := "" + reqPath

	payload := bytes.NewBuffer(b)

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", authorization)

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(string(body))
}

// Signature
func GenerateAuthorizationHeader(appID, mchID, reqURL, signBody, appSecret string) (string, error) {
	nonceStr := RandomString(32)
	timestamp := time.Now().Unix()
	message := fmt.Sprintf(SignatureMessageFormat, reqURL, timestamp, nonceStr, signBody)

	signatureResult, err := AesECBEncrypt(message, appSecret)
	if err != nil {
		return "", err
	}
	authorization := fmt.Sprintf(
		HeaderAuthorizationFormat, getAuthorizationType(), appID,
		mchID, nonceStr, timestamp, signatureResult,
	)
	return authorization, nil
}

// AES encryption
func AesECBEncrypt(orig, key string) (string, error) {
	dst, _ := openssl.AesECBEncrypt([]byte(orig), []byte(key), openssl.PKCS7_PADDING)
	return base64.StdEncoding.EncodeToString(dst), nil
}

// AES encryption
func AesECBDecrypt(crypted, key string) (string, error) {
	x := len(crypted) * 3 % 4
	switch {
	case x == 2:
		crypted += "=="
	case x == 1:
		crypted += "="
	}
	crytedByte, err := base64.StdEncoding.DecodeString(crypted)
	if err != nil {
		return "", err
	}
	origData, err := openssl.AesECBDecrypt(crytedByte, []byte(key), openssl.PKCS7_PADDING)
	if err != nil {
		return "", err
	}
	return string(origData), err
}

// Generate random character
func RandomString(length int) string {
	str := []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
	var result []byte
	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
	for i := 0; i < length; i++ {
		result = append(result, str[rnd.Intn(len(str))])
	}
	return string(result)
}

func getAuthorizationType() string {
	return "TTPAY-AES-256-ECB"
}
```

{% endtab %}

{% tab title="PHP" %}

```php
<?php
$key = "xxxxxxxx";                //  AppSecret
$APP_ID = "xxxxxxxx";              // Application ID
$mchID = "xxxxxxxx";              // Merchat ID
$transactionID = "xxxxxxxx";      // Platform order number
$outTradeNo = "xxxxxxxx";         // Merchat order number
$url = "/v1/transaction/query";   // Interface path
$timestamp = time();              // Current timestamp
$nonce = generateStr(32);         // 32-bit random string

// Generate random string
function generateStr($length) {
  $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  $res ='';

  for ( $i = 0; $i < $length; $i++ ) {
    $res .= $chars[ mt_rand(0, strlen($chars) - 1) ];
  }

  return $res;
}

// Message body
$body = '{"app_id":"'.$APP_ID.'","mch_id":"'.$mchID.'","transaction_id":"'.$transactionID.'","out_trade_no":"'.$outTradeNo.'"}';

// Construct signature string
$message = $url."\n".$timestamp."\n".$nonce."\n".$body;

// Encrypt signature string
$cipher = "aes-256-ecb";
$sign = openssl_encrypt($message, $cipher, $key);

// Split signature information
$schema = 'TTPAY-AES-256-ECB';
$authorization = sprintf('%s app_id=%s,mch_id=%s,nonce_str=%s,timestamp=%d,signature=%s', $schema, $APP_ID, $mchID, $nonce, $timestamp, $sign);

// Set HTTP header
$header[] = "Accept: application/json";
$header[] = "Content-Type: application/json";
$header[] = "Authorization: $authorization";

// Request interface
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_URL, "".$url);
curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($curl);
curl_close($curl);

var_export($result);
```

<br>
{% endtab %}

{% tab title="JavaScript" %}

```javascript
let cryptoJs = require("crypto-js");

let jsonObj = JSON.parse(pm.request.body.raw); //Convert the submitted body string into an object
let jsonStr = JSON.stringify(jsonObj)//Turn the object back into a string to compress, avoiding line breaks in the body that affect signature compression.
let appID = jsonObj.app_id;
let mchID = jsonObj.mch_id;

// Generate the current timestamp
let timestamp = Math.round(new Date() / 1000).toString()

//Define random string
var nonceStr = randomString(); // Random string
function randomString(e) {
    e = e || 32;
    var t = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678",
    a = t.length,
    n = "";
    for (i = 0; i < e; i++) n += t.charAt(Math.floor(Math.random() * a));
    return n
}

//Get the full path
var reqURL = "/" + pm.request.url.path.join("/");

//Splice signature plaintext
message = reqURL + "\n" + timestamp + "\n" + nonceStr + "\n" + jsonStr

let key = CryptoJS.enc.Utf8.parse("********************************") // AppSecret
//AES encryption
let ciphertext = cryptoJs.AES.encrypt(message, key, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7,
  }).toString();

//Splice on the authorization of Header, TTPAY-{EncryMode)},currently supports only AES-256-ECB
let authorization = 'TTPAY-AES-256-ECB app_id=' + appID + ',mch_id=' + mchID + ',nonce_str=' + nonceStr + ',timestamp=' + timestamp + ',signature=' + ciphertext

pm.request.headers.upsert({ key: "authorization", value: authorization })
```

{% endtab %}
{% endtabs %}
