Шифрование данных в Python без херни - (не)Уникальный опыт

Шифрование данных в Python без херни


Этот пост должен был выйти сильно раньше, но я не смог собрать Tink для Python-а и поэтому остался ждать релиза 1.4.0 где они пообещали добавить уже собранный пакет в PyPI. Наконец-то релиз настал, а с ним появилась возможность нормально покопаться в питонячьих биндингах для Tink и рассказать об этом вам.

Если вы вдруг не знаете, что такое Tink и не умеете читать по-английски вам лень идти в репозиторий, чтобы прочитать две строчки, то я вам расскажу.

Tink это кросс-платформенная библиотека, предоставляющая безопасное криптографическое API, которое просто использовать правильно и сложно использовать неправильно. (реально сложно… я пробовал)

В питонах долгое время было принято использовать PyCrypto, но крайний ее релиз датируется далеким 2013-м годом, что как бы намекает на стабильность то, что пора выбрать инструмент посвежее. И такой инструмент появился, и название ему Cryptography. Хорошая библиотека, есть почти все, что нужно для жизни. А чего нет можно дописать, это же open source 🤣. Единственная проблема с ней — она ожидает от вас наличия определенных познаний в криптографии иначе, легким движением руки, можно ослабить всю вашу криптокухню. Приведу пример:

>>> import os
>>> from cryptography.hazmat.primitives.ciphers.aead import AESGCM
>>> data = b"a secret message"
>>> aad = b"authenticated but unencrypted data"
>>> key = AESGCM.generate_key(bit_length=128)
>>> aesgcm = AESGCM(key)
>>> nonce = os.urandom(12)
>>> ct = aesgcm.encrypt(nonce, data, aad)
>>> aesgcm.decrypt(nonce, ct, aad)
b'a secret message'

Если вам в этом коде предельно понятна каждая команда и почему у нее именно такие параметры, а не другие, то можете закрывать эту статью и дальше не читать. Вы уже криптографический джедай и настолько преисполнились, что мне вас нечем удивить. Для тех, у кого вопросики все же остались и предназначена эта статья.

Приведу примеры, где тут можно ошибиться:

  • AESGCM.generate_key(bit_length=128) - выбор длины ключа, есть только 3 возможных значения, но, во-первых какое лучше, а во вторых почему именно эти три? Рискну предположить, что новичок в криптографии вообще тут может написать bit_length=129. Поймает ValueError конечно, но все же.
  • nonce = os.urandom(12) - почему urandom, а не getrandom и что такое 12?. Покопавшись в доках, вам станет понятно, что это рекомендация NIST, но станете ли вы разбираться в причинах или просто оставите все как есть? Ах, да, nonce никогда нельзя переиспользовать в рамках одного ключа.
  • Ну и мой самый любимый момент - где хранить key? Вероятно его можно засунуть в БД или положить в секретный файлик на сервере. Ведь злоумышленник никогда не получит доступ к серверу!

А ведь это весьма простой пример и в реальности ваше шифрование будет выглядеть гораздо сложнее и “размазаннее” по одному или нескольким классам. И вот тут на помощь приходит Tink, который стремится отгородить вас от этих и многих других проблем связанных с применением криптографии. Давайте сразу рассмотрим базовый пример использования:

import tink
from tink import aead

plaintext = b'your data...'
associated_data = b'context'

aead.register()

keyset_handle = tink.new_keyset_handle(aead.aead_key_templates.AES256_GCM)
aead_primitive = keyset_handle.primitive(aead.Aead)

ciphertext = aead_primitive.encrypt(plaintext, associated_data)

Я специально убрал все комментарии из этого примера, чтобы приблизить его к предыдущему. Даже не разбираясь в терминах библиотеки, можно понять что делает этот код. Единственное место, в этом коде, которое может вызывать вопросы это тип шифрования: AES256_GCM. Но с этим ничего поделать нельзя. Вам в любом случае придется озаботиться этим вопросом, зато не придется думать о других. Кроме типа шифрования (или “шаблона ключа” в терминах библиотеки) здесь нет никаких низкоуровневых нюансов. Они все скрыты внутри и благодаря этому ваш код становится проще и безопаснее.

Осталось обсудить вопрос хранения ключей. Тут есть два подхода. Правильный и как делать не надо. Авторы библиотеки (и я вместе с ними) не рекомендую вам хранить ключи где-либо на сервере или в базе данных. Даже если злые хакеры не получат туда доступ, то туда может зайти какой-нибудь админ, младший разработчик, да и вообще кто попало. А если кто попало по вашим серверам не ходит, то есть бэкапы, и эти бэкапы иногда делаете не вы, а потом эти бэкапы расползаются куда попало. И вот ваши ключи уже растиражированы по куче серверов и контролировать это становится практически невозможно. Но в качестве учебного примера я покажу как можно сохранить ключи из Tink в файл, а потом их из этого файла загрузить.

Ниже вы увидите плохие практики, никогда не делайте так в продакшене

plaintext = b'your data...'
associated_data = b'context'

with open('keyset.json', 'w') as keyset_file:
    keyset_handle = tink.new_keyset_handle(aead.aead_key_templates.AES256_GCM)
    cipher = keyset_handle.primitive(aead.Aead)

    cipher_text = cipher.encrypt(plaintext, associated_data)
    print(cipher_text)
    cleartext_keyset_handle.write(tink.JsonKeysetWriter(keyset_file), keyset_handle)

with open('keyset.json', 'r') as keyset_file:
    new_keyset_handle = cleartext_keyset_handle.read(tink.JsonKeysetReader(keyset_file.read()))
    new_ciper = new_keyset_handle.primitive(aead.Aead)

    print(new_ciper.decrypt(cipher_text, associated_data))

Сам файл с ключами может выглядеть следующим образом:

{
  "primaryKeyId": 1686482621,
  "key": [
    {
      "keyData": {
        "typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
        "value": "GiCOp9VZ95071OD7BApNg7aGHGDt6hrDTXpqTCoYuzWtqg==",
        "keyMaterialType": "SYMMETRIC"
      },
      "status": "ENABLED",
      "keyId": 1686482621,
      "outputPrefixType": "TINK"
    }
  ]
}

Второй способ заключается в хранении ключевого материала в специально отведенных для этого местах. Из коробки Tink поддерживает хранение в Google Cloud KMS и AWS KMS (подробнее тут). Для Go народные умельцы уже успели добавить поддержку HashiCorp Vault, но питон пока там хранить не умеет. Но ничего не мешает портировать эту поддержку с Go на Python конечно же. Если портировать откровенно лень, а хранить ключи в Vault-е уже хочется, то можно повторить упражнение показанное выше, но сдампить ключ не на диск, а в Vault. Конечно это менее удобно чем использовать интеграцию, но уж как есть. Если же вам повезло иметь у себя Google Cloud KMS, то работать с ним из питона можно так:

import tink
from tink.integration import gcpkms

json_encrypted_keyset = ...
reader = tink.JsonKeysetReader(json_encrypted_keyset)

# Create the aead used for encrypting the keyset
key_uri = 'gcp-kms://projects/tink-examples/locations/global/keyRings/foo/cryptoKeys/bar'
gcp_client = gcpkms.GcpKmsClient(key_uri, 'path/to/credentials...')
master_key_aead = gcp_client.get_aead(key_uri)

keyset_handle = tink.read_keyset_handle(reader, master_key_aead)

В заключении хочу сказать пару слов о генерации ключей в коде. Авторы библиотеки не рекомендуют смешивать генерацию ключей и операции шифрования вместе во избежание утечек ключевого материала и предлагают бороться с этим генерацией ключей за границами кода. Чтобы это было делать проще, они написали специальную утилиту Tinkey, которая позволяет осуществлять генерацию и прочие манипуляции с ключами прямо из командной строки. Вот как это может выглядеть:

$ tinkey create-keyset --key-template AES256_GCM --out cleartext_keyset.json
$ tinkey list-keyset --in cleartext_keyset.json                           
primary_key_id: 1760638710
key_info {
  type_url: "type.googleapis.com/google.crypto.tink.AesGcmKey"
  status: ENABLED
  key_id: 1760638710
  output_prefix_type: TINK
}

Если вам понравился Tink и захотелось его попробовать, то pip3 install tink вам в помощь. А если захотелось еще и Tinkey поставить, то для маководов это можно сделать так:

brew tap google/tink https://github.com/google/tink
brew install tinkey

Всем хорошего шифрования.


Смотрите также