Этот пост должен был выйти сильно раньше, но я не смог собрать 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
Всем хорошего шифрования.