A biblioteca Ring é uma popular biblioteca de criptografia em Rust que oferece suporte para gerar chaves criptográficas a partir de uma única chave de entrada usando o HKDF (HMAC-based Extract-and-Expand Key Derivation Function), uma função de derivação de chaves baseada em HMAC. Conforme você verá neste artigo, o módulo ring::hkdf
suporta a geração segura de bytes pseudoaleatórios usando os métodos Salt::extract
e Prk::expand
implementados na biblioteca.
Introdução ao HKDF
O HKDF (HMAC-based Extract-and-Expand Key Derivation Function) foi definido pela IETF (Internet Engineering Task Force) na RFC 5869. Trata-se de um tipo de função de derivação de chaves (KDF) baseada em HMAC (consulte meu artigo sobre HMACs em Rust aqui). Especialistas em criptografia projetaram KDFs como uma primitiva para receber uma chave de entrada e gerar uma ou mais chaves criptograficamente fortes. As chaves geradas são pseudoaleatórias, derivadas de forma determinística a partir da entrada e não possuem entropia adicional.
O HKDF, conforme definido na RFC, possui duas etapas: HKDF-Extract e depois HKDF-Expand. A primeira etapa recebe a chave de entrada e extrai dela uma chave pseudoaleatória de comprimento fixo. A segunda etapa expande a chave pseudoaleatória gerada em várias outras chaves pseudoaleatórias adicionais (a saída da KDF).
HKDF-Extract
O documento RFC define a função HKDF-Extract da seguinte forma:
HKDF-Extract(salt, IKM) -> PRK
salt - Você pode definir o sal como um valor aleatório opcional para fornecer separação de domínio entre diferentes chamadas ao HKDF. É recomendável usar um sal para aumentar a segurança do HKDF.
IKM (Input Keying Material, ou Material de Chave de Entrada) - Esta é a chave de entrada que é usada como a fonte inicial de entropia para o HKDF.
PRK (Pseudo-Random Key) - Esta é a chave pseudoaleatória de comprimento fixo retornada como saída. O comprimento desta chave é igual ao comprimento da função hash usada pelo algoritmo HMAC escolhido (consulte meu artigo sobre funções hash em Rust aqui).
HKDF-Expand
O documento RFC define a função HKDF-Expand da seguinte forma:
HKDF-Expand(PRK, info, L) -> OKM
PRK (Pseudo-Random Key) - Esta é a chave pseudoaleatória de comprimento fixo que foi retornada pela chamada ao HKDF-Extract.
info - Esta é uma string opcional de dados de contexto. Forneça esse parâmetro para criar separação de domínio entre diferentes chamadas ao HKDF-Extract. Alterar o valor no parâmetro info irá alterar o valor de OKM produzido. Isso pode ser útil ao usar a mesma chave de entrada em vários contextos.
L - O comprimento do material de chave de saída em bytes. Esse parâmetro define a quantidade de material de chave a ser retornada na etapa HKDF-Expand.
OKM (Input Keying Material, ou Material de Chave de Saída) - Este é o material de chave de saída final que é retornado como uma única saída. Observe que essa chave de saída final pode ter até 255 vezes o comprimento da função hash usada pelo HMAC. Você pode criar várias chaves de saída a partir desse valor, se necessário.
Diretrizes de Uso do HKDF
- O HKDF requer uma chave de entrada forte com entropia suficiente para gerar chaves seguras. Isso significa que a entrada deve ser tanto aleatória (imprevisível) quanto longa. O uso de material de chave de entrada com um comprimento menor pode tornar o sistema vulnerável a ataques de força bruta.
- Chaves inseguras podem resultar de uma função hash fraca, portanto, é aconselhável evitar funções hash antigas, como SHA1.
- Você não deve usar o HKDF para outros fins, como autenticação de mensagens ou derivação direta de chaves a partir de senhas, embora possa usá-lo em combinação com algoritmos de hash de senha.
Resumo dos Tipos
O módulo ring::hkdf
contém os seguintes tipos e funções:
trait KeyType - Um tipo que define um método len
que determina o comprimento da chave de saída final.
struct Algorithm - O tipo de algoritmo HKDF a ser usado. Os algoritmos HKDF_SHA1_FOR_LEGACY_USE_ONLY
, HKDF_SHA256
, HKDF_SHA384
e HKDF_SHA512
são suportados. O tipo Algorithm
implementa o trait KeyType
, para que possa ser passado no lugar de KeyType
.
struct Salt - Criada fornecendo um algoritmo e um valor de sal. Suporta a extração da chave de entrada e a devolução de uma Prk
(chave pseudoaleatória).
struct Prk - Representa uma chave pseudoaleatória. Suporta a expansão da chave pseudoaleatória para a chave de saída. Implementa a função expand
, que retorna o Okm
(material de chave de saída).
struct Okm - Representa o material de chave de saída e define um método fill que consome o Okm
e copia o material de chave em um buffer.
fn Salt::extract - Esta é a implementação da função HKDF-Extract conforme definido no documento RFC.
pub fn extract(&self, secret: &[u8]) -> Prk
fn Prk::expand - Esta é a implementação da função HKDF-Expand conforme definida no documento RFC.
pub fn expand<'a, L: KeyType>(
&'a self,
info: &'a [&'a [u8]],
len: L
) -> Result<Okm<'a, L>, error::Unspecified>
Importações do Rust
Vamos começar importando os tipos necessários para o nosso projeto em Rust.
use ring::digest::SHA256_OUTPUT_LEN;
use ring::hkdf::HKDF_SHA1_FOR_LEGACY_USE_ONLY;
use ring::hkdf::HKDF_SHA256;
use ring::hkdf::HKDF_SHA384;
use ring::hkdf::HKDF_SHA512;
use ring::hkdf::KeyType;
use ring::hkdf::Algorithm;
use ring::hkdf::Salt;
use ring::hkdf::Prk;
use ring::hkdf::Okm;
Crie uma instância da struct Salt
Primeiro, criamos uma instância da struct Salt
passando o algoritmo HKDF e o valor de sal. Criar uma instância de Salt
é relativamente custoso, portanto, as instâncias que têm o mesmo valor de sal devem ser reutilizadas sempre que possível em vez de serem reconstruídas. O valor do sal é opcional, mas é uma prática recomendada usar um sal gerado aleatoriamente para melhorar a segurança do HKDF.
let salt = Salt::new(HKDF_SHA256, b"salt bytes");
Extraia a Chave Pseudoaleatória
Em seguida, chamamos o método Salt::extract
, passando o material de chave de entrada, para gerar uma instância de Prk (a chave Pseudoaleatória). Isso representa a operação HKDF-Extract.
let input_key_material = b"secret key"; // use uma chave longa gerada aleatoriamente
println!("Input key material: {}", hex::encode(input_key_material)); // não imprima isso na produção
let pseudo_rand_key: Prk = salt.extract(input_key_material);
Expanda a Chave Pseudoaleatória
Em seguida, chamamos o método Prk::expand
, passando alguns dados de contexto específicos da aplicação e uma instância de KeyType, que é usada para fornecer o comprimento da chave de saída a ser retornada. Neste caso, estamos passando a instância de Algorithm
HKDF_SHA256
, que implementa KeyType
e retorna o valor SHA256_OUTPUT_LEN
na chamada de KeyType::len
. A chamada só falhará se KeyType::len
for mais de 255 vezes maior que o tamanho da Prk. Isso significa que podemos gerar até 255 chaves com o mesmo comprimento da chave pseudoaleatória.
Em seguida, usamos o método Okm::fill
para obter os bytes da chave de saída. Observe que o método fill consome a instância de Okm
e, portanto, só pode ser chamado uma vez. Essa estrutura de design impede a reutilização da chave de saída.
let context_data = &["context one".as_bytes()];
let output_key_material: Okm<Algorithm> = pseudo_rand_key.expand(context_data, HKDF_SHA256).unwrap();
let mut result = [0u8; SHA256_OUTPUT_LEN];
output_key_material.fill(&mut result).unwrap();
println!("Derived output key material: {}", hex::encode(result)); // não imprima isso na produção
Após executar o código, obtemos a seguinte saída:
Input key material: 736563726574206b6579
Derived output key material: 811105f7ecc53c62b340bc116a94a57c740b33c4d306844d8a88a1431e8c7b6e
Fornecendo Dados de Contexto com o Info
Observe que é possível fazer várias chamadas a Prk::expand
usando a mesma instância de Prk
, mas, como a função é determinística, usar os mesmos dados de contexto em chamadas separadas produzirá o mesmo material de chave de saída. Por esse motivo, podemos reutilizar o Prk
e fornecer dados de contexto diferentes no parâmetro info em chamadas separadas à Prk::expand
para gerar chaves diferentes a partir da mesma instância da Prk
. No entanto, certifique-se de estar ciente das implicações de segurança ao fazer isso.
let context_data = &["context one".as_bytes()];
let output_key_material: Okm<Algorithm> = pseudo_rand_key.expand(context_data, HKDF_SHA256).unwrap();
let mut result = [0u8; SHA256_OUTPUT_LEN];
output_key_material.fill(&mut result).unwrap();
println!("Derived output key material: {}", hex::encode(result)); // não imprima isso na produção
// segunda chamada com dados de contexto diferentes produz uma saída diferente
let context_data = &["context two".as_bytes()];
let output_key_material: Okm<Algorithm> = pseudo_rand_key.expand(context_data, HKDF_SHA256).unwrap();
let mut result = [0u8; SHA256_OUTPUT_LEN];
output_key_material.fill(&mut result).unwrap();
println!("Derived output key material: {}", hex::encode(result)); // não imprima isso na produção
Após executar o código, obtemos a seguinte saída:
Input key material: 736563726574206b6579
Derived output key material: 811105f7ecc53c62b340bc116a94a57c740b33c4d306844d8a88a1431e8c7b6e
Derived output key material: a0ed15560bbed56af00c380e985ef8f9162866332a6e107f823623861d1fc37f
Gerando Múltiplas Chaves de Saída
No exemplo acima, a chave de saída gerada tinha o mesmo tamanho da chave pseudoaleatória gerada (32 bytes no caso do SHA256), mas também podemos gerar chaves de saída mais longas na etapa de expansão, com até 255 vezes o tamanho da chave pseudoaleatória. Isso é útil se precisarmos gerar mais de uma chave. Neste exemplo, geramos três chaves de saída:
const NUM_OF_KEYS: usize = 3;
const OUTPUT_KEY_SIZE: usize = NUM_OF_KEYS * SHA256_OUTPUT_LEN;
struct MyKeyType(usize);
impl KeyType for MyKeyType {
fn len(&self) -> usize {
self.0
}
}
// Etapas de Salt & HDKF-Extract omitidas por brevidade
// A operação HKDF-Expand
let context_data = &["context one".as_bytes()];
let output_key_material: Okm<MyKeyType> = pseudo_rand_key.expand(context_data, MyKeyType(OUTPUT_KEY_SIZE)).unwrap();
let mut result = [0u8; OUTPUT_KEY_SIZE];
output_key_material.fill(&mut result).unwrap();
println!("Derived output key 1: {}", hex::encode(&result[0..32])); // não imprima isso na produção
println!("Derived output key 2: {}", hex::encode(&result[32..64])); // não imprima isso na produção
println!("Derived output key 3: {}", hex::encode(&result[64..96])); // não imprima isso na produção
Após executar o código, obtemos a seguinte saída:
Derived output key 1: 811105f7ecc53c62b340bc116a94a57c740b33c4d306844d8a88a1431e8c7b6e
Derived output key 2: f4ad09555fb7c36a21ee7632f35c89ecb82c99c7dac499ce3b495789528749a3
Derived output key 3: e0897b8085fd74edfc868f850cabf8a4a1df7dbb038dbb0a385189bcefe0f234
Código de Amostra Completo
Aqui está o código completo para referência:
use ring::digest::SHA256_OUTPUT_LEN;
use ring::hkdf::HKDF_SHA1_FOR_LEGACY_USE_ONLY;
use ring::hkdf::HKDF_SHA256;
use ring::hkdf::HKDF_SHA384;
use ring::hkdf::HKDF_SHA512;
use ring::hkdf::KeyType;
use ring::hkdf::Algorithm;
use ring::hkdf::Salt;
use ring::hkdf::Prk;
use ring::hkdf::Okm;
fn main() {
// cenário 1 - gerar chave de saída única
let input_key_material = b"secret key";
println!("Input key material: {}", hex::encode(input_key_material)); // não imprima isso na produção
// Constrói um novo Salt com o valor fornecido, com base no algoritmo de resumo fornecido
let salt = Salt::new(HKDF_SHA256, b"salt bytes");
// A operação HKDF-Extract
let pseudo_rand_key: Prk = salt.extract(input_key_material);
// A operação HKDF-Expand
let context_data = &["context one".as_bytes()];
let output_key_material: Okm<Algorithm> = pseudo_rand_key.expand(context_data, HKDF_SHA256).unwrap();
let mut result = [0u8; SHA256_OUTPUT_LEN];
output_key_material.fill(&mut result).unwrap();
println!("Derived output key material: {}", hex::encode(result)); // não imprima isso na produção
// second call with different context data produces a different output
let context_data = &["context two".as_bytes()];
let output_key_material: Okm<Algorithm> = pseudo_rand_key.expand(context_data, HKDF_SHA256).unwrap();
let mut result = [0u8; SHA256_OUTPUT_LEN];
output_key_material.fill(&mut result).unwrap();
println!("Derived output key material: {}", hex::encode(result)); // não imprima isso na produção
// cenário 2 - gerar várias chaves de saída
const NUM_OF_KEYS: usize = 3;
const OUTPUT_KEY_SIZE: usize = NUM_OF_KEYS * SHA256_OUTPUT_LEN;
struct MyKeyType(usize);
impl KeyType for MyKeyType {
fn len(&self) -> usize {
self.0
}
}
// A operação HKDF-Expand
let context_data = &["context one".as_bytes()];
let output_key_material: Okm<MyKeyType> = pseudo_rand_key.expand(context_data, MyKeyType(OUTPUT_KEY_SIZE)).unwrap();
let mut result = [0u8; OUTPUT_KEY_SIZE];
output_key_material.fill(&mut result).unwrap();
println!("Derived output key 1: {}", hex::encode(&result[0..32])); // não imprima isso na produção
println!("Derived output key 2: {}", hex::encode(&result[32..64])); // não imprima isso na produção
println!("Derived output key 3: {}", hex::encode(&result[64..96])); // não imprima isso na produção
}
Conclusão
Neste artigo, apresentamos a primitiva e o algoritmo criptográfico HKDF, explicamos como ele funciona e as propriedades criptográficas que ele pode fornecer. Em seguida, explicamos como usar o módulo hkdf
do Ring para extrair chaves criptográficas fortes de uma única chave de entrada, utilizando as funções Salt::extract
e Prk::expand
. Demonstramos alguns cenários: um usando diferentes dados de contexto fornecidos no parâmetro info para fornecer separação de domínio entre chamadas à Prk::expand
em diferentes contextos, e outro mostrando como gerar múltiplas chaves de saída.
Obrigado por ler. Se você tiver alguma dúvida ou comentário, não hesite em entrar em contato. Se você deseja ver mais artigos como este, pode se inscrever por e-mail ou me seguir no Twitter.
Artigo original publicado por Web3 Developer. Traduzido por Paulinho Giovannini.
Top comments (0)