> For the complete documentation index, see [llms.txt](https://docs.aitu.io/aituapps/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.aitu.io/aituapps/~/changes/ONoDO1W9pWIRWk5DR1p4/aitu.apps/methods/sign-check.md).

# Проверка подписи

Результаты серверных запросов (методы [**getMe**](/aituapps/~/changes/ONoDO1W9pWIRWk5DR1p4/aitu.apps/methods/getme.md), [**getPhone**](/aituapps/~/changes/ONoDO1W9pWIRWk5DR1p4/aitu.apps/methods/getphone.md) и [**getContacts**](/aituapps/~/changes/ONoDO1W9pWIRWk5DR1p4/aitu.apps/methods/getcontacts.md)) содержат специальное поле **sign**.

С помощью этого поля серверная сторона вашего приложения может убедиться, что клиент отправил данные, действительно полученные со стороны Aitu Bridge.

В конце статьи предоставлены [референсные имплементации](/aituapps/~/changes/ONoDO1W9pWIRWk5DR1p4/aitu.apps/methods/sign-check.md#reference-implementations) для проверки подписи на некоторых серверных языках, которые можно скопировать и использовать на вашем сервере.

{% hint style="danger" %}
Если у ответа нет поля **sign**, то возможность проверить подлинность отсутствует. В таком случае относитесь к клиентским данным с опаской.
{% endhint %}

Подлинность данных гарантируется тем, что API-ключ для подписывания данных хранится только в Aitu и на стороне вашего сервера.

{% hint style="danger" %}
Не передавайте API-ключ третьим лицам!
{% endhint %}

{% hint style="warning" %}
В случае подозрения на компрометацию API-ключа, следует как можно скорее сформировать новый [в личном кабинете](https://aitu.io/dev). Предыдущий при этом деактивируется.
{% endhint %}

#### Алгоритм проверки достоверности данных <a href="#algorithm" id="algorithm"></a>

Для того, чтобы убедиться в достоверности полученных данных, необходимо следовать следующему алгоритму:

1. Получить результат ответа API-метода — некий JSON-объект.
2. Исключить из этого результата поле **sign**, после чего у него (и у всех вложенных объектов) исключить ключи с false-значениями (`0`, `null` , `false`, пустой строкой `""`), пустыми массивами `[]` или пустыми объектами `{}`. Затем преобразовать его в строку вида **`ключ1:значение1ключ2:значение2ключ3:значение3`**, где все ключи объектов **отсортированы по алфавиту**.&#x20;
3. Используя алгоритм проверки целостности [HMAC](https://ru.wikipedia.org/wiki/HMAC) с хеш-функцией **sha256** и API-ключом, получить хеш строки, сформированной на предыдущем шаге.
4. Полученный хеш закодировать в **base64**-строку (**base64url**-вариант, с заменой `+` и `/` на `-` и `_` соответственно, с сохранением возможных `=` в конце).
5. Сравнить результат предыдущего шага с полем **sign** пришедшего объекта.

{% hint style="info" %}
При формировании строки для хеширования следует исключить из нее ключ **sign**.
{% endhint %}

{% hint style="info" %}
Ключи вложенных объектов, а также объектов находящихся в списках, также должны сортироваться в алфавитном порядке и добавляться к строке последовательно согласно порядку сортировки их родительских ключей.
{% endhint %}

{% hint style="info" %}
Ключи объектов со значениями `0`,`null, false` или `""` (пустая строка), `[]` (пустой массив), `{}` (пустой объект) опускаются.
{% endhint %}

{% hint style="info" %}
В качестве хеш-функции для **HMAC** необходимо использовать **sha256.**
{% endhint %}

{% hint style="success" %}
Если значение сформированного хеша в **base64url** совпадает с полем **sign** полученного объекта, то данные достоверны.
{% endhint %}

#### Пример <a href="#example" id="example"></a>

Пусть API-ключ имеет значение `my_secret_key`. Предположим, при вызове метода **getContacts** был получен следующий JSON-объект (для демонстрации в нем намеренно перемешаны ключи и добавлены ключи с опускаемыми значениями):

```javascript
{
    "empty_string_key": "",
    "sign": "tdMk-vw3bTMPDMldnx4MgCbdJJNH2B60LizMzHv_De4=",
    "contacts": [
        {
            "last_name": "pupkin",
            "phone": "7991118837",
            "first_name": "vasya",
            "null_key_deep": null
        },
        {
            "first_name": "john",
            "last_name": "doe",
            "phone": "79992222210"
        },
        {
            "first_name": "kavychka",
            "last_name": "\"",
            "phone": "79992222211"
        }
    ],
    "zero_key": 0,
    "null_key": null,
    "false_key": false,
    "empty_array": [],
    "empty_object": {}
}
```

Строка для хэширования, сформированная по описанному выше алгоритму, должна получиться следующей:

> contacts:first\_name:vasyalast\_name:pupkinphone:7991118837first\_name:johnlast\_name:doephone:79992222210first\_name:kavychkalast\_name:"phone:79992222211

{% hint style="info" %}
Обратите внимание, что между парами нет разделителей.
{% endhint %}

Результат формирования HMAC-хеша (на базе алгоритма sha256) этой строки с ключом `my_secret_key` с кодированием в base64url должен получиться следующим:

> tdMk-vw3bTMPDMldnx4MgCbdJJNH2B60LizMzHv\_De4=

{% hint style="info" %}
Для тестирования генерации HMAC можно воспользоваться online-сервисами. Например, <https://www.devglan.com/online-tools/hmac-sha256-online> с выводом результата в base64-формате. Обратите внимание, что полученный в этом сервисе хеш нужно дополнительно привести в base64url-формат, заменив все `+` на `-`, а `/` на `_`.
{% endhint %}

Полученный хеш совпадает с полем **sign** исходного объекта, а значит данные подлинные.

### Референсные имплементации <a href="#reference-implementations" id="reference-implementations"></a>

{% tabs %}
{% tab title="Node.js (v14.15.5)" %}

```javascript
function isValidSign(fullBridgeResponse, secretKey) {
    if (
        typeof fullBridgeResponse !== 'string' && typeof fullBridgeResponse !== 'object'
        || typeof secretKey !== 'string'
    ) {
        throw new Error('Can accept string or object as first argument and string as second argument only');
    }

    const { sign: inputSign, ...bridgeResponse } = typeof fullBridgeResponse === 'object'
        ? fullBridgeResponse
        : JSON.parse(fullBridgeResponse);

    const hmac = require('crypto').createHmac('sha256', secretKey);
    const base64URLUnsafeHash = hmac.update(makeStringToHash(bridgeResponse)).digest('base64');
    const calculatedSign = base64URLUnsafeHash.replace(/\+/g, '-').replace(/\//g, '_');

    return inputSign === calculatedSign;

    function makeStringToHash(input) {
        if (Array.isArray(input)) {
            return input.map(makeStringToHash).join('');
        } else if (typeof input === 'object') {
            return Object.keys(input)
                .filter((key) => input[key] && (!Array.isArray(input[key]) || input[key].length > 0) && (typeof input[key] !== 'object' || Object.keys(input[key]).length > 0))
                .sort()
                .map((key) => `${key}:${makeStringToHash(input[key])}`)
                .join('');
        } else {
            return String(input);
        }
    }
}

const bridgeResponseJSONExample = '{"empty_string_key":"","sign":"tdMk-vw3bTMPDMldnx4MgCbdJJNH2B60LizMzHv_De4=","contacts":[{"last_name":"pupkin","phone":"7991118837","first_name":"vasya","null_key_deep":null},{"first_name":"john","last_name":"doe","phone":"79992222210"},{"first_name":"kavychka","last_name":"\\"","phone":"79992222211"}],"zero_key":0,"null_key":null}';
const secretKeyExample = 'my_secret_key';
console.log(isValidSign(bridgeResponseJSONExample, secretKeyExample)); // Should be true

```

{% endtab %}

{% tab title="Node.js (v14.15.5) + TypeScript" %}

```typescript
function isValidSign(fullBridgeResponse: Record<string, unknown> | string, secretKey: string) {
    const { sign: inputSign, ...bridgeResponse } = typeof fullBridgeResponse === 'object'
      ? fullBridgeResponse
      : JSON.parse(fullBridgeResponse);

    const hmac = require('crypto').createHmac('sha256', secretKey);
    const base64URLUnsafeHash: string = hmac.update(makeStringToHash(bridgeResponse)).digest('base64');
    const calculatedSign = base64URLUnsafeHash.replace(/\+/g, '-').replace(/\//g, '_');

    return inputSign === calculatedSign;

    function makeStringToHash(input): string {
        if (Array.isArray(input)) {
            return input.map(makeStringToHash).join('');
        } else if (typeof input === 'object') {
            return Object.keys(input)
                .filter((key) => input[key] && (!Array.isArray(input[key]) || input[key].length > 0) && (typeof input[key] !== 'object' || Object.keys(input[key]).length > 0))
                .sort()
                .map((key) => `${key}:${makeStringToHash(input[key])}`)
                .join('');
        } else {
            return String(input);
        }
    }
}

const bridgeResponseJSONExample = '{"empty_string_key":"","sign":"tdMk-vw3bTMPDMldnx4MgCbdJJNH2B60LizMzHv_De4=","contacts":[{"last_name":"pupkin","phone":"7991118837","first_name":"vasya","null_key_deep":null},{"first_name":"john","last_name":"doe","phone":"79992222210"},{"first_name":"kavychka","last_name":"\\"","phone":"79992222211"}],"zero_key":0,"null_key":null}';
const secretKeyExample = 'my_secret_key';
console.log(isValidSign(bridgeResponseJSONExample, secretKeyExample)); // Should be true

```

{% endtab %}

{% tab title="Ruby (v2.7.2)" %}

```ruby
require 'JSON'
require 'OpenSSL'
require 'base64'
 
def check_sign(data, secret_key)
  sign = data.delete('sign')
 
  string = build_string(data)
  mac = OpenSSL::HMAC.digest('SHA256', secret_key, string)
  base64_url = Base64.urlsafe_encode64 mac
 
  sign == base64_url
end
 
def build_string(obj)
  if obj.is_a? Hash
    return obj.reject { |_k, v| v == 0 || v.nil? || v == '' || v == false || (v.is_a? Hash && v.empty?) || (v.is_a? Array && v.empty?) }.sort.to_h.map do |k, v|
      k.to_s + ':' + build_string(v)
    end.join
  end
 
  if obj.is_a? Array
    return obj.map { |el| build_string(el) }.join
  end
 
  obj.to_s
end
 
json = <<JSON
{
    "empty_string_key": "",
    "sign": "tdMk-vw3bTMPDMldnx4MgCbdJJNH2B60LizMzHv_De4=",
    "contacts": [
        {
            "last_name": "pupkin",
            "phone": "7991118837",
            "first_name": "vasya",
            "null_key_deep": null
        },
        {
            "first_name": "john",
            "last_name": "doe",
            "phone": "79992222210"
        },
        {
            "first_name": "kavychka",
            "last_name": "\\\"",
            "phone": "79992222211"
        }
    ],
    "zero_key": 0,
    "null_key": null
}
JSON
data = JSON.parse(json)
p check_sign(data, 'my_secret_key')
```

{% endtab %}

{% tab title="Python 3" %}

```python
import base64
import hashlib
import hmac


def get_sign_data_from_dict(obj: dict) -> str:
    result = ''
    keys = sorted(obj.keys())
    for key in keys:
        val = obj[key]

        if not val:
            continue

        if isinstance(val, dict):
            result += key + ':' + get_sign_data_from_dict(val)
            continue

        if isinstance(val, list):
            result += key + ':' + get_sign_data_from_list(val)
            continue

        result += key + ':' + str(val)

    return result


def get_sign_data_from_list(data: list) -> str:
    result = ''
    for d in data:
        result += get_sign_data_from_dict(d)
    return result


data = {
    "contacts": [
        {"first_name": "FirstName", "last_name": "LastName", "phone": "PhoneNumber"},
        {"first_name": "OnlyFirstName", "last_name": "", "phone": ""},
        {"first_name": "", "last_name": "OnlyLastName", "phone": ""},
        {"first_name": "", "last_name": "", "phone": "OnlyPhoneNumber"}
    ]
}

message = get_sign_data_from_dict(data)
assert message == 'contacts:first_name:FirstNamelast_name:LastNamephone:PhoneNumberfirst_name:OnlyFirstNamelast_name:OnlyLastNamephone:OnlyPhoneNumber'

message = bytes(message, 'utf-8')
secret = bytes('secret', 'utf-8')
signature = base64.urlsafe_b64encode(hmac.new(secret, message, digestmod=hashlib.sha256).digest())
assert signature.decode('utf8') == 'LNfD638IVfC5x-XVhKXWFE7ztRRATDbLgqNgiOvefuo='

data = {"contacts": []}

message = get_sign_data_from_dict(data)
assert message == ''

message = bytes(message, 'utf-8')
secret = bytes('secret', 'utf-8')
signature = base64.urlsafe_b64encode(hmac.new(secret, message, digestmod=hashlib.sha256).digest())
assert signature.decode('utf8') == '-eZuF5tnR65UEI-C-K3os8Jddv0wr95sOVgixTAZYWk='


```

{% endtab %}

{% tab title="Java" %}

<pre class="language-java"><code class="lang-java">
<strong>import com.fasterxml.jackson.databind.ObjectMapper;
</strong>
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class SignValidator {
    private final StringBuilder result = new StringBuilder();
    private String calculatedSign;
    private String sign;

    public SignValidator(Object object, String secret) {
        try {
            ObjectMapper om = new ObjectMapper();
            String jsonString = om.writeValueAsString(object);
            Map&#x3C;String, Object> map = om.readValue(jsonString, Map.class);
            calcSign(map, secret);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    public SignValidator(Map&#x3C;String, Object> jsonMap, String secret) {
        calcSign(jsonMap, secret);
    }

    private void calcSign(Map&#x3C;String, Object> jsonMap, String secret) {
        fillSignStringForMap(jsonMap);

        byte[] signature = calcHmacSha256(secret.getBytes(StandardCharsets.UTF_8),
                result.toString().getBytes(StandardCharsets.UTF_8));

        calculatedSign = Base64.getUrlEncoder().encodeToString(signature);
        sign = getExpectedSign(jsonMap);
    }

    byte[] calcHmacSha256(byte[] secret, byte[] data) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(secret, "HmacSHA256");
            mac.init(secretKeySpec);
            return mac.doFinal(data);
        } catch (Exception e) {
            throw new RuntimeException("Failed to calculate hmac-sha256", e);
        }
    }

    public String getCalculatedSign() {
        return calculatedSign;
    }

    public String getSign() {
        return sign;
    }

    public boolean isValid() {
        if (sign == null)
            return false;
        return sign.equals(calculatedSign);
    }

    public String getSignString() {
        return result.toString();
    }

    private String getExpectedSign(Map&#x3C;String, Object> map) {
        Object elem = map.get("sign");
        if (elem != null) {
            return elem.toString();
        } else {
            return null;
        }
    }

    private void fillSignStringForMap(Map&#x3C;String, Object> map) {

        List&#x3C;String> sortedKeys = map.keySet().stream()
                .filter(key -> !key.equalsIgnoreCase("sign"))
                .sorted(String::compareToIgnoreCase)
                .collect(Collectors.toList());

        for (String key : sortedKeys) {
            Object value = map.get(key);

            if (isEmptyValue(value)) {
                continue;
            }

            result.append(key.toLowerCase())
                    .append(":");

            if (value instanceof Map) {
                fillSignStringForMap((Map) value);
            } else if (value instanceof Collection) {
                fillSignStringForCollection((Collection) value);
            } else if (value instanceof Array) {
                fillSignStringForArray((Array) value);
            } else {
                result.append(value);
            }
        }
    }

    private void fillSignStringForCollection(Collection collection) {
        for (Object elem : collection) {
            if (!(elem instanceof Map)) {
                throw new IllegalArgumentException("Collection must contain only objects.");
            }
            fillSignStringForMap((Map) elem);
        }
    }

    private void fillSignStringForArray(Array array) {
        int length = Array.getLength(array);

        for (int i = 0; i &#x3C;= length - 1; i++) {
            Object elem = Array.get(array, i);
            if (!(elem instanceof Map)) {
                throw new IllegalArgumentException("Collection must contain only objects.");
            }
            fillSignStringForMap((Map) elem);
        }
    }

    private boolean isEmptyValue(Object value) {
        if (value == null) {
            return true;
        }

        if (value instanceof Collection) {
            return ((Collection) value).isEmpty();
        } else if (value instanceof Map) {
            return ((Map) value).isEmpty();
        } else if (value instanceof Array) {
            return Array.getLength(value) == 0;
        } else if (value instanceof String) {
            return ((String) value).isEmpty();
        } else if (value instanceof Boolean) {
            return !(Boolean) value;
        } else if (value instanceof Number) {
            return isZero((Number) value);
        } else {
            throw new IllegalArgumentException("Unsupported value type");
        }
    }

    private boolean isZero(Number number) {
        if (number instanceof Byte) {
            return number.byteValue() == 0;
        } else if (number instanceof Short) {
            return number.shortValue() == 0;
        } else if (number instanceof Integer) {
            return number.intValue() == 0;
        } else if (number instanceof Long) {
            return number.longValue() == 0;
        } else if (number instanceof Float) {
            return number.floatValue() == 0;
        } else if (number instanceof Double) {
            return number.doubleValue() == 0;
        } else if (number instanceof BigInteger) {
            return number.equals(BigInteger.ZERO);
        } else if (number instanceof BigDecimal) {
            return number.equals(BigDecimal.ZERO);
        } else {
            throw new IllegalArgumentException("Unsupported number type");
        }
    }

    // Должны быть включены ассерты java -ea SignChecker
    public static void main(String[] args) throws IOException {
        ObjectMapper om = new ObjectMapper();

        String[] inputs = new String[]{
                "{\n" +
                        "    \"empty_string_key\": \"\",\n" +
                        "    \"sign\": \"NAZEing3oTCZX8UFFjy_noJAWKUSpv2SYxPYjdGsp50=\",\n" +
                        "    \"contacts\": [\n" +
                        "        {\n" +
                        "            \"last_name\": \"pupkin\",\n" +
                        "            \"phone\": \"7991118837\",\n" +
                        "            \"first_name\": \"vasya\",\n" +
                        "            \"null_key_deep\": null\n" +
                        "        },\n" +
                        "        {\n" +
                        "            \"first_name\": \"john\",\n" +
                        "            \"last_name\": \"doe\",\n" +
                        "            \"phone\": \"79992222210\"\n" +
                        "        },\n" +
                        "        {\n" +
                        "            \"first_name\": \"kavychka\",\n" +
                        "            \"last_name\": \"\\\"\",\n" +
                        "            \"phone\": \"79992222211\"\n" +
                        "        }\n" +
                        "    ],\n" +
                        "    \"zero_key\": 0,\n" +
                        "    \"null_key\": null\n" +
                        "}",

                "{\n" +
                        "    \"sign\": \"LNfD638IVfC5x-XVhKXWFE7ztRRATDbLgqNgiOvefuo=\",\n" +
                        "    \"contacts\": [\n" +
                        "        {\"first_name\": \"FirstName\", \"last_name\": \"LastName\", \"phone\": \"PhoneNumber\"},\n" +
                        "        {\"first_name\": \"OnlyFirstName\", \"last_name\": \"\", \"phone\": \"\"},\n" +
                        "        {\"first_name\": \"\", \"last_name\": \"OnlyLastName\", \"phone\": \"\"},\n" +
                        "        {\"first_name\": \"\", \"last_name\": \"\", \"phone\": \"OnlyPhoneNumber\"}\n" +
                        "    ]\n" +
                        "}",
                "{" +
                        "    \"sign\": \"-eZuF5tnR65UEI-C-K3os8Jddv0wr95sOVgixTAZYWk=\",\n" +
                        "    \"contacts\": []" +
                        "}"};

        String[] resultString = new String[]{"contacts:first_name:vasyalast_name:pupkinphone:7991118837first_name:johnlast_name:doephone:79992222210first_name:kavychkalast_name:\"phone:79992222211",
                "contacts:first_name:FirstNamelast_name:LastNamephone:PhoneNumberfirst_name:OnlyFirstNamelast_name:OnlyLastNamephone:OnlyPhoneNumber",
                ""};

        for (int i = 0; i &#x3C; 3; i++) {
            Map&#x3C;String, Object> map = om.readValue(inputs[i], Map.class);
            SignValidator validator = new SignValidator(map, "secret");
            assert validator.getSignString().equals(resultString[i]);

            assert validator.isValid();
        }
    }

}

</code></pre>

{% endtab %}

{% tab title="Kotlin" %}

{% code overflow="wrap" %}

```kotlin
import com.fasterxml.jackson.databind.ObjectMapper
import java.lang.reflect.Array
import java.math.BigDecimal
import java.math.BigInteger
import java.nio.charset.StandardCharsets
import java.util.Base64
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

class SignValidator {
    val result = StringBuilder()
    private var calculatedSign: String? = null
    private var sign: String? = null

    constructor(obj: Any, secret: String) {
        val om = ObjectMapper()
        val jsonString = om.writeValueAsString(obj)
        val map = om.readValue(jsonString, Map::class.java)
        calcSign(map as Map<String, Any?>, secret)
    }

    constructor(jsonMap: Map<String, Any?>, secret: String) {
        calcSign(jsonMap, secret)
    }

    private fun calcSign(jsonMap: Map<String, Any?>, secret: String) {
        fillSignStringForMap(jsonMap)

        val signature = calcHmacSha256(
            secret.toByteArray(StandardCharsets.UTF_8),
            result.toString().toByteArray(StandardCharsets.UTF_8)
        )

        calculatedSign = Base64.getUrlEncoder().encodeToString(signature)
        sign = getExpectedSign(jsonMap)
    }

    fun calcHmacSha256(secret: ByteArray, data: ByteArray): ByteArray {
        val mac = Mac.getInstance("HmacSHA256")
        val secretKeySpec = SecretKeySpec(secret, "HmacSHA256")
        mac.init(secretKeySpec)
        return mac.doFinal(data)
    }

    fun isValid(): Boolean {
        if (sign == "") {
            return false
        }
        return sign == calculatedSign
    }

    fun getSignString() = result.toString()

    private fun getExpectedSign(map: Map<String, Any?>): String? {
        val elem = map.get("sign")
        if (elem != null) {
            return elem.toString()
        } else {
            return null
        }
    }

    private fun fillSignStringForMap(map: Map<String, Any?>) {

        val sortedKeys = map.keys
            .filter { key -> !key.equals("sign", ignoreCase = true) }
            .sortedBy { it.lowercase() }

        for (key in sortedKeys) {
            val value = map[key]

            if (isEmptyValue(value)) {
                continue
            }

            result.append(key.lowercase())
                .append(":")

            if (value is Map<*, *>) {
                fillSignStringForMap(value as Map<String, Any?>)
            } else if (value is Collection<*>) {
                fillSignStringForCollection(value)
            } else if (value is Array) {
                fillSignStringForArray(value)
            } else {
                result.append(value)
            }
        }
    }

    private fun fillSignStringForCollection(collection: Collection<*>) {
        for (elem in collection) {
            if (elem !is Map<*, *>) {
                throw IllegalArgumentException("Collection must contain only objects.")
            }
            fillSignStringForMap(elem as Map<String, Any?>)
        }
    }

    private fun fillSignStringForArray(array: Array) {
        val length = Array.getLength(array)

        for (i in 0..length - 1) {
            val elem = Array.get(array, i)
            if (elem !is Map<*, *>) {
                throw IllegalArgumentException("Collection must contain only objects.")
            }
            fillSignStringForMap(elem as Map<String, Any?>)
        }
    }

    private fun isEmptyValue(value: Any?): Boolean {
        if (value == null) {
            return true
        }

        return if (value is Collection<*>) {
            value.isEmpty()
        } else if (value is Map<*, *>) {
            value.isEmpty()
        } else if (value is Array) {
            Array.getLength(value) == 0
        } else if (value is String) {
            value.isEmpty()
        } else if (value is Boolean) {
            !value
        } else if (value is Number) {
            isZero(value)
        } else {
            throw IllegalArgumentException("Unsupported value type")
        }
    }

    private fun isZero(number: Number): Boolean {
        if (number is Byte) {
            return number.toByte().toInt() == 0
        } else if (number is Short) {
            return number.toShort().toInt() == 0
        } else if (number is Int) {
            return number == 0
        } else if (number is Long) {
            return number == 0L
        } else if (number is Float) {
            return number == 0.0f
        } else if (number is Double) {
            return number == 0.0
        } else if (number is BigInteger) {
            return number == BigInteger.ZERO
        } else if (number is BigDecimal) {
            return number == BigDecimal.ZERO
        } else {
            throw IllegalArgumentException("Unsupported number type")
        }
    }
}

fun main(args: kotlin.Array<String>) {
    val om = ObjectMapper()

    val inputs = arrayOf(
        """
         {
                "empty_string_key": "",
                "sign": "NAZEing3oTCZX8UFFjy_noJAWKUSpv2SYxPYjdGsp50=",
                "contacts": [
                    {
                        "last_name": "pupkin",
                        "phone": "7991118837",
                        "first_name": "vasya",
                        "null_key_deep": null
                    },
                    {
                        "first_name": "john",
                        "last_name": "doe",
                        "phone": "79992222210"
                    },
                    {
                        "first_name": "kavychka",
                        "last_name": "\"",
                        "phone": "79992222211"
                    }
                ],
                "zero_key": 0,
                "null_key": null
            }
            """,

        """
            {
                "sign": "LNfD638IVfC5x-XVhKXWFE7ztRRATDbLgqNgiOvefuo=",
                "contacts": [
                    {"first_name": "FirstName", "last_name": "LastName", "phone": "PhoneNumber"},
                    {"first_name": "OnlyFirstName", "last_name": "", "phone": ""},
                    {"first_name": "", "last_name": "OnlyLastName", "phone": ""},
                    {"first_name": "", "last_name": "", "phone": "OnlyPhoneNumber"}
                ]
            }
            """,
        """
            { 
                "sign": "-eZuF5tnR65UEI-C-K3os8Jddv0wr95sOVgixTAZYWk=",
                "contacts": []
            }
            """
    )

    val resultString = arrayOf(
        """contacts:first_name:vasyalast_name:pupkinphone:7991118837first_name:johnlast_name:doephone:79992222210first_name:kavychkalast_name:"phone:79992222211""",
        "contacts:first_name:FirstNamelast_name:LastNamephone:PhoneNumberfirst_name:OnlyFirstNamelast_name:OnlyLastNamephone:OnlyPhoneNumber",
        ""
    )

    for (i in 0..2) {
        val map = om.readValue(inputs[i], Map::class.java)
        val validator = SignValidator(map, "secret")
        assert(validator.getSignString() == resultString[i])
        assert(validator.isValid())
    }
}

```

{% endcode %}
{% endtab %}
{% endtabs %}

Для упрощения тестирования вашей собственной реализации подписи мы подготовили интерактивную форму на codepen.io

{% embed url="<https://codepen.io/angly-cat/pen/wvoLVYX>" %}
