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

Результаты серверных запросов (методы [**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>" %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.aitu.io/aituapps/~/changes/ONoDO1W9pWIRWk5DR1p4/aitu.apps/methods/sign-check.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
