Como organizar a criptografia no nível do protocolo? Na verdade, o tema não é simples e, talvez (na minha humilde opinião), seja exatamente o tema onde chegar a um compromisso quase nunca funciona. A menos que você simplesmente não transmita dados confidenciais.
Eu vou contar como a criptografia pode ser organizada no nível do protocolo e, em nenhum caso, vou tocar nas decisões de princípio que afetam a segurança (como transmitir, para onde transmitir, enviar ou armazenar dados confidenciais). Em outras palavras, estamos interessados no lado instrumental da questão.
Então, em brec, existem duas funcionalidades adicionais: crypt e PayloadContext. Se crypt é um recurso que precisa ser ativado, então o contexto é uma ferramenta embutida que não precisa necessariamente estar relacionada à criptografia, mas sem ela, organizar a criptografia será muito caro. Portanto, primeiro, brevemente, sobre o que é contexto e por que ele é necessário.
Então você declarou um protocolo simples:
rustuse 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, }
Vamos declarar um contexto primitivo:
rust#[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(()) } }
Declaramos o contexto usando a macro payload(ctx), que diz ao brec - faça desta estrutura um contexto para todos os meus Payload. Sim, vale a pena mencionar aqui que o contexto é sobre Payload, mas não sobre Blocks.
Este "tipo" que surgiu do nada crate::PayloadContext será gerado pelo brec e incluirá nosso contexto como crate::PayloadContext::CounterContext(ctx), ou seja, você pode ter vários contextos diferentes.
Agora vale a pena prestar atenção em como declaramos um pouco acima GreetingPayload. Se você olhar para este artigo, verá a diferença: em vez de payload(bincode), especificamos apenas payload. Assim, dissemos ao brec - não aplique nenhum codec embutido (bincode é apenas uma referência ao codec), nós o implementamos por conta própria. Abaixo, ainda uso bincode dentro da implementação manual, mas para brec este já não é um codec embutido, mas apenas nosso próprio código.
Isso não é difícil.
rustimpl 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>> { // Aumentamos o contador CounterContext::inc_encoded(ctx)?; // Codificamos nosso Payload self.to_bytes() } } impl PayloadEncodeReferred for GreetingPayload { fn encode(&self, _ctx: &mut Self::Context<'_>) -> std::io::Result<Option<&[u8]>> { // Isso pode ser implementado opcionalmente, se quisermos acesso "barato" aos nossos dados, o que // infelizmente nem sempre é possível Ok(None) } } impl PayloadDecode<GreetingPayload> for GreetingPayload { fn decode(buf: &[u8], ctx: &mut Self::Context<'_>) -> std::io::Result<GreetingPayload> { // Aumentamos o contador CounterContext::inc_decoded(ctx)?; // Decodificamos nosso Payload Self::from_bytes(buf) } } impl PayloadSize for GreetingPayload { fn size(&self, _ctx: &mut Self::Context<'_>) -> std::io::Result<u64> { // Implementamos a capacidade de descobrir o tamanho do Payload Ok(self.to_bytes()?.len() as u64) } } impl PayloadCrc for GreetingPayload { // Isso geralmente não precisa de sua própria implementação. // Default é suficiente } // Tudo está pronto, chamamos o gerador, que irá entregar // - `Block` // - `Payload` // - `Packet` // - `PayloadContext<'a>` // - `PacketBufReader` // - `Reader` / `Writer` brec::generate!();
Eu acho que a partir deste código agora é claramente visível pelo que exatamente o contexto é responsável. Uma tarefa bastante trivial - compartilhar dados entre iterações de codificação/decodificação sem a necessidade de entidades globais. Na prática, esta tarefa trivial pode se transformar em uma enorme dor de cabeça, mas brec contém a solução já na caixa.
E aqui está um detalhe importante: o contexto não torna o estado "oculto" ou "global". Pelo contrário, ele força você a trazer explicitamente este estado para o ponto de leitura ou escrita. Isso é conveniente tanto para testes quanto para código produtor/consumidor: se o payload requer dados adicionais em tempo de execução, isso é visível diretamente na API, e não escondido em algum lugar em um singleton.
E o que a criptografia tem a ver com isso? E como armazenamos as chaves? Não é para manter globalmente na memória. É aqui que está a mesma "ponte" para o recurso crypt, que usa ativamente o contexto. Mas antes, vamos concordar - apenas o Payload é coberto pela criptografia. Esta é uma decisão consciente. Os Blocks permanecem sem criptografia sempre, como uma espécie de índice para uma busca de dados eficiente e rápida.
À primeira vista, isso pode parecer estranho: se você já está criptografando, por que não tudo de uma vez? Mas no protocolo, muitas vezes existem duas camadas diferentes de dados. Payload é o conteúdo, onde geralmente vivem informações confidenciais. Blocks são metadados que são convenientes para filtrar, rotear, pesquisar e ignorar pacotes rapidamente sem descompactação completa. Se você criptografar tudo de uma vez, cada um desses cenários exigirá primeiro a descriptografia do pacote, o que significa perder parte da utilidade prática da estrutura do protocolo. Portanto, a regra é simples: confidencial - no Payload, indexando e seguro para revelação - no Block.
Então, adicionamos o recurso e declaramos nosso protocolo novamente:
rust[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!();
Preste atenção em como agora declaramos GreetingPayload, ou seja, através de payload(bincode, crypt). Em primeiro lugar, indicamos o uso do codec embutido bincode, para não escrever o codec nós mesmos. Em segundo lugar, indicamos através de crypt que GreetingPayload requer criptografia. Ao mesmo tempo, a criptografia permanece seletiva: payloads sem crypt podem viver no mesmo protocolo e não exigir um contexto criptográfico.
Vamos ver como trabalhar com isso agora:
rustconst 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>> { // Criamos um pacote. Sem criptografia. let mut packet = Packet::new( vec![Block::MetaBlock(MetaBlock { request_id, level })], Some(Payload::GreetingPayload(GreetingPayload { message, })), ); // Agora criamos um contexto que se tornou disponível para nós quando o recurso `crypt` foi ativado 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); // Gravamos o pacote, nosso GreetingPayload dentro do pacote será criptografado 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; // Criamos um contexto para decodificação 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); // Lemos o pacote 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()), } }
Claro, para a compacidade do exemplo, nossas chaves estão na memória, estão estaticamente. Mas esta é apenas uma demonstração. Na prática, você cria EncryptOptions / DecryptOptions, passa para o produtor / consumidor e não se lembra. Ou seja, o material-chave e a política de criptografia pertencem ao seu aplicativo, e PayloadContext apenas os entrega cuidadosamente ao codec de payload no momento da escrita ou leitura.
E aqui a pergunta surge honestamente: por que ativar crypt se você pode fazer um payload com o campo bytes: Vec<u8>, criptografar esses bytes em algum lugar do lado de fora e, em seguida, passá-los para brec? Este é um caminho absolutamente funcional. Além disso, em um grande número de sistemas, é assim que eles fazem: o protocolo vê apenas bytes, e a camada criptográfica vive separadamente. Se você já tem essa camada, ela foi testada, coberta por auditoria, sabe como fazer a rotação de chaves, versionamento, key id, envelope-format e erros, então a transição para o modelo brec + crypt não tem pré-requisitos óbvios.
O significado de crypt não é que a criptografia seja impossível sem ele. O significado é que brec assume a parte repetitiva e desagradável do protocolo: serializar o payload, embrulhá-lo em um único envelope criptográfico, gravar a versão, algoritmo, session id, RSA-wrapped session key, nonce, ciphertext/tag e key_id opcional, e na leitura, verificar tudo isso na direção oposta. Em outras palavras, crypt remove o contêiner caseiro ao redor de Vec<u8> do código do aplicativo. Você continua trabalhando com um GreetingPayload tipado, e não com um "saco de bytes" que você não deve esquecer de descriptografar, verificar, analisar e corresponder corretamente à chave.
Sim, qualquer recurso criptográfico embutido expande a superfície do código que deve estar correto. Esta não é uma magia gratuita. Mas a alternativa com bytes manuais também expande o campo para erros, mas já em cada aplicativo específico: alguém esquecerá o key id, alguém não colocará a versão do envelope, alguém reutilizará o session key de forma não óbvia, alguém misturará erros de decodificação e erros de descriptografia. Em brec, esse risco se concentra em um lugar pequeno e explícito, onde primitivas prontas são usadas (ChaCha20Poly1305 para o corpo do payload e RSA-OAEP-SHA256 para a session key), há um formato de envelope e um modelo de erro unificado. Portanto, a escolha aqui é pragmática: se você precisa de controle total ou já tem sua própria camada criptográfica - use Vec<u8> e o caminho manual. Mas se você precisa de criptografia como parte do protocolo brec, sem espalhar boilerplate pelo código produtor/consumidor, crypt oferece uma rota mais suave e verificável.
Não vou sobrecarregá-lo com detalhes técnicos e uma lista dos métodos e configurações suportados para EncryptOptions / DecryptOptions, há documentação para isso, mas, como sempre, peço que você compartilhe uma "estrela" no GitHub. Para você, é apenas um clique, e para mim - feedback e motivação para não abandonar o projeto, mas desenvolvê-lo ainda mais. Com antecedência, raios de bondade e luz para todos que não passarem por perto.
PS: toda vez que peço para marcar o projeto com uma estrela no GitHub, me sinto como um modelo onlyfuns) só que não há nada para mostrar além do código) Em geral, apoie, se não for difícil, se for difícil, então não precisa)
Obrigado.








