Шифрование на уровне протокола

Страницы:  1

Ответить
 

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 модель ) только вот показывать кроме кода особо и нечего ) В общем поддержите, если не трудно, если трудно, то и не надо )
Спасибо.-Источник
 
Loading...
Error