|
Professor Seleznov
|
Как организовать шифрование на уровне протокола? На самом деле тема непростая и пожалуй (имхо) это как раз та самая тема, где прийти к компромиссу почти никогда не получается. Разве что просто не передавать чувствительные данные вовсе. Я расскажу как шифрование можно организовать на уровне протокола brec и ни в коем случае не буду затрагивать те самые принципиальные решения, влияющие на безопасность (как передавать, куда передавать, отправлять ли, и хранить ли чувствительные данные вовсе). Иными словами нас интересует инструментальная сторона вопроса.-Итак в brec есть две дополнительные возможности: crypt и PayloadContext. Если crypt - это фича, которую нужно активировать, то контекст - это встроенный инструмент, который не обязательно должен быть связан с шифрованием, но без него организовать шифрование будет весьма накладно. Поэтому вначале коротко о том, что такое контекст и зачем он нужен. Итак вы объявили простой протокол
use brec::prelude::*; use serde::{Deserialize, Serialize}; use std::io::{Error, ErrorKind}; #[block] pub struct MetaBlock { pub request_id: u32, pub level: u8, } #[payload] #[derive(Serialize, Deserialize)] pub struct GreetingPayload { pub message: String, }
Давайте объявим примитивный контекст
#[payload(ctx)] pub struct CounterContext { pub encoded: u32, pub decoded: u32, } impl CounterContext { fn extract<'a>(ctx: &'a mut crate::PayloadContext<'_>) -> std::io::Result<&'a mut Self> { match ctx { crate::PayloadContext::CounterContext(ctx) => Ok(ctx), crate::PayloadContext::None => Err(Error::new( ErrorKind::InvalidInput, "GreetingPayload expects PayloadContext::CounterContext", )), } } fn inc_encoded<'a>(ctx: &'a mut crate::PayloadContext<'_>) -> std::io::Result<()> { Self::extract(ctx)?.encoded += 1; Ok(()) } fn inc_decoded<'a>(ctx: &'a mut crate::PayloadContext<'_>) -> std::io::Result<()> { Self::extract(ctx)?.decoded += 1; Ok(()) } }
Объявляем контекст мы с помощью макроса payload(ctx), который говорит brec - сделай из этой структуры контекст для всех моих Payload. Да, тут стоит упомянуть что контекст это про Payload, но не про Blocks. Вот этот взявшийся ниоткуда "тип" crate::PayloadContext будет сгенерирован brec и включать наш контекст как crate::PayloadContext::CounterContext(ctx), то есть вы можете иметь несколько разных контекстов. Теперь стоит обратить внимание на то как мы объявили чуть выше GreetingPayload. Если вы посмотрите на эту статью, то увидите разницу: вместо payload(bincode), мы указали лишь payload. Тем самым мы сказали brec - не применяй никаких встроенных кодеков (bincode - как раз ссылка на кодек), мы имплементируем его самостоятельно. Ниже я всё равно использую bincode внутри ручной реализации, но для brec это уже не встроенный кодек, а просто наш собственный код. Это не сложно.
impl GreetingPayload { fn to_bytes(&self) -> std::io::Result<Vec<u8>> { bincode::serde::encode_to_vec(self, bincode::config::standard()) .map_err(|err| Error::new(ErrorKind::InvalidData, err)) } fn from_bytes(buf: &[u8]) -> std::io::Result<Self> { let (payload, _): (Self, usize) = bincode::serde::decode_from_slice(buf, bincode::config::standard()) .map_err(|err| Error::new(ErrorKind::InvalidData, err))?; Ok(payload) } } impl PayloadEncode for GreetingPayload { fn encode(&self, ctx: &mut Self::Context<'_>) -> std::io::Result<Vec<u8>> { // Увеличим счётчик CounterContext::inc_encoded(ctx)?; // Кодируем наш Payload self.to_bytes() } } impl PayloadEncodeReferred for GreetingPayload { fn encode(&self, _ctx: &mut Self::Context<'_>) -> std::io::Result<Option<&[u8]>> { // Это можно имплементировать опционально, если хотим "дешёвый" доступ к нашим данным, что // увы не всегда возможно Ok(None) } } impl PayloadDecode<GreetingPayload> for GreetingPayload { fn decode(buf: &[u8], ctx: &mut Self::Context<'_>) -> std::io::Result<GreetingPayload> { // Увеличим счётчик CounterContext::inc_decoded(ctx)?; // Декодируем наш Payload Self::from_bytes(buf) } } impl PayloadSize for GreetingPayload { fn size(&self, _ctx: &mut Self::Context<'_>) -> std::io::Result<u64> { // Реализуем возможность узнать размер Payload Ok(self.to_bytes()?.len() as u64) } } impl PayloadCrc for GreetingPayload { // Это зачастую не нуждается в собственной имплементации. // Достаточно дефалтного } // Всё готово, вызываем генератор, который подвезёт // - `Block` // - `Payload` // - `Packet` // - `PayloadContext<'a>` // - `PacketBufReader` // - `Reader` / `Writer` brec::generate!();
Думаю, что из этого кода теперь ясно видно за что именно отвечает контекст. Довольно тривиальная задача - расшарить данные между итерациями кодирования/декодирования без необходимости наличия глобальных сущностей. На практике эта тривиальная задача может превратиться в огромную головную боль, brec же содержит решение уже в коробке. И тут важная деталь: контекст не делает состояние "скрытым" или "глобальным". Наоборот, он заставляет вас явно принести это состояние в точку чтения или записи. Это удобно и для тестов, и для producer/consumer кода: если payload требует дополнительных runtime-данных, это видно прямо в API, а не спрятано где-то в синглтоне. И при чём тут шифрование? А как же нам ключи хранить? Не глобально же держать в памяти. Вот здесь как раз тот самый "мостик" к фиче crypt, которая активно использует контекст. Но прежде оговоримся - шифрованием покрывается только Payload. Это осознанное решение. Block'и остаются без шифрования всегда, как своего рода индекс для эффективного и быстрого поиска данных. На первый взгляд это может казаться странным: если уж шифровать, то почему не всё сразу? Но в протоколе часто есть два разных слоя данных. Payload - это содержимое, где обычно и живёт чувствительная информация. Block'и - это метаданные, по которым удобно быстро фильтровать, маршрутизировать, искать и пропускать пакеты без полной распаковки. Если зашифровать всё целиком, то каждый такой сценарий потребует сначала расшифровать пакет, а значит потерять часть практической пользы от структуры протокола. Поэтому правило простое: чувствительное - в Payload, индексирующее и безопасное для раскрытия - в Block. Итак добавляем фичу и вновь объявляем наш протокол
[dependencies] brec = { version = "...", features = ["bincode", "crypt"] } serde = { version = "1.0", features = ["derive"] }
use brec::prelude::*; use serde::{Deserialize, Serialize}; #[block] pub struct MetaBlock { pub request_id: u32, pub level: u8, } #[payload(bincode, crypt)] #[derive(Serialize, Deserialize)] pub struct GreetingPayload { pub message: String, } brec::generate!();
Обратите внимание как теперь мы объявили GreetingPayload, а именно через payload(bincode, crypt). Во-первых, мы указали на использование встроенного кодека bincode, чтобы не писать кодек самим. Во-вторых, мы указали через crypt, что GreetingPayload требует шифрования. При этом шифрование остаётся выборочным: payload'ы без crypt могут жить в том же протоколе и не требовать криптографического контекста. Давайте посмотрим как теперь с этим работать
const EXAMPLE_PUBLIC_KEY_PEM: &str = r#"-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----"#; const EXAMPLE_PRIVATE_KEY_PEM: &str = r#"-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----"#; const KEY_ID: &[u8] = b"some_crypt_demo-key"; fn encode( message: String, request_id: u32, level: u8, ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { // Создадим пакет. Никакого шифрования. let mut packet = Packet::new( vec![Block::MetaBlock(MetaBlock { request_id, level })], Some(Payload::GreetingPayload(GreetingPayload { message, })), ); // Теперь создадим контекст, который нам стал доступен при активации фичи `crypt` let mut encrypt = EncryptOptions::from_public_key_pem(EXAMPLE_PUBLIC_KEY_PEM)? .with_key_id(KEY_ID.to_vec()); let mut encrypt_ctx = PayloadContext::Encrypt(&mut encrypt); // Записали пакет, наш GreetingPayload внутри пакета будет зашифрован let mut bytes = Vec::new(); packet.write_all(&mut bytes, &mut encrypt_ctx)?; Ok(bytes) } fn decode( bytes: Vec<u8>, ) -> Result<Packet, Box<dyn std::error::Error>> { use std::io::Cursor; // Создаём контекст для декодирования let mut decrypt = DecryptOptions::from_private_key_pem(EXAMPLE_PRIVATE_KEY_PEM)? .with_expected_key_id(KEY_ID.to_vec()); let mut decrypt_ctx = PayloadContext::Decrypt(&mut decrypt); // Читаем пакет let mut source = Cursor::new(bytes.as_slice()); let mut reader = PacketBufReader::new(&mut source); match reader.read(&mut decrypt_ctx)? { NextPacket::Found(packet) => Ok(packet), NextPacket::NotEnoughData(_) => Err("unexpected read status: NotEnoughData".into()), NextPacket::NoData => Err("unexpected read status: NoData".into()), NextPacket::NotFound => Err("unexpected read status: NotFound".into()), NextPacket::Skipped => Err("unexpected read status: Skipped".into()), } }
Конечно для компактности примера наши ключи лежат в памяти, лежат статично. Но это лишь демонстрация. На практике вы создаёте EncryptOptions / DecryptOptions, передаёте в producer / consumer и не вспоминаете. То есть ключевой материал и политика шифрования принадлежат вашему приложению, а PayloadContext просто аккуратно доносит их до payload-кодека в момент записи или чтения. И здесь вполне честно возникает вопрос: а зачем вообще активировать crypt, если можно сделать payload с полем bytes: Vec<u8>, зашифровать эти байты где-то снаружи, а потом уже передать их в brec? Это абсолютно рабочий путь. Более того, в большом количестве систем именно так и делают: протокол видит просто bytes, а криптографический слой живёт отдельно. Если у вас уже есть такой слой, он проверен, покрыт аудитом, умеет ротацию ключей, версионирование, key id, envelope-формат и ошибки, то переход на модель brec + crypt не имеет явных предпосылок. Смысл crypt не в том, что без него шифрование невозможно. Смысл в том, что brec берёт на себя повторяющуюся и неприятную протокольную часть: сериализовать payload, завернуть его в единый crypto envelope, записать туда версию, алгоритм, session id, RSA-wrapped session key, nonce, ciphertext/tag и опциональный key_id, а на чтении проверить всё это в обратную сторону. Иными словами crypt убирает из прикладного кода самодельный контейнер вокруг Vec<u8>. Вы продолжаете работать с типизированным GreetingPayload, а не с "мешком байт", который надо не забыть расшифровать, проверить, распарсить и правильно сопоставить с ключом. Да, любая встроенная криптографическая фича расширяет поверхность кода, который должен быть корректным. Это не бесплатная магия. Но альтернатива с ручным bytes тоже расширяет поле для ошибок, только уже в каждом конкретном приложении: кто-то забудет key id, кто-то не заложит версию envelope, кто-то будет неочевидно переиспользовать сессионный ключ, кто-то смешает ошибки декодирования и ошибки расшифровки. В brec этот риск концентрируется в одном небольшом и явном месте, где используются готовые примитивы (ChaCha20Poly1305 для payload body и RSA-OAEP-SHA256 для session key), есть формат envelope и единая модель ошибок. Поэтому выбор тут прагматичный: если вам нужен полный контроль или уже есть свой криптографический слой - используйте Vec<u8> и ручной путь. Если же вам нужно шифрование именно как часть brec-протокола, без размазывания boilerplate по producer/consumer коду, crypt даёт более ровный и проверяемый маршрут. Утомлять более техническими деталями и перечнем поддерживаемых методов и конфигурации для EncryptOptions / DecryptOptions я не буду, для этого есть документация, но как всегда, призываю поделиться "звездочкой " на GitHub. Для вас это просто клик, а для меня - обратная связь и мотивация не забрасывать проект, а развивать его дальше. Заранее лучи добра и света каждому, кто не пройдёт мимо. ЗЫ: каждый раз, когда прошу отметить проект звездой на GitHub, чувствую себя как onlyfuns модель ) только вот показывать кроме кода особо и нечего ) В общем поддержите, если не трудно, если трудно, то и не надо ) Спасибо.-Источник
|