O que é o Desenvolvimento Baseado em Testes ou TDD?
Quais são as Três Leis do TDD?
É isso o que veremos neste post. Veja nesta página os posts da série sobre TDD.
Continue lendo “As Três Leis do TDD”Engenharia elétrica e eletrônica!
Cursos do LivrosEletronica.com! Veja a lista de cursos disponíveis em Cursos.
O que é o Desenvolvimento Baseado em Testes ou TDD?
Quais são as Três Leis do TDD?
É isso o que veremos neste post. Veja nesta página os posts da série sobre TDD.
Continue lendo “As Três Leis do TDD”Abaixo estão algumas das minhas preferências e suas razões.
Para mim 2 espaços é pouco e 8 espaços é muito na endentação. Quatro espaços parecem ser o meio termo adequado.
Linhas longas atrapalham a leitura do código: eu desejo um código tão legível quanto livros e jornais. Além disso, o limite de 80 caracteres permite abrir arquivos lado a lado sem necessitar de rolagem horizontal e ainda facilita revisão do código anterior e atual abertos lado a lado.
Com respeito a proibir tabulações, existe o argumento que tabulações são mais flexíveis, pois o programador pode escolher o tamanho de sua preferência. O argumento é válido, porém somente até se misturarem as tabulações e os espaços: nesse instante, a única pessoa que vê alinhado é o próprio autor do código; e todo o resto do mundo vê uma bagunça desalinhada. Usar apenas um tipo de espaço em branco elimina esse problema.
Abertura de chaves na linha abaixo.
Isso ajuda a identificar onde acontece a abertura do bloco. Chave abrindo na própria linha permite determinar o início do bloco sem precisar procurar por ela no fim de alguma linha.
unsigned long int funcao_h( unsigned long int parametro1, unsigned long int parametro2, unsigned long int parametro3) { return parametro1 + parametro2 - parametro3; }
Compare abaixo a mesma função abrindo as chaves na linha. Não é tão óbvio como no caso acima.
unsigned long int funcao_h( unsigned long int parametro1, unsigned long int parametro2, unsigned long int parametro3) { return parametro1 + parametro2 - parametro3; }
Abrindo as chaves na linha abaixo, fica óbvio onde os parâmetros terminam e onde inicia o corpo da função. O mesmo acontece em blocos de controle como if, for, etc.
Mesmo se eliminarmos qualquer informação sobre os caracteres, como é mostrado abaixo, facilmente identificamos a lista de parâmetros e o início do corpo da função.
XXXXXXXX XXXX XXX XXXXXXXXX XXXXXXXX XXXX XXX XXXXXXXXXXX XXXXXXXX XXXX XXX XXXXXXXXXXX XXXXXXXX XXXX XXX XXXXXXXXXXX X XXXXXX XXXXXXXXXX X XXXXXXXXXX X XXXXXXXXXXX X XXXXXXXX XXXX XXX XXXXXXXXX XXXXXXXX XXXX XXX XXXXXXXXXXX XXXXXXXX XXXX XXX XXXXXXXXXXX XXXXXXXX XXXX XXX XXXXXXXXXXX X XXXXXX XXXXXXXXXX X XXXXXXXXXX X XXXXXXXXXXX X
A maior parte do tempo que passamos programando é utilizada, na verdade, lendo o código. Esta pequena escolha ajuda a encontrar a parte que importa no momento.
A lista de parâmetros de funções e a lista de argumentos de chamada de uma função devem estar todos em uma linha ou cada um em sua linha.
Dessa forma, a lista de parâmetros (ou argumentos) realmente se parece com uma lista. É fácil de contar os parâmetros e é fácil de encontrá-los.
Nesse ponto, há três possibilidades avaliadas na ordem seguinte:
Exemplos abaixo:
/* Protótipo inteiro cabe em uma linha: */ int funcao_f(int parametro1, int parametro2) { return parametro1 + parametro2; } /* * Protótipo não cabe em uma única linha, porém * a lista de parâmetros cabe inteira na segunda linha: */ unsigned long int funcao_g( unsigned long int parametro1, unsigned long int parametro2) { return parametro1 + parametro2; } /* * Parâmetros não cabem todos na segunda linha, então * coloca-se um parâmetro por linha: */ unsigned long int funcao_h( unsigned long int parametro1, unsigned long int parametro2, unsigned long int parametro3) { return parametro1 + parametro2 - parametro3; }
Comentários não são analisados pelo compilador e não precisam fazer sentido para o código funcionar. Por isso não são atualizados em conjunto com o código. Consequentemente, com o passar do tempo a semântica (o significado) do código se desvia dos comentários presentes.
Na maioria dos casos é possível fazer um código autoexplicativo através da refatoração e da melhoria de nomes de funções e nomes de variáveis.
/* executa_loop1 */ void executa_loop1(void) { // Lê o valor do sinal do sensor int16_t val = ADC0; // Calcula tensão do sensor em volts // a partir do offset e do ganho float valf = (val - 512) * (24.0 / 512.0); // Escreve na serial Serial_EscreveFloat(valf); } /* ---------------------------------------- */ /* executa_loop2 */ #define SENSOR_OFFSET 512 #define SENSOR_GANHO (24.0 / 512.0) float TensaoSensor_Volts(void); void executa_loop2(void) { Serial_EscreveFloat(TensaoSensor_Volts()); } int16_t SinalSensor_ADC(void) { return ADC0; } float TensaoSensor_Volts(void) { return (SinalSensor_ADC() - SENSOR_OFFSET) * SENSOR_GANHO; }
Qual versão da função executa_loopX() você prefere ler?
Alguns comentários, por sua vez, são impossíveis de serem transformados em código. Por exemplo, o comentário a seguir mostra um diagrama de um LCD 16×2 que mostra a tensão elétrica e a corrente medida.
/* * 0123456789012345 * 0 Tensão XX,XX V 0 * 1 Corrente Y,YYY A 1 * 0123456789012345 */
Sobre o caractere inicial dos comentários multilinha, alguns editores e formatadores eliminam espaços em branco iniciais, alinhando o primeiro caractere não branco de cada linha.
Dessa forma, todo o esforço de alinhamento abaixo é perdido em um piscar de olhos:
/* 0123456789012345 0 Display 16x2 0 1 Linha 2 1 0123456789012345 */
O comentário acima acaba por se transformar em alguma dessas atrocidades:
/* 0123456789012345 0 Display 16x2 0 1 Linha 2 1 0123456789012345 */ /* 0123456789012345 0 Display 16x2 0 1 Linha 2 1 0123456789012345 */
Colocando os asteriscos de alinhamento o problema deixa de existir.
/* * 0123456789012345 * 0 Display 16x2 0 * 1 Linha 2 1 * 0123456789012345 */
Essas são as algumas das minhas preferências, ao menos as que eu acho mais importantes.
Todas essas já mudaram algumas vezes desde que comecei a programar e, com a experiência, são as que me parecem mais naturais e que facilitam a leitura do código.
Além disso, a maioria delas (exceto as sobre comentários), não me dão trabalho algum, pois utilizo um formatador automático. Assim posso focar no código em si e em como melhorá-lo para eliminar comentários.
Quando desenvolvido em grupo ou time, a formatação do código de um projeto é um assunto que cria polêmicas com frequência. Cada pessoa tem suas preferências e isso gera conflitos durante as revisões.
Para minimizar esse problema há três pontos que podem ser adotados pelo projeto:
Para o projeto em si, não importa qual é o padrão estético do código, desde que o código funcione. Os programadores, no entanto, possuem preferências com respeito à estética do código.
Esse primeiro ponto reforça que algum padrão exista. Não importa qual seja o padrão, desde que ele exista e seja seguido. Dessa forma, essas discussões nas revisões deixam de focar em critérios pessoais e passam a usar um critério objetivo: estar ou não de acordo com o padrão.
Com o padrão definido, as discussões sobre preferência pessoal são minimizadas e, caso haja algum ponto adicional que o padrão não abrange, faz-se uma reunião para definir o padrão para tal caso.
O segundo ponto, por sua vez, elimina o trabalho manual de formatar e consequentemente elimina apontamentos de erro nas revisões por conta de erros de formatação.
Também são eliminadas algumas reuniões sobre preferências pessoais não abrangidas pelo padrão, porque as decisões precisam ser mantidas dentro das opções fornecidas pelo formatador automático.
Além disso, são menos regras para se lembrar na hora de escrever código.
Só há uma regra: execute o formatador nos arquivos modificados.
Existe um projeto chamado pre-commit que realiza tarefas logo antes de realizar um commit com o git. O tipo de tarefa mais comum é formatar o código com alguma ferramenta externa, como o clang-format (para C/C++).
Ainda melhor: alguns editores podem ser configurados para formatar o código sempre que o arquivo é salvo.
É importante documentar como abaixar, instalar e configurar essas ferramentas, de forma que todos possam aderir sem dificuldades.
Crie um link com a informação no LEIAME (README) do próprio projeto ou adicione a informação diretamente nele.
Essa documentação só precisa ser feita uma vez e outros projetos podem simplesmente copiar e adaptar para as suas necessidades.
Definir um padrão para o código é importante para evitar discussões sem fim sobre a estética do código.
Ferramentas de formatação automática são a melhor opção para eliminar essas discussões e manter o código formatado consistentemente.
Documentar o processo de configuração das ferramentas é importante para os desenvolvedores fazerem uso delas.
Muitas vezes desejamos compilar e executar testes de unidade para a plataforma Windows. No entanto, reiniciar sua máquina e dar boot no Windows só para testar fica meio na contramão.
Uma alternativa é instalar o MinGW e o Wine, para podermos compilar e para executar os testes de unidade plataforma Windows, sem sair do conforto do Linux.
Para instalar o MinGW e o Wine:
# MinGW - Windows 32-bit: sudo apt install gcc-mingw-w64-i686 g++-mingw-w64-i686 # MinGW - Windows 64-bit: sudo apt install gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 # Wine sudo apt install wine
Para compilar para a plataforma Windows usamos os compiladores MinGW para C ou C++, para 32-bit ou 64-bit.
Compilador | Windows 32-bit | Windows 64-bit |
C (GCC) | i686-w64-mingw32-gcc | x86_64-w64-mingw32-gcc |
C++ (G++) | i686-w64-mingw32-g++ | x86_64-w64-mingw32-g++ |
Por exemplo, para compilar os arquivos test_unit.c
e unit.c
e para Windows 32-bit usamos:
i686-w64-mingw32-gcc test_unit.c unit.c -o test.exe
Então, para rodar o executável no próprio Linux, através o Wine, usamos:
wine test.exe
Podemos executar testes de unidade da plataforma Windows (32-bit ou 64-bit), sem sair do Linux, usando o Wine.
Para rodar um executável qualquer com o Wine, no entanto, pode ser necessário fornecer DLLs (bibliotecas). Porém, a princípio, um teste de unidade não deve depender de nada externo, pois todas as dependências devem ser fakes (falsos) ou mocks (imitadores) e, portanto, não deveria cair neste problema.
Se o seu teste PRECISA de alguma DLL, ele provavelmente ele se encaixa melhor no grupo de testes de integração, não de testes de unidade.
No post anterior vê-se como configurar os dois Watchdogs do microcontrolador STM32F.
Porém, é possível saber qual o motivo que causou o reset? O microcontrolador ligou? Foi pressionado o botão de reset? Ou foi algum dos Watchdogs?
A resposta é: sim, é possível determinar a causa do reset.
Para determinar o motivo do reset, basta avaliar o valor lido no registrador RCC_CSR. Os seis bits mais significativos são utilizados para determinar a fonte do reset:
Bit | Valor | Motivo |
31 | 0x80000000 | Low-power reset Reset de baixo-consumo |
30 | 0x40000000 | Window watchdog Watchdog de janela |
29 | 0x20000000 | Independent watchdog Watchdog independente |
28 | 0x10000000 | Software reset Reset por software |
27 | 0x08000000 | POR/PDR reset Reset de energização |
26 | 0x04000000 | Pin reset Reset pelo pino |
Abaixo segue o código que lê a fonte do reset, limpa o registrador e determina a fonte, executando blocos de acordo. Com isso é possível determinar tomar ações diferenciadas para cada um dos casos. Note que é usada a operação lógica & (E bit-a-bit).
Para obter apenas o motivo do último reset é necessário realizar a limpeza do registrador. Isso é necessário porque quando ocorre um reset, os bits do registrador não são limpados para zero, mas apenas setados para um, fazendo-os acumular cada uma das fontes de reset que ocorreram.
uint32_t reset_source = 0;
// Lê a fonte do reset
reset_source = RCC->CSR;
// Limpa o registrador
__HAL_RCC_CLEAR_RESET_FLAGS();
if (reset_source & RCC_CSR_LPWRRSTF) {
// 0x80000000 Low-power reset
}
if (reset_source & RCC_CSR_WWDGRSTF) {
// 0x40000000 Window watchdog reset
}
if (reset_source & RCC_CSR_IWDGRSTF) {
// 0x20000000 Independent watchdog reset
}
if (reset_source & RCC_CSR_SFTRSTF) {
// 0x10000000 Software reset
}
if (reset_source & RCC_CSR_PORRSTF) {
// 0x08000000 POR/PDR reset
}
if (reset_source & RCC_CSR_PINRSTF) {
// 0x04000000 Pin reset
}
Veja abaixo dois casos:
Portanto, podemos verificar qual a fonte de um reset, ou seja, qual o seu motivo. Dessa forma, podemos determinar se houve um reset por algum Watchdog, pino, energização, etc. Com isso podemos ter uma dica do que pode ter acontecido logo antes do reset.
No post anterior foi visto como configurar o clock do microcontrolador STM32F para rodar a 72 MHz, baseado no cristal externo.
Neste post serão explicados pra que são e como configurar os dois Watchdogs do STM32F (IWDG e WWDG).
O Watchdog (cão de guarda) é um periférico interno do microcontrolador que serve para identificar mal funcionamento do programa e reiniciar o sistema a partir de um reset.
É preciso recarregar/alimentar (reload/feed) periodicamente o Watchdog para que ele não cause um reset.
Então, quando um programa entra em um laço infinito ou entra em algum estado inconsistente que evita que o Watchdog seja adequadamente recarregado, o sistema vai resetar em um tempo máximo estipulado pela configuração do Watchdog.
O IWDG (Watchdog Independente) é chamado assim porque ele utiliza uma fonte de clock independente do clock do sistema.
Para habilitar o IWDG:
O IWDG pode ser recarregado a qualquer momento, independente do valor do seu contador. Ele vai causar um reset quando seu contador chegar até zero.
Com um clock de 40 kHz, prescaler igual a 16 e valor de recarregamento 4095, o IWDG causará um reset em de 1,6 segundos.
T = 16*(4095+1)/40000
Para evitar isso, chamamos a função que reseta o IWDG no fim do laço while(1){…}, o qual demora 1.0 segundo para ser executado:
int main(void) {
// ...
while (1)
{
// ...
// Recarrega o IWDG
HAL_IWDG_Refresh(&hiwdg);
// ...
}
// ...
}
Note que, para poder debugar o microcontrolador com o IWDG ativo, é necessário configurá-lo para que pare a contagem quando o processador pára em um breakpoint.
Na função MX_IWDG_Init() adicione as linhas a seguir:
static void MX_IWDG_Init(void)
{
// ...
// Necessário para parar o IWDG quando
// o programa para em um breakpoint
__HAL_DBGMCU_FREEZE_IWDG();
// ...
}
O WWDG (Watchdog de Janela) é chamado assim porque com ele é possível configurar uma janela de tempo onde ele deve ser recarregado, de forma a não gerar um reset do microcontrolador. Se recarregar antes do tempo ou depois do tempo o reset ocorre.
Em outras palavras, o WWDG pode ser configurado para exigir um tempo mínimo e máximo entre os recarregamentos, causando um reset caso seja recarregado antes do tempo mínimo ou caso não seja recarregado no tempo máximo.
Este Watchdog funciona de forma um pouco diferente:
Para habilitar o WWDG:
Com um clock de 72 MHz, portanto 36 MHz em APB1, prescaler igual a 8 e valor de recarregamento igual a 127, o WWDG causará um reset em de 58,2 milissegundos após o o recarregamento anterior.
T1 = 8*4096*(127-63)/36000000
Com o valor da janela igual a 64, a janela de recarregamento inicia em 57,3 milissegundos após o recarregamento anterior.
T2 = 8*4096*(127-64)/36000000
Dessa forma há uma janela de tempo de 0,091 milissegundos para que o recarregamento ocorra: depois de T2 e antes de T1.
ΔT = T1-T2 = 8*4096*(64-63)/36000000
+--------------------------+
| Recarrega Reset |
| |-------------|----| |
| T0 T2 T1 |
| <--> |
| Janela |
+--------------------------+
Para evitar isso, no arquivo main.c (ou em qualquer arquivo) criamos a função HAL_WWDG_EarlyWakeupCallback(), a qual chama a função de reset do WWDG.
Essa função criada é automaticamente chamada pelo tratamento da interrupção do WWDG.
void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef *hwwdg) {
// Recarrega o WWDG
HAL_WWDG_Refresh(hwwdg);
}
De forma semelhante ao IWDG, para que seja possível debugar o microcontrolador com o WWDG ativo, é necessário configurá-lo para que pare a contagem quando a execução é pausada em um breakpoint.
Na função MX_WWDG_Init() adicione as linhas a seguir:
static void MX_WWDG_Init(void)
{
// ...
// Necessário para parar o WWDG quando
// o programa para em um breakpoint
__HAL_DBGMCU_FREEZE_WWDG();
// ...
}
Você pode se perguntar porque seriam necessários dois Watchdogs?
Caso o clock principal do sistema falhe, o que pode acontecer por exemplo escrevendo indevidamente em certos registradores, o WWDG vai parar de contar e, portanto, não causará reset. Nesse caso o IWDG entrará em ação, reiniciando o microcontrolador.
Além disso, como foi feito neste exemplo, ambos podem cuidar de dois escopos diferentes:
Assim, com o uso de ambos os Watchdogs do STM32F, podemos obter uma garantia de que o programa está operando de forma correta: executando todas as suas tarefas e mantendo interrupções habilitadas.
No post anterior foi explicado como configurar um projeto no STM32CubeIDE e debugar o microcontrolador STM32F usando o debugger ST-LINK V2.
Neste post será visto como configurar o clock do microcontrolador para rodar com uma frequência de 72 MHz, utilizando o cristal de 8MHz que vem na placa de desenvolvimento. Na foto este cristal está logo à direita do microcontrolador.
Por padrão o clock do microcontrolador STM32F vem de um oscilador RC interno de 8MHz, calibrado em fábrica. Porém, de acordo com o datasheet, sua frequência pode variar até 2,5 %, o que facilmente se torna problemático em sistemas que fazem uso de comunicação ou de alguma outra forma dependem da precisão do tempo.
Um cristal, por sua vez possui uma variação muito menor na frequência de operação, por exemplo 100 ppm, ou seja, 0,01 %.
Para habilitar e configurar o cristal oscilador:
Para comparar o clock anterior com o atual, pode-se usar o bloco de código abaixo, colocado logo antes do laço while(1){…}.
// Conta quanto incrementos faz em 1 segundo
// Clock 72 MHz: Cristal externo
{
uint32_t wait = 1000; // 1 segundo
uint32_t count = 0;
uint32_t tickstart = HAL_GetTick();
while ((HAL_GetTick() - tickstart) < wait)
{
count++;
}
count = 0;
}
Este código conta quantas vezes a variável count é incrementada enquanto se espera o delay de 1 segundo. O código foi adaptado da função HAL_Delay().
Executando para cada uma das configurações temos o seguinte resultado:
Oscilador | Frequência | Incrementos | Proporção |
RC interno | 8 MHz | 274.030 | 1 x |
Cristal externo | 72 MHz | 1.892.968 | 6.9 x |
Dessa forma, rodando a 72 MHz o contador é incrementado 6.9 vezes mais rápido do que quando rodando a 8 MHz. Revelando um ganho em velocidade, porém menor do que as 9.0 vezes que o clock foi acelerado.
Dessa forma, configurando o clock em 72 MHz usando o cristal externo de 8 MHz, podemos acelerar o processamento e aumentar a precisão temporal do microcontrolador.
Portanto, podemos fazer o LED piscar com mais precisão e fazer mais trabalho entre cada uma das piscadas.
Esta semana chegou em minhas mãos uma pequena placa de desenvolvimento e o seu gravador.
Esta placa possui o microcontrolador STM32F103C8T6, um ARM Cortex-M3 que roda em até 72 MHz, com 64 kB de flash (programa), 20 kB de RAM (variáveis) e diversas interfaces de comunicação.
Para programar vamos usar a STM32CubeIDE, uma IDE focada em microcontroladores fornecida pela STM. Vamos criar o projeto:
Para fazer o LED piscar é muito simples. Usamos duas funções:
O arquivo com a função main() se encontra em Core/Src/main.c. Este é um dos arquivos gerados automaticamente pela IDE.
Note que você pode modificar o código entre os comentários de início (USER CODE BEGIN XXX) e fim (USER CODE END XXX).
A IDE usa estes comentários para saber onde inserir o código gerado automaticamente. O que estiver fora das regiões permitidas será eliminado na próxima vez que o arquivo Blink.ioc for editado e o código for gerado novamente.
Encontre o laço infinito [while(1){…}] e adicione as chamadas de função para aguardar (delay) e escrever no pino (write pin).
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
// Desligar LED
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
HAL_Delay(500);
// Ligar LED
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
Para configurar o debug basta seguir os passos abaixo:
Agora é só se divertir com a placa de desenvolvimento: configure os pinos como entrada ou saída, escreva e leia eles.
MAC significa Código de Autenticação de Mensagem (Message Authentication Code). Este código é enviado junto com a mensagem, para provar sua integridade e autenticidade.
Integridade: Se algum bit da mensagem for alterado o MAC fica inválido. Semelhante a enviar a mensagem junto com sua hash.
Autenticidade: Apenas quem possui a chave criptográfica K é capaz de criar e verificar uma mensagem autenticada com esta chave.
A partir da chave e da mensagem criamos o código de autenticação, também chamado de TAG de autenticação. Enviamos o par (MENSAGEM, TAG) e quem recebe este par pode verificar a integridade e autenticidade da mensagem usando a chave K.
A chave K e a tag de autenticação precisam ambas ter comprimentos de acordo com o nível de segurança da aplicação.
A primeira opção é HMAC, que usa uma Função Hash para criar o código de autenticação. Eis o algoritmo:
se (comprimento(K) > Hash.Tamanho_Bloco) { K = Hash(K) }
enquanto (comprimento(K) < Hash.Tamanho_Bloco) { K = K || 00 }
K1 = K XOR 5C…5C
K2 = K XOR 36…36
TEMP = Hash(K2 || MENSAGEM)
TAG = Hash(K1 || TEMP)
Usamos uma chave para criar duas chaves diferentes. Cada uma é usada em uma etapa: criar uma hash da mensagem (TEMP) e tag de autenticação (TAG). Note que estamos utilizando o símbolo || com o significado de concatenação.
Antes de gerar K1 e K2, a chave K deve ter o tamanho do bloco da função hash. Este preenchimento do bloco permite pré-computar Hash(K1) e Hash(K2), acelerando a autenticação.
Se o comprimento da chave K for maior que o bloco de entrada da função hash (64 bytes/512 bits para SHA-256 ou 128 bytes/1024 bits SHA-512), primeiro realizamos a hash da chave.
Então, se o comprimento da cahve K for menor que o tamanho do bloco de entrada da função hash adicionamos zeros até completar o tamanho.
Podemos usar qualquer função hash com HMAC.
TAG =HMAC-SHA256(K, MENSAGEM)
TAG =HMAC-SHA512(K, MENSAGEM)
Obviamente vamos utilizar hashes que são criptograficamente seguras, por exemplo SHA2 e SHA3, e com comprimentos de saída adequados para o nível de segurança exigido pela aplicação. A chave K também precisa ser longa o suficiente para ser segura.
KMAC é uma extensão do padrão SHA3 usando XOFs (funções hash com saída de comprimento variável) baseada na esponja criptográfica Keccak.
Uma vantagem clara de usar KMAC, é que permite diferentes comprimentos de saída, gerando TAGs completamente diferentes para cada comprimento de saída.
KMAC é mais flexível e complicada que HMAC. Não vamos entrar em detalhes.
Podemos escolher entre os níveis de segurança máximos de 128 e 256 bits.
TAG =KMAC-128(K, MENSAGEM, COMPRIMENTO_TAG)
TAG =KMAC-256(K, MENSAGEM, COMPRIMENTO_TAG)
A chave K e a TAG precisam ter comprimentos compatíveis com o nível de segurança da aplicação e a função KMAC também precisa ser escolhida adequadamente. Ex: chave 256 bits e tag 256 bits só fazem sentido com KMAC-256.
É possível também usar o sistema de autenticação CBC-MAC, que usa cifras em blocos como AES em modo CBC.
CBC-MAC possui características semelhantes a HMAC. Usa duas chaves diferentes K1 e K2, geradas a partir de uma chave mestre K. Primeiro calculamos Cn, o último bloco do modo CBC com a chave K1. Então criptografamos este bloco com a chave K2, gerando a tag de autenticação.
K1 = K XOR 5C…5C
K2 = K XOR 36…36
C0 = EK1(IV)
Ci = EK1(Pi XOR Ci-1)
Cn = EK1(Pn XOR Cn-1)
TAG = EK2(Cn)
A tag de autenticação tem o mesmo comprimento do bloco da cifra, 128 bits no caso de AES.
TAG = CBC-MAC-AES(K, MENSAGEM)
Porque não usar HMAC ou KMAC sempre? Em sistemas embarcados com pouca memória de programa, por exemplo, pode ser inviável implementar uma cifra (como AES-128) e também uma hash (como SHA2 ou SHA3). Então usa-se a cifra para ambos criptografia e autenticação.
Note que no modo CBC-MAC, deixamos explícito a existência de um vetor de inicialização (IV).
Este é um nonce, um número utilizado apenas uma vez com a chave K. Ele pode ser aleatório ou um contador incrementado a cada mensagem.
O nonce serve para que duas mensagens tenham tags diferentes, mesmo se o conteúdo seja idêntico.
HMAC e KMAC também podem utilizar um IV, basta colocá-lo logo antes da mensagem:
MENSAGEM = IV || MENSAGEM
Isso é semelhante ao CBC-MAC, que coloca o IV no bloco zero, antes da mensagem.
Se considerarmos os IV como sendo parte da mensagem, sua função é de fazer com que sempre tenhamos uma mensagem diferente sendo autenticada, gerando sempre uma tag diferente.
Note que o vetor de inicialização (IV) é enviado junto com a mensagem, ou seja, não é secreto. Em alguns casos ele não é transmitido, se pode ser identificado pelo contexto.
HMAC – FIPS PUB 198-1 – The Keyed-Hash Message Authentication Code
KMAC – NIST 800-185 – SHA-3 Derived Functions
CBC-MAC – FIPS PUB 113 – Computer Data Authentication
Veja mais posts na Categoria Criptografia!
Exitem diversos modos de operação de criptografia com os quais podemos utilizar cifras de blocos, como a Criptografia AES.
Cada um tem suas características, que os fazem mais ou menos adequados para certas aplicações.
Os modos de operação mais comuns são: ECB, CBC, OFB, CFB, CTR.
Livro Eletrônico de Códigos.
Cada chave criptográfica possui um livro de códigos, que relaciona dados plenos com dados cifrados. Este modo de operação é o mais simples, mas dificilmente é recomendado.
Encriptar:
Ci = EK(Pi)
Para n blocos, envia-se:
C1, C2, …, Cn
Decriptar:
Pi = DK(Ci)
Note que este modo revela igualdade igualdade de blocos.
Este modo exige que as mensagens sejam de tamanho divisível pelo tamanho do bloco (16 bytes para AES). Caso contrário, a mensagem deve ser preenchida (padding). [TODO Link padding.]
Cadeia de Blocos Cifrados.
Os dados plenos Pi são misturados (XOR) com a cifra Ci-1 do bloco anterior antes de encriptar.
Encriptar:
C0 = EK(IV)
Ci = EK(Pi XOR Ci-1)
Para n blocos, envia-se:
IV, C1, C2, …, Cn
Decriptar:
C0 = EK(IV)
Pi = DK(Ci) XOR Ci-1
Este modo utiliza um vetor de inicialização (IV) que deve ser um número utilizado apenas uma vez (nonce) para cada chave K, ou seja, cada mensagem criptografada com a chave K precisa de um IV diferente. O IV não precisa ser secreto, pode ser transmitido junto com a mensagem.
Se repetir o mesmo IV os dados cifrados revelam igualdade dos primeiros blocos e a diferença (XOR) do primeiro bloco diferente. Conhecendo uma mensagem, revela-se a outra, exceto depois do bloco diferente. Isso no melhor dos casos, ou seja, com uma cifra ideal. Algumas cifras são extremamente fracas caso houver repetição de IV.
Note que o primeiro bloco cifrado é misturado com C0, que é o IV encriptado. Não se envia C0.
Este modo, assim como ECB, também exige que as mensagens sejam de tamanho divisível pelo tamanho do bloco ou que o último bloco seja preenchido (padding).
Realimentação da saída.
Este modo transforma uma cifra de blocos (block cipher) em uma cifra de corrente (stream cipher).
Os dados plenos Pi são misturados (XOR) com a fonte pseudo-aleatória Oi-1 para realizar a encriptação.
Encriptar:
O0 = EK(IV)
Oi = EK(Oi-1)
Ci = Pi XOR Oi-1
Para n blocos, envia-se:
IV, C1, C2, …, Cn
Decriptar (idêntico a encriptar):
O0 = EK(IV)
Oi = EK(Oi-1)
Pi = Ci XOR Oi-1
Este modo utiliza um vetor de inicialização (IV) que deve ser um número utilizado apenas uma vez (nonce) para cada chave K, ou seja, cada mensagem criptografada com a chave K precisa de um IV diferente. O IV não precisa ser secreto, pode ser transmitido junto com a mensagem.
Se repetir o mesmo IV os dados cifrados revelam igualdade dos blocos e a diferença (XOR) dos blocos diferentes. Conhecendo uma mensagem, revela-se a outra. [Novamente… Isso no melhor dos casos, ou seja, com uma cifra ideal. Algumas cifras são extremamente fracas caso houver repetição de IV.]
Este modo não exige preenchimento (padding). Se o último bloco não está completo com dados plenos, podemos enviar apenas estes bytes encriptados de Cn, ignorando o restante dos bytes. Note que isso revela o tamanho da mensagem. Se o tamanho da mensagem deve ser secreto ou obfuscado é necessário realizar o preenchimento (padding).
Realimentação de cifra.
Este modo transforma uma cifra de blocos (block cipher) em uma cifra de corrente (stream cipher).
Os dados plenos Pi são misturados (XOR) com a cifra anterior Ci-1 para realizar a encriptação.
Encriptar:
C0 = IV
Ci = Pi XOR EK(Ci-1)
Para n blocos, envia-se:
IV, C1, C2, …, Cn
Decriptar (idêntico a encriptar):
C0 = IV
Pi = Ci XOR EK(Ci-1)
Este modo utiliza um vetor de inicialização (IV) que deve ser um número utilizado apenas uma vez (nonce) para cada chave K, ou seja, cada mensagem criptografada com a chave K precisa de um IV diferente. O IV não precisa ser secreto, pode ser transmitido junto com a mensagem.
Como no modo CBC, se repetir o mesmo IV os dados cifrados revelam igualdade dos primeiros blocos e a diferença (XOR) do primeiro bloco diferente. Conhecendo uma mensagem, revela-se a outra, exceto depois do bloco diferente. [Novamente… Isso no melhor dos casos, ou seja, com uma cifra ideal. Algumas cifras são extremamente fracas caso houver repetição de IV.]
Este modo não exige preenchimento (padding). Se o último bloco não está completo com dados plenos, podemos enviar apenas estes bytes encriptados de Cn, ignorando o restante dos bytes. Note que isso revela o tamanho da mensagem. Se o tamanho da mensagem deve ser secreto ou obfuscado é necessário realizar o preenchimento (padding).
Existem ainda outros modos de operação.
CTR (Counter): Usa um nonce (número usado apenas uma vez) em conjunto com um contador para gerar uma sequência pseudo-aleatória e realizar XOR com os dados plenos (parecido com o que acontece nos modos OFB e CFB).
CFB “parcial”: É possível usar blocos menores do que o bloco da cifra no modo CFB.
Wikipedia Block Cipher Modes of Operation
NIST Recommendation for Block Cipher Modes of Operation
Veja mais posts na Categoria Criptografia!