Secrets.compare digest: примеры (PYTHON)

Использование secrets.compare_digest для сравнения данных в Python
Раздел: Криптография, Сравнение строк
secrets.compare_digest(a: str or bytes, b: str or bytes): bool

Описание функции secrets.compare_digest

Функция secrets.compare_digest(a, b) является частью стандартного модуля secrets, появившегося в Python 3.6. Её основное назначение - безопасное сравнение двух строк или байтовых последовательностей, устойчивое к атакам по времени (timing attacks).

Такой тип атак основан на анализе времени выполнения операции: обычное сравнение строк (оператор ==) завершается при первом несовпадении символов, что позволяет злоумышленнику постепенно подбирать секретные значения, измеряя время ответа. compare_digest реализует алгоритм постоянного времени, где время выполнения не зависит от совпадающих префиксов.

Функция обычно применяется для проверки криптографических хешей, токенов аутентификации, ключей API и других чувствительных данных.

Аргументы функции

  • a: первый объект для сравнения. Может быть типом bytes или str (как Unicode, так и ASCII).
  • b: второй объект для сравнения. Также может быть bytes или str.

Возвращаемое значение

Функция возвращает True, если объекты a и b равны. В противном случае возвращается False. Важно, что сравнение безопасно по времени только когда оба аргумента имеют одинаковый тип (оба bytes или оба str). При сравнении строк разной длины также обеспечивается постоянное время выполнения.

Короткие примеры использования

Базовое сравнение строк:

import secrets

result = secrets.compare_digest('secret_token', 'secret_token')
print(result)
True

Сравнение байтовых последовательностей:

import secrets

bytes_a = b'\xde\xad\xbe\xef'
bytes_b = b'\xde\xad\xbe\xef'
result = secrets.compare_digest(bytes_a, bytes_b)
print(result)
True

Сравнение различных строк:

import secrets

result = secrets.compare_digest('password', 'password1')
print(result)
False

Сравнение строк разной длины:

import secrets

result = secrets.compare_digest('short', 'very_long_string')
print(result)
False

Похожие функции в Python

hmac.compare_digest: Функция из модуля hmac, обладающая идентичной реализацией и поведением. Она появилась в Python 3.3. Для нового кода рекомендуется использовать secrets.compare_digest, так как модуль secrets явно предназначен для криптографически безопасных операций.

Оператор ==: Обычное сравнение строк или байтов. Небезопасно по времени, поэтому не должно использоваться для сравнения секретных данных, таких как токены или хеши паролей.

Аналоги функции в других языках

PHP

Функция hash_equals предназначена для безопасного сравнения строк.

$result = hash_equals('secret', 'secret');
var_dump($result);
bool(true)

JavaScript (Node.js)

Модуль crypto предоставляет метод timingSafeEqual для сравнения буферов.

const crypto = require('crypto');
const buf1 = Buffer.from('secret');
const buf2 = Buffer.from('secret');
const result = crypto.timingSafeEqual(buf1, buf2);
console.log(result);
true

Java

Класс MessageDigest имеет метод isEqual, который реализует сравнение с постоянным временем.

import java.security.MessageDigest;
boolean result = MessageDigest.isEqual("secret".getBytes(), "secret".getBytes());
System.out.println(result);
true

C#

В .NET Core 3.0 и выше доступен метод CryptographicOperations.FixedTimeEquals.

using System.Security.Cryptography;
bool result = CryptographicOperations.FixedTimeEquals(
    Encoding.UTF8.GetBytes("secret"),
    Encoding.UTF8.GetBytes("secret")
);
Console.WriteLine(result);
True

Golang

Пакет crypto/subtle содержит функцию ConstantTimeCompare.

package main
import (
    "crypto/subtle"
    "fmt"
)
func main() {
    result := subtle.ConstantTimeCompare([]byte("secret"), []byte("secret"))
    fmt.Println(result == 1)
}
true

Главное отличие Python-функции - универсальность работы как со строками, так и с байтами, в то время как во многих других языках работа ведётся только с байтовыми массивами.

Типичные ошибки при использовании

Передача аргументов разных типов вызывает исключение TypeError.

import secrets

try:
    result = secrets.compare_digest(b'bytes', 'str')
except TypeError as e:
    print(f'Ошибка: {e}')
Ошибка: a and b must be of the same type

Использование нестроковых и небайтовых объектов также приводит к ошибке.

import secrets

try:
    result = secrets.compare_digest(12345, 12345)
except TypeError as e:
    print(f'Ошибка: {e}')
Ошибка: a and b must be of the same type

Недостаточная обработка данных от пользователя перед сравнением. Функция не выполняет декодирование или преобразование, данные должны быть уже подготовлены.

Изменения в последних версиях

Начиная с Python 3.10, в реализации функции не было внесено существенных изменений, влияющих на её сигнатуру или поведение. Основные оптимизации проводились на уровне исходного кода CPython для повышения производительности. Функция остаётся стабильной и рекомендованной для использования.

Расширенные примеры использования

Сравнение HMAC-подписей для веб-запросов:

Пример python
import secrets
import hmac

def verify_signature(received_signature, payload, secret_key):
    expected_signature = hmac.new(secret_key, payload, 'sha256').hexdigest()
    return secrets.compare_digest(received_signature, expected_signature)

secret = b'my_secret_key'
payload = b'important_data'
valid_sig = hmac.new(secret, payload, 'sha256').hexdigest()
invalid_sig = 'invalid_signature'

print(verify_signature(valid_sig, payload, secret))
print(verify_signature(invalid_sig, payload, secret))
True
False

Проверка API-токена с предварительным кодированием в байты:

Пример python
import secrets
import base64

def check_api_token(header_token, stored_token):
    # Декодируем токен из base64, если необходимо
    decoded_header = base64.b64decode(header_token) if len(header_token) % 4 == 0 else header_token.encode()
    decoded_stored = stored_token.encode() if isinstance(stored_token, str) else stored_token
    return secrets.compare_digest(decoded_header, decoded_stored)

stored = 'actual_token'
header_good = 'actual_token'
header_bad = 'wrong_token'

print(check_api_token(header_good, stored))
print(check_api_token(header_bad, stored))
True
False

Использование в контексте сравнения хешей паролей (хотя для хеширования паролей лучше применять специализированные функции, такие как bcrypt или argon2):

Пример python
import secrets
import hashlib

def verify_password_hash(input_password, stored_hash, salt):
    input_hash = hashlib.pbkdf2_hmac('sha256', input_password.encode(), salt, 100000)
    return secrets.compare_digest(input_hash, stored_hash)

salt = secrets.token_bytes(16)
password = 'user_password'
stored = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)

print(verify_password_hash('user_password', stored, salt))
print(verify_password_hash('wrong_password', stored, salt))
True
False

Сравнение данных большой длины. Функция работает с постоянным временем даже на больших объёмах данных:

Пример python
import secrets
import time

large_data1 = secrets.token_bytes(1000000)
large_data2 = secrets.token_bytes(1000000)
large_data3 = large_data1  # Та же ссылка

start = time.perf_counter()
result1 = secrets.compare_digest(large_data1, large_data2)
time1 = time.perf_counter() - start

start = time.perf_counter()
result2 = secrets.compare_digest(large_data1, large_data3)
time2 = time.perf_counter() - start

print(f'Разные данные: {result1}, время: {time1:.6f} сек')
print(f'Одинаковые данные: {result2}, время: {time2:.6f} сек')
print(f'Время примерно одинаковое: {abs(time1 - time2) < 0.001}')
Разные данные: False, время: 0.002123 сек
Одинаковые данные: True, время: 0.002056 сек
Время примерно одинаковое: True

питон secrets.compare_digest function comments

En
Secrets.compare digest Compare two strings in constant time to avoid timing attacks