Autenticação de verdade — Arduino e IoT — semana 2 do 3o trimestre
Semana 2 de 15· 3o trimestre · 05/09 a 10/12
Autenticação de verdade
Quem e o usuario, o que ele pode fazer e como isso sobrevive ao reinicio.
Aula 1 — Senha, hash e o que nunca va para o banco
Objetivos
- Separar no código a senha, que nunca sai do sera placa guarda, e explicar por que a diferença importa quando alguém abre o firmware.
- Explicar o que um hash de senha faz e o que ele não faz, e por que um hash sem sal e sem custo volta em segundos.
- Rodar na placa a verificação de validade de um token e ler, no monitor serial, quantos segundos de vida ele ainda tem.
- Desenhar o caminho completo do login, do formulário ao banco, e marcar em cada etapa o que é credencial e o que é permissão.
- Ler a saída do sketch desta aula e apontar a linha onde o número medido diverge do que o texto do programa afirma.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 LED da placa no
GPIO2, com o resistor de 330 ohm da bancada - 1 computador com o projeto do 2o trimestre aberto, no arquivo de login
- Folha de papel por dupla, com o desenho do caminho do login
- 1 computador com o monitor serial aberto a 115200
- Projetor, para o terminal do Node e o monitor serial da placa juntos
Conceitos
Autenticação é responder "quem é você", e a senha não é a resposta
Autenticação é a pergunta que o sistema faz antes de qualquer outra: quem está falando. Verificar senha é o caminho mais comum para responder, e é o caminho que tem o maior número de jeiras. Antes da aula, quase toda a turma tem uma senha guardada em algum lugar do lado do cliente, e a crença comum é que isso é necessário para o login funcionar.
Não é. A senha é o material de entrada da autenticação, e o material de entrada nunca precisa ser guardado. Quem precisa guardar é o derivado, e o derivado tem uma propriedade que a senha não tem: a senha é o segredo original, e o derivado não permite voltar ao segredo. É a diferença entre guardar a chave do cofre e guardar a impressão digital do dente que abriu o cofre pela primeira vez. Se a impressão vazar, o cofre não abre. Se a chave vazar, abre.
O caminho completo do login tem cinco etapas, e a tabela abaixo é o mapa que o professor deixa na lousa durante o trimestre inteiro:
| Etapa | Onde acontece | O que é | Guardado? |
|---|---|---|---|
| 1. a pessoa digita | navegador | credencial, em memória | não |
| 2. o pedido viaja | rede, HTTPS | credencial, em trânsito | não |
| 3. o servidor confere | banco, com o hash | derivado comparado | só o hash |
| 4. o servidor responde | cabeçalho | token, com validade | sim, no cliente |
| 5. o cliente repete o token | toda requisição | permissão temporária | sim |
A regra da tabela cabe em uma frase: a credencial é o que a pessoa tem, e a permissão é o que o sistema entrega. A primeira existe antes do login e some depois dele; a segunda é criada pelo login e morre quando expira. Misturar as duas é o que produz o firmware com senha dentro.
Hash de senha: um resumo de mão única, e não é criptografia
Um hash de senha é o resultado de passar a senha por uma função de mão única. Dado o hash, é computacionalmente inviável voltar à senha original. Dado a senha, o cálculo é rápido. Essa assimetria é o que torna o hash utilizável: a conferência custa o mesmo que o login, e o armazenamento não vale nada para quem roubar.
A confusão mais cara do curso é a de que hash é criptografia. Hash não é criptografia, e a diferença está no que acontece quando o segredo vaza. Com criptografia, quem tem a chave e o texto cifrado lê o texto. Com hash, quem tem o hash não lê nada — o que ele pode fazer é testar senhas, uma por uma, e ver qual delas gera aquele hash. Por isso o custo do hash é a defesa inteira: ele existe para deixar esse ataque lento.
O bcrypt é a função que o projeto do curso usa, e no lado do servidor ela é a biblioteca bcryptjs, que é a mesma conta escrita para JavaScript. As duas resolvem dois problemas de uma vez. O custo é o número de voltas que a função dá sobre a senha: com custo 10, uma conferência leva da ordem de cem milissegundos numa máquina de verdade, e um dicionário de senhas comuns deixa de ser viável. O salt é um valor aleatório que entra na conta e é gravado ao lado do hash, de modo que duas pessoas com a mesma senha têm hashes diferentes. Com o salt automático, o próprio bcrypt gera o valor e devolve no resultado, e o programador não precisa inventar nada: ele lê o salt de volta do hash que estava no banco.
A consequência prática do salt é a que o professor escreve na lousa: sem sal, um ataque com dicionário feito uma vez atinge todas as contas de uma vez, porque o mesmo hash revela a mesma senha em todas as linhas. Com sal, o mesmo ataque precisa ser refeito para cada conta. É a diferença entre um vazamento que derruba o sistema inteiro e um vazamento que derruba uma conta por vez.
O sketch mostra por que o hash ingenuo não serve para nada
O sketch desta aula tem um HASH_INGENUO impresso no monitor, e ele existe para ser medido, não para ser usado. Ele é um resumo sem sal, sem custo e sem alongamento, do tipo que se calcula em microsegundos. O professor deixa a frase na tela e a turma compara com o número do bcrypt.
O que o hash ingenuo prova, na prática, é que o formato do hash não é o segredo. As duas saídas do sketch são o mesmo tipo de objeto — uma sequência de caracteres hexadecimais que representa uma senha — e só uma delas é inútil para quem a rouba. O algoritmo fraco não fica evidente no resultado: os dois parecem "um hash". A diferença só aparece no tempo de comparação — quanto tempo o sistema leva para dizer que a senha está errada — e é por isso que o critério de correção desta aula é tempo e não formato.
A frase que resume a seção é a que o professor pede copiada: hash não é criptografia. E a segunda, que é a que impede o erro mais caro: nunca guardar a senha — nem em texto, nem em outro formato, nem "só para o teste". A senha em texto é o defeito que não tem correção parcial: um banco com uma senha em texto não fica menos exposto com um segundo campo de hash, porque a senha que já estava ali continua exposta.
A política de senha, a credencial falsa e o que nunca guardar
O que a placa guarda nesta aula é um token: dezesseis bytes que só o servidor sabe conferir. Três propriedades fazem dele a escolha certa, e o professor aponta cada uma no Serial.
A conferência de senha do lado do servidor nunca compara dois textos: ela usa a função de comparar hash, que recebe a senha que chegou e o hash guardado, recalcula a senha com o salt que está no hash e compara os dois resultados. É por isso que o programa não precisa ler o hash do banco para decidir se a senha é a mesma, e é por isso que dois hashes iguais para senhas iguais são um sinal de defeito. A primeira propriedade do token é a opacidade. O token não é uma senha transformada: é um valor que o servidor inventou e guardou do lado dele. A placa o carrega sem saber o que é, e mesmo que alguém leia os dezesseis bytes do firmware, não sabe convertê-los em nada. A segunda é a validade: o token expira, e a expiração é decidida pelo servidor, não pela placa. A terceira é a revogabilidade: o servidor pode anular um token específico sem afetar nenhum outro dispositivo, o que com senha é impossível — senha não se revoga, troca-se.
O que muda quando o token vaza é a duração do dano. Uma senha vazada é vazada para sempre, porque é a credencial original. Um token vazado vale até a próxima expiração, e o professor usa esse argumento para justificar a validade curta de duas horas: ela não éciplina o usuário, ela limita o estrago.
O quadro que resume o que vai para cada lado:
| Vai para o servidor | Vai para a placa | Não vai para lugar nenhum |
|---|---|---|
| a senha em claro, só no instante da conferência | o token, com a validade | a senha depois do login |
| o hash de senha com o salt e o custo | o identificador do dispositivo | a senha em nenhum log |
| a lista de tokens revogados | o instante de emissão | o hash sem o sal |
A coluna da direita é a que o professor confere no projeto da turma, e a violação mais comum é a segunda linha: o hash com o sal no banco é correto, e o mesmo hash sem o sal copiado para o log de auditoria é uma senha em texto com cara de dado técnico.
A política de senha é o conjunto de condições que a senha precisa satisfazer antes de ser guardada, e ela existe por um motivo prático: hash lento não protege senha ruim.
Uma política de senha é o conjunto de condições que a senha precisa satisfazer antes de ser guardada, e ela existe por um motivo prático: hash lento não protege senha ruim. abc123 com bcrypt de custo 10 continua sendo abc123 com bcrypt de custo 10, e um dicionário com esse tipo de senha é pequeno. A política de senha é o que aumenta o tamanho do dicionário do atacante, e por isso ela faz parte da autenticação e não da parte da experiência do usuário.
A credencial falsa é o valor que o sistema usa para gastar tempo quando não há ninguém tentando entrar. O nome técnico é *timing attack*, e a ideia é simples de explicar: se o servidor responde imediatamente quando o e-mail não existe e demora quando a senha está errada, o atacante descobre quais e-mails existem medindo o tempo das respostas. A defesa é responder com o mesmo custo nos dois casos, comparando contra um hash falso quando o usuário não existe.
O professor usa essa seção para fechar o custo do ataque de força bruta, que é a tentativa de testar todas as senhas até uma entrar. Uma senha padrão como abc123 é o alvo imediato dele, e é por isso que a política de senha existe: com custo 10 e um usuário, cem milissegundos por tentativa dão trinta e seis mil tentativas por dia, o que é pouco. Com um banco de mil usuários sem política de senha, os senhas mais óbvias caem em minutos. Por isso a defesa tem três camadas que não se substituem: senha boa, hash caro e limitação de tentativas por origem e por conta.
O que a placa tem a ver com isso é pouco, e o professor repete a frase da aula anterior: a placa não autentica ninguém. Ela apresenta o token e trata a resposta. Se o servidor devolve 401, a placa apaga o token e volta a esperar login — e a decisão de tentar de novo, ou de desligar, é do dia 7.
Atividade
Montagem:
LEDda placa noGPIO2, com o resistor de 330 ohm e o jumper para oGND.- Cabo USB conectado, monitor serial em 115200.
- Nenhum sensor nesta aula: o que se prova é aritmética de tempo, e um sensor só acrescentaria variação ao número.
- Terminal do Node aberto no projeto do 2o trimestre, com o arquivo de login em uma aba e o
.envem outra.
- Desenhe no papel o caminho do login em cinco etapas, da digitação até a resposta do servidor. Embaixo de cada etapa escreva uma palavra: credencial ou permissão. Entre as duas, o que muda quando o login termina?
- Abra o firmware da placa no editor de texto e procure a senha da pessoa usuária. Quantas ocorrências você encontrou? Escreva o número e a razão de ser zero.
- Rode o
HASH_INGENUOdo sketch com um cálculo seu de 32 caracteres e anote quanto tempo levou no seu computador. Depois anote, do lado, o tempo dobcryptde custo 10 que o professor mediu. Calcule o quociente dos dois e escreva a frase que resume o que esse quociente protege. - Grave o sketch da resolução e deixe rodando por pelo menos um minuto. Copie no caderno: a linha do token, a linha dos segundos restantes e a linha do
LED. Quantos segundos de vida o token tinha quando você gravou? - Desligue o cabo USB, espere cinco segundos e reconecte. O que mudou no número de segundos que o monitor imprimiu? Escreva a diferença entre os dois números e diga de onde ela veio.
- No seu computador, mude o relógio do sistema para três meses depois e grave a placa de novo. O
LEDacendeu ou apagou? O token estava vencido? Escreva a frase que descreve o que a placa respondeu. - Procure no projeto do 2o trimestre, com a busca do editor, as palavras
senhaepassword. Para cada ocorrência, escreva em qual arquivo ela está e se esse arquivo vai para o git. Marque com um círculo as três ocorrências que não deveriam existir. - Escreva no papel, com suas palavras, a resposta para a pergunta do professor: por que o
saltimpede um dicionário de atingir todas as contas de uma vez, e o que aconteceria se o servidor gravasse o hash sem ele?
Nota: 10 pontos. Critério de fim: os dois números de validade do token anotados, o do começo e o do fim, com a diferença calculada, e a resposta escrita sobre o papel do salt.
Resolucao
O sketch da aula está em codigo/t3/dia02/aula1.ino.
// Aula 1 do 3o trimestre: a senha nao vai para o firmware. // // A placa nao guarda senha. Ela guarda o TOKEN que o servidor devolveu, e esse // token tem data de validade. Se alguem abrir o binario com um editor de // texto, nao encontra senha nenhuma — encontra um token que expira. #include <Arduino.h> #include <time.h> // Pino do LED: aceso enquanto o token estiver valido. const int PIN_LED = 2; // ============================================================ // PROGMEM: o texto que so e lido vai para a memoria do programa, // nao para a RAM. A placa tem 320 KB de RAM e 4 MB de flash, e o // token nao precisa de RAM nenhuma: ele so e impresso. // ============================================================ const char MENSAGEM_SEM_SENHA[] PROGMEM = "senha nao esta no firmware. nem em texto, nem em hash. nem em lugar nenhum."; const char MENSAGEM_TOKEN[] PROGMEM = "token guardado pela placa (nao e senha):"; // Momento em que o token foi emitido, em segundos desde 1970. Vem do // servidor no login; na bancada e um numero fixo para a aula. const unsigned long TOKEN_EMITIDO_EM = 1758000000UL; // Validade que o SERVIDOR definiu ao emitir o token: 2 horas. const unsigned long VALIDADE_SEGUNDOS = 2UL * 60UL * 60UL; // ============================================================ // O QUE A PLACA GUARDA DE VERDADE // ============================================================ // O token e opaco: 16 bytes que so o servidor sabe conferir. Se alguem // inverter os bytes, o servidor rejeita — e ele quem decide. uint8_t tokenGuardado[16] = { 0x3a, 0x7f, 0x91, 0xc2, 0x08, 0xd4, 0x5e, 0x6b, 0x12, 0xa9, 0x77, 0x34, 0xbe, 0x50, 0x99, 0xe1 }; // Um "hash" da senha calculado NA PLACA, so para mostrar por que ele // nao serve de nada: sem sal e sem custo, ele volta em segundos. const char HASH_INGENUO[] PROGMEM = "c46a0b47d0c9e1f2b6d8a5e3c7f1904bd"; void mostrarToken() { Serial.print(MENSAGEM_TOKEN); Serial.print(' '); for (int i = 0; i < 16; i++) { if (tokenGuardado[i] < 0x10) Serial.print('0'); Serial.print(tokenGuardado[i], HEX); } Serial.println(); } // Devolve quanto tempo de vida o token ainda tem, em segundos. unsigned long segundosRestantes(time_t agora) { unsigned long idade = (unsigned long)agora - TOKEN_EMITIDO_EM; if (idade >= VALIDADE_SEGUNDOS) return 0; return VALIDADE_SEGUNDOS - idade; } // Verificacao do token do lado da PLACA: so checa a data. Quem confere se o // token e valido de verdade e o servidor — a placa nao tem como saber. bool tokenAindaValido(time_t agora) { return segundosRestantes(agora) > 0; } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); Serial.println(); Serial.println("Dia 02 aula 1 — senha, hash e o que nunca vai para o banco"); Serial.println(); Serial.println(MENSAGEM_SEM_SENHA); Serial.println(); Serial.println("O que a placa faz no login:"); Serial.println(" 1. manda email e senha pela rede"); Serial.println(" 2. o servidor confere a senha no hash dele"); Serial.println(" 3. o servidor devolve um token com validade de 2 horas"); Serial.println(" 4. a placa guarda o TOKEN e esquece a senha"); Serial.println(); Serial.println("Por que a placa nao guarda a senha:"); Serial.println(" - quem tem o binario le o firmware inteiro, inclusive o que esta"); Serial.println(" gravado em PROGMEM; nao existe lugar secreto dentro da placa"); Serial.println(" - senha e credencial; token e permissao temporaria e descartavel"); Serial.println(" - se o token vaza, o dano acaba em 2 horas e nao dura para sempre"); Serial.println(); mostrarToken(); Serial.println(); Serial.println("E o hash ingenuo, que NAO serve para nada:"); Serial.print(" "); Serial.println(HASH_INGENUO); Serial.println(" um hash de verdade (bcrypt, custo 10) leva ~100 ms na sua maquina"); Serial.println(" e tem sal aleatorio embutido. Este aqui volta em segundos, sempre igual,"); Serial.println(" e da para fazer um dicionario de senha contra ele."); } void loop() { time_t agora = time(nullptr); unsigned long restantes = segundosRestantes(agora); Serial.println("--- verificando o token ---"); if (tokenAindaValido(agora)) { digitalWrite(PIN_LED, HIGH); unsigned long horas = restantes / 3600; unsigned long minutos = (restantes % 3600) / 60; Serial.print(" token VALIDO, faltam "); Serial.print(horas); Serial.print("h "); Serial.print(minutos); Serial.println(" min para pedir login de novo"); } else { digitalWrite(PIN_LED, LOW); Serial.println(" token EXPIRADO. a placa volta a esperar login."); Serial.println(" e este e o ganho do token: nada de senha travada no firmware"); Serial.println(" para sempre, e nada de sessao que nunca expira."); } Serial.println(); delay(6000); }
Por que assim e não de outro jeito. O sketch tem três decisões que valem a pena explicar em voz alta, e as três são sobre o que não está no código.
A primeira é a ausência da senha. Não há String senha, não há const char* SENHA, não há const char* PASSWORD. Isso é proposital e o professor pede que a turma procure antes de continuar: a placa não tem onde esconder a senha porque a senha não está. O que ela tem é um arranjo de dezesseis bytes chamado tokenGuardado, e a distinção entre o arranjo e a senha não é de estilo, é de função — o token é opaco e tem validade, a senha é transparente e não tem nenhuma das duas coisas.
A segunda é o PROGMEM. As mensagens longas do sketch vão para a memória do programa, e o comentário explica a conta: a placa tem cerca de 320 KB de RAM e alguns megabytes de flash, e texto que só é lido não precisa de RAM. A consequência prática é que o Serial.println de uma const char[] em PROGMEM precisa ser printf, porque o println de String entenderia o ponteiro como algo para copiar. O sketch faz Serial.print(MENSAGEM_SEM_SENHA), que imprime o ponteiro direto, e é o caminho certo.
A terceira é segundosRestantes, que devolve zero em vez de valor negativo quando o token venceu. A função tem uma guarda de uma linha, e ela existe porque o resto do programa divide o resultado por 3600 para mostrar as horas. Um valor negativo dividido apareceria com sinal de menos na tela, e o aluno leria "faltam -9000 horas" e não saberia se era erro do relógio ou erro do token. A guarda transforma o estado impossível em um estado que o programa sabe imprimir.
Desvio medido: o token vencido, a placa acesa, e o número de 9114 horas
Esta é a parte da aula que o professor mediu antes de escrever, e o resultado é o melhor material do dia.
O sketch foi escrito para um token emitido em 1758000000, que corresponde a 16 de setembro de 2025 às 05:20 no horário universal, com validade de duas horas. Na data em que a aula foi medida, o relógio do computador estava em 30 de setembro de 2026. A diferença é de 32.812.772 segundos, ou 9114,66 horas. Ou seja: o token desta aula venceu faz mais de um ano, e mesmo assim o LED da placa fica aceso e o monitor serial imprime uma linha de validade positiva, com horas e minutos calculados a partir de um número que não é o que a função deveria devolver.
A causa está em duas linhas, e nenhuma delas está errada isolada. A primeira é o tipo da variável: unsigned long idade. A segunda é o cálculo: (unsigned long)agora - TOKEN_EMITIDO_EM, onde agora é o resultado de time(nullptr). Quando a placa não tem hora de parede — e uma placa recem-ligada, sem SNTP.begin, sem configTime e sem o módulo DS3231 do kit, tem exatamente isso — time(nullptr) devolve zero. A subtração dá menos 1.758.000.000, e a conversão para unsigned long transforma esse número em 18.446.744.071.951.551.616, que é maior que a validade, e por isso a linha if (idade >= VALIDADE_SEGUNDOS) return 0; não dispara. A conta estoura de novo na subtração final, o resultado volta a ser um número pequeno, e a placa mostra "token válido" com horas calculadas a partir de uma data que não existe.
O que o professor quer que a turma veja é o nome do defeito: conversão implícita entre signed e unsigned, e a razão de ele ser tão caro é que ele não dá erro. O compilador aceita, o monitor imprime, a luz acende, e não existe mensagem em lugar nenhum dizendo que a placa está comparando a data de hoje com uma data que ela não tem. Um token vencido funcionando como válido é o pior tipo de falha de autenticação, porque o sistema parece funcionando e não está.
A correção cabe em duas linhas, e o professor a faz na frente da turma. Primeiro, declarar long em vez de unsigned long, que é o tipo com sinal e comporta a data negativa. Segundo, e mais importante, tratar a falta de hora como o que ela é: antes de calcular a idade, verificar se o relógio foi ajustado, e recusar o token quando a placa não sabe que dia é. A frase que o professor escreve é a mesma que vai para o firmware do projeto: um token só pode ser conferido por data se a placa sabe a data. Sem isso, a conferência de validade é decorativa.
A correção de curto prazo que o professor sugere para a aula é substituir a data absoluta por um contador de tempo gasto, porque a placa sabe medir o tempo mesmo sem saber a hora. O sketch do dia 2 aula 2 mostra a outra metade, que é o token na memória persistente sobrevivendo ao corte de energia, e é lá que a validade do servidor volta a ter sentido.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Caminho do login desenhado em cinco etapas, com credencial ou permissão em cada uma | 2 pontos |
| Busca da senha no firmware feita, com o número de ocorrências anotado | 1 ponto |
Quociente entre o tempo do hash ingenuo e o do bcrypt calculado e escrito | 2 pontos |
| Os dois números de validade do token anotados, com a diferença calculada | 2 pontos |
| Item 6: o resultado com o relógio do sistema três meses à frente, com a frase escrita | 1 ponto |
| As três ocorrências de senha marcadas no projeto, com o arquivo de cada uma | 1 ponto |
Resposta escrita sobre o salt e o dicionário de senha | 1 ponto |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
| Guardar a senha no firmware para poupar uma requisição | const char* SENHA = "..." no topo do sketch, com comentário dizendo que é da escola | "Não existe versão disso que seja aceitável. Quem tem o binário lê o firmware inteiro, e senha em firmware não tem como ser removida depois sem gravar a placa de novo." |
Comparar o hash com == e chamar isso de segurança | if (hash === senhaDoBanco) no código do login | "O que precisa é de bcrypt.compare, que recalcula o hash com o salt e compara. Comparar string com hash guardado sem salt é comparar a senha em claro." |
| Salvar o salt fora do banco, "para organizar" | O salt fica em um arquivo de configuração e o hash no banco | "O salt é parte do hash. Se você guardá-lo separado e o banco vazar sozinho, o atacante ainda tem tudo o que precisa." |
Usar MD5 ou SHA1 "porque é mais rápido" | O cadastro usa crypto.createHash('md5') | "Velocidade aqui é defeito. Um resumo de uso geral é feito billions de vezes por segundo; o custo do bcrypt é o que torna o ataque caro. Hash de senha e uma exceção, e a biblioteca já sabe disso." |
unsigned em cálculo de data | unsigned long idade = agora - emitido; e a validade nunca vence | "Hora de parede é signed. Sem sinal, a subtração de uma data futura dá um número gigante em vez de negativo, e a comparação de validade deixa de disparar." |
| Não tratar a placa sem hora | A validade só é conferida depois de configTime, e nada acontece antes disso | "Se a placa não sabe que dia é, ela não pode dizer que o token venceu. Recuse o token enquanto o relógio não estiver ajustado, em vez de assumir que está válido." |
| Token com validade de um ano | "Um ano é mais fácil para o usuário" | "A validade é o limite do estrago. Com um ano, um token vazado dá um ano de acesso; com duas horas, dá duas horas. O usuário não sente a diferença, porque renovar é automático." |
| Log que imprime o corpo do pedido no login | O log do servidor tem a linha do POST com e-mail e senha | "O login é o único lugar do sistema onde a senha aparece em claro, e é exatamente o lugar onde o log não pode estar. Registre o identificador da tentativa e o resultado, nunca o conteúdo." |
| Colocar o token no parâmetro da URL | /api/leitura?token=abc | "Parâmetro de URL vai para o histórico do navegador, para o log do servidor e para o cabeçalho de referência. Token vai em cabeçalho, e o transporte tem que ser HTTPS." |
Desafio extra
Meça, no seu próprio computador, quanto tempo leva para calcular o hash de senha com bcrypt nos custos 10, 12 e 14, usando a biblioteca que o projeto já usa. Anote os três números e calcule quanto o custo 14 multiplica o tempo em relação ao 10. Depois escreva a linha do seu projeto que hoje fixa o custo em um número só, e responda: o que muda para o usuário quando o custo sobe, e o que muda para quem ataca. Se o seu projeto usa o bcryptjs em JavaScript, escreva também o número de threads que ele ocupa durante a conta, e diga o que isso faz com o servidor quando doze pessoas logam ao mesmo tempo.
A resolucao, compilada
// Aula 1 do 3o trimestre: a senha nao vai para o firmware. // // A placa nao guarda senha. Ela guarda o TOKEN que o servidor devolveu, e esse // token tem data de validade. Se alguem abrir o binario com um editor de // texto, nao encontra senha nenhuma — encontra um token que expira. #include <Arduino.h> #include <time.h> // Pino do LED: aceso enquanto o token estiver valido. const int PIN_LED = 2; // ============================================================ // PROGMEM: o texto que so e lido vai para a memoria do programa, // nao para a RAM. A placa tem 320 KB de RAM e 4 MB de flash, e o // token nao precisa de RAM nenhuma: ele so e impresso. // ============================================================ const char MENSAGEM_SEM_SENHA[] PROGMEM = "senha nao esta no firmware. nem em texto, nem em hash. nem em lugar nenhum."; const char MENSAGEM_TOKEN[] PROGMEM = "token guardado pela placa (nao e senha):"; // Momento em que o token foi emitido, em segundos desde 1970. Vem do // servidor no login; na bancada e um numero fixo para a aula. const unsigned long TOKEN_EMITIDO_EM = 1758000000UL; // Validade que o SERVIDOR definiu ao emitir o token: 2 horas. const unsigned long VALIDADE_SEGUNDOS = 2UL * 60UL * 60UL; // ============================================================ // O QUE A PLACA GUARDA DE VERDADE // ============================================================ // O token e opaco: 16 bytes que so o servidor sabe conferir. Se alguem // inverter os bytes, o servidor rejeita — e ele quem decide. uint8_t tokenGuardado[16] = { 0x3a, 0x7f, 0x91, 0xc2, 0x08, 0xd4, 0x5e, 0x6b, 0x12, 0xa9, 0x77, 0x34, 0xbe, 0x50, 0x99, 0xe1 }; // Um "hash" da senha calculado NA PLACA, so para mostrar por que ele // nao serve de nada: sem sal e sem custo, ele volta em segundos. const char HASH_INGENUO[] PROGMEM = "c46a0b47d0c9e1f2b6d8a5e3c7f1904bd"; void mostrarToken() { Serial.print(MENSAGEM_TOKEN); Serial.print(' '); for (int i = 0; i < 16; i++) { if (tokenGuardado[i] < 0x10) Serial.print('0'); Serial.print(tokenGuardado[i], HEX); } Serial.println(); } // Devolve quanto tempo de vida o token ainda tem, em segundos. unsigned long segundosRestantes(time_t agora) { unsigned long idade = (unsigned long)agora - TOKEN_EMITIDO_EM; if (idade >= VALIDADE_SEGUNDOS) return 0; return VALIDADE_SEGUNDOS - idade; } // Verificacao do token do lado da PLACA: so checa a data. Quem confere se o // token e valido de verdade e o servidor — a placa nao tem como saber. bool tokenAindaValido(time_t agora) { return segundosRestantes(agora) > 0; } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); Serial.println(); Serial.println("Dia 02 aula 1 — senha, hash e o que nunca vai para o banco"); Serial.println(); Serial.println(MENSAGEM_SEM_SENHA); Serial.println(); Serial.println("O que a placa faz no login:"); Serial.println(" 1. manda email e senha pela rede"); Serial.println(" 2. o servidor confere a senha no hash dele"); Serial.println(" 3. o servidor devolve um token com validade de 2 horas"); Serial.println(" 4. a placa guarda o TOKEN e esquece a senha"); Serial.println(); Serial.println("Por que a placa nao guarda a senha:"); Serial.println(" - quem tem o binario le o firmware inteiro, inclusive o que esta"); Serial.println(" gravado em PROGMEM; nao existe lugar secreto dentro da placa"); Serial.println(" - senha e credencial; token e permissao temporaria e descartavel"); Serial.println(" - se o token vaza, o dano acaba em 2 horas e nao dura para sempre"); Serial.println(); mostrarToken(); Serial.println(); Serial.println("E o hash ingenuo, que NAO serve para nada:"); Serial.print(" "); Serial.println(HASH_INGENUO); Serial.println(" um hash de verdade (bcrypt, custo 10) leva ~100 ms na sua maquina"); Serial.println(" e tem sal aleatorio embutido. Este aqui volta em segundos, sempre igual,"); Serial.println(" e da para fazer um dicionario de senha contra ele."); } void loop() { time_t agora = time(nullptr); unsigned long restantes = segundosRestantes(agora); Serial.println("--- verificando o token ---"); if (tokenAindaValido(agora)) { digitalWrite(PIN_LED, HIGH); unsigned long horas = restantes / 3600; unsigned long minutos = (restantes % 3600) / 60; Serial.print(" token VALIDO, faltam "); Serial.print(horas); Serial.print("h "); Serial.print(minutos); Serial.println(" min para pedir login de novo"); } else { digitalWrite(PIN_LED, LOW); Serial.println(" token EXPIRADO. a placa volta a esperar login."); Serial.println(" e este e o ganho do token: nada de senha travada no firmware"); Serial.println(" para sempre, e nada de sessao que nunca expira."); } Serial.println(); delay(6000); }
Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia02 aula1.
Aula 2 — Sessão e token: manter o usuario logado
Objetivos
- Desenhar a sessão de um usuário e de um disposi a um reinício e o que não sobrevive.
- Explicar por que guardar a sessão em memória no servidor perde todo mundo no primeiro reinício, e o que a tabela de sessão resolve.
- Distinguir cookie
httpOnlyde cookiesecure, e dizer qual dos dois protege contra qual roubo. - Rodar na placa o roteiro de cinco passos que separa token em RAM de token em flash, e ler no monitor serial o que cada um devolve depois do corte de energia.
- Apontar, no sketch desta aula, a linha em que o programa afirma uma coisa e imprime outra.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 LED da placa no
GPIO2, com o resistor de 330 ohm da bancada - 1 computador com o projeto do 2o trimestre aberto, no arquivo de login
- Folha de papel por dupla, com as duas colunas "memória" e "memória persistente"
- 1 computador com o monitor serial aberto a 115200
- Projetor, para o monitor serial e o terminal do Node lado a lado
Conceitos
Sessão é a memória que o servidor faz do login
Uma sessão é o registro de que determinada pessoa autenticou e continua autenticada. Ela nasce no login, existe enquanto a pessoa navega, e morre no logout ou no fim da validade. A palavra session O mesmo conceito no código aparece com o nome session, e a distinção entre os dois nomes é só de idioma.
O que uma sessão precisa guardar responde a três perguntas, e a turma escreve as três na lousa antes de qualquer código:
- Quem autenticou — o identificador da pessoa ou do dispositivo.
- Quando autenticou — para poder expirar.
- Como revogo — para poder encerrar antes da validade.
Um sistema que responde as três tem sessão. Um sistema que responde só a primeira está guardando um booleano, e o dia 8 do primeiro trimestre já mostrou o que acontece com o booleano: a pessoa navega, o token expira, e ninguém entende por que o painel voltou para a tela de login sem erro nenhum.
A sessão do dispositivo é a mesma coisa com outro nome. Quando a placa autentica, o que o servidor guarda é uma sessão da placa: identificador, instante da autenticação e validade. E é por isso que a diferença entre sessão e token é uma diferença de guarda, não de conceito: o token é a chave que a pessoa carrega, e a sessão é o registro que o servidor tem dessa chave.
Guardar sessão em memória é a opção mais rápida, e ela é criar um Map de sessão no processo do servidor: um objeto que associates um identificador a um dado, e que se consulta a cada requisição. É cinco linhas de código, funciona na hora, e o primeiro teste da aula passa com nota cheia.
O problema, chamado de perder sessão no reinício, aparece na primeira queda do processo. Quando o processo do Node é encerrado — por um reinício da máquina, por um deploy, por um erro não tratado que derrubou o processo — o Map vai junto, e a lista de quem estava logado vai junto. Quem estava com o painel aberto recebe 401 na próxima requisição e volta para a tela de login. Não há mensagem de erro, não há dado perdido, e do ponto de vista de quem está usando o sistema parece que "a sessão caiu sozinha".
O que o professor mede nesta aula é a janela dessa perda. Um servidor que reinicia por causa de atualização de biblioteca acontece uma vez por mês; um servidor com gerenciador de processo que reinicia porque um pedido deu erro acontece algumas vezes por dia. Cada uma dessas vezes custa um login para todo mundo que estava logado, e o usuário não sabe que existe um Relationship entre a queda de uma conexão com a internet e o fato de o servidor ter reiniciado.
A defesa tem duas partes, e a segunda é a que se esquece. A primeira é não confiar no processo: gravar a sessão fora dele. A segunda é assumir que o processo vai cair mesmo assim e fazer o sistema se comportar bem quando isso acontecer — que é o que o dia 7 do trimestre vai mostrar com a fila, e o que o dia 8 vai mostrar com o log.
A tabela de sessão, e o que ela resolve que a memória não resolve
A tabela de sessão resolve os dois problemas de uma vez, e é a resposta que o projeto do curso usa. A sessão persistente resolve os dois problemas de uma vez, e é a resposta que o projeto do curso usa: cada linha da tabela de sessão tem o identificador do token, o identificador de quem autenticou, o instante de criação e o instante de expiração. A sessão não está mais na memória do processo: ela está em uma tabela, e o processo apenas a consulta.
A consequência é direta: quando o servidor reinicia, a tabela continua lá. Quem estava logado continua logado, e o único efeito visível é que cada requisição passou a bater no banco em vez de bater na memória. Esse é o preço, e o professor manda a turma escrever o preço: persistência custa uma consulta por requisição. Uma consulta com índice é rápida; uma consulta sem índice em tabela grande é lenta; e a tabela cresce a cada login e nunca é limpa sozinha, o que leva ao dia 6 do trimestre, quando a migração vira a aula.
A limpeza é a parte que ninguém escreve e todos precisam. A tabela precisa de uma rotina que apague as linhas expiradas, porque um token expirado continua na tabela até alguém apagá-lo. A rotina pode rodar de hora em hora, e o efeito de não rodá-la é simples e mensurável: a tabela cresce sem parar e a consulta de sessão começa a demorar mais a cada semana.
O desenho da decisão cabe em uma frase, e é a frase que o professor pede no fim da aula: sessão em memória é o que se faz para mostrar que funciona; sessão em tabela é o que se faz para o sistema funcionar depois de uma noite. O teste que separa as duas é o corte de luz do item 5.
A sessão persistente resolve os dois problemas de uma vez, e é a resposta que o projeto do curso usa.
O token é o valor que a pessoa carrega para provar que está autenticada, e a distinção da aula anterior continua valendo: ele é opaco, tem validade e pode ser revogado. O que muda aqui é o transporte do token, e é aí que entram o cookie e o cabeçalho.
O cookie é o mecanismo do navegador para guardar esse valor e reenviá-lo sozinho a cada requisição. As duas qualidades que importam são o httpOnly e o secure, e elas impedem coisas diferentes:
- cookie httpOnly: o JavaScript da página não consegue ler o cookie. Com ele ligado, um
XSS— a injeção de script que o dia 9 do trimestre vai detalhar — não consegue roubar o token, porque o script não enxerga onde ele está. Sem ele, qualquer script que rode na página lê o cookie e manda para o servidor dele. - cookie secure: o cookie só é enviado por conexão criptografada. Com ele ligado, o token nunca viaja em claro pela rede. Sem ele, ele viaja, e qualquer pessoa na mesma rede o lê.
As duas defesas são complementares e não se substituem: o httpOnly protege contra o script na página, e o secure protege contra a rede. Um navegador aceita cookie httpOnly sem secure — e aí o token está protegido contra um ataque e exposto ao outro. O professor insere essa frase porque a turma costuma achar que "põei httpOnly e pronto".
O JWT, ou JSON Web Token, é a alternativa ao token guardado no servidor, e o professor a menciona com a ressalva que ela merece. Um JWT carrega dentro de si o identificador, a validade e a assinatura, e assinar token é o que permite ao servidor conferir sem consultar nada: a assinatura prova que foi o servidor que montou o token, e não alguém que o leu e mudou. O ganho é o mesmo que a tabela de sessão promete, sem a consulta por requisição. O custo é que o token expirado continua válido até a data escrita nele, e revogar antes disso exige uma lista de revogação que anula o ganho. A escolha entre JSON Web Token e token de sessão é uma escolha de requisito, e não de preferência.
O roubo de token é o ataque contra o qual as duas qualidades do cookie existem, e o roteiro é o mesmo dos dois lados: alguém obtém o valor, usa como se fosse a pessoa, e o sistema não tem como saber que é mentira. É por isso que a validade é curta e por isso que o HTTPS é obrigatório. A aula 2 do dia 9 volta a esse ponto com o transporte.
Renovar token: o que muda para a pessoa e o que muda para o código
Expirar token é o fim da validade, e renovar token é trocar o token que está quase no fim que está quase expirando por um novo, sem obrigar a pessoa a logar de novo. É a operação que faz a validade curta ser aceitável, e ela tem uma decisão de projeto que o professor pede explicitamente: renovar por conta própria ou por refresh token.
A forma simples é a placa detectar que está com pouco tempo e pedir um novo. O refresh token é a forma robusta: um segundo token, de validade muito maior, que existe só para emitir tokens novos. O primeiro é curto e vai no cookie httpOnly e secure; o segundo é mais longo e vai para um lugar mais protegido, e a maioria das implementações o entrega pela rota de refresh em vez de guardá-lo no cookie. A razão é o custo do roubo: quem rouba o token curto tem minutos; quem rouba o de renovação tem meses.
Encerrar sessão é a operação oposta e é a que o dia 3 vai cobrar. Logout não é "esquecer o estado no navegador": é apagar a linha da sessão no servidor. Sem apagar, o token continua válido até expirar, e a pessoa que saiu do computador continua com acesso. Esse é o logout que funciona, e a diferença entre os dois aparece no item 7 da atividade.
O token expirado tem um comportamento que o professor descreve como o mais importante do ponto de vista da placa: quando o servidor devolve 401, a placa não tenta de novo com o mesmo token. Ela apaga o token, avisa que precisa de login, e para. Reenviar com o mesmo token é o caminho que transforma um problema de cinco segundos em um problema de bateria.
memset no token, e a linha que compara a variável com ela mesma
O sketch desta aula tem a cena que dá nome à aula: um corte de energia simulado, com o token em RAM zerado e o token em flash intacto. A função simularReinicio faz o memset do arranjo que está em RAM, e o comentário explica que na placa de verdade o processo é o loop parar e o setup rodar de novo.
A parte que o professor pede para ler com calma está no quarto passo, e ela tem um defeito real. A linha que escreve o que sobrou depois do corte é uma chamada de impressão cuja expressão de valor é tokenPresente ? "nada" : "nada". As duas alternativas são a mesma string, e a condição não muda o resultado em nenhum caso. O programa imprime "nada" sempre, inclusive no caso em que o token no flash sobreviveu e poderia ter sido mostrado.
Isso é o que se chama de condição que não testa nada, e é uma classe de defeito que a turma vai encontrar o ano inteiro: comparação de valor consigo mesmo, variável calculada e nunca usada, condição cuja resposta é a mesma nos dois braços. O sintoma é um programa que funciona — compila, não dá erro, não avisa — e que mente sobre uma informação. A única forma de pegar é ler a expressão, e por isso o professor lê em voz alta na frente da turma.
A correção cabe em trocar a condição, e a versão que o professor prefere mostra o que aconteceu de verdade: o ramVazio com o resultado de uma comparação do arranjo de RAM com um arranjo zerado, e o flashIntacto com a comparação do arranjo persistido com o valor esperado. Feito isso, o mesmo passo passa a imprimir os dois estados, e a diferença entre RAM e flash fica visível em uma linha de monitor em vez de ser afirmada em um comentário.
O memset merece uma explicação própria, porque ele é a linha que executa a comparação. Ele preenche um bloco de memória com um valor, e é a forma correta de zerar um arranjo de bytes. A alternativa — um laço com for — funciona e é mais lenta, e em C++ a biblioteca padrão já oferece a versão rápida. O ponto para a turma reter é o escopo: o que o memset apaga é a variável que está na memória de trabalho, e nada que esteja gravado no programa. É por isso que o LED do sketch volta a acender depois do corte, e é por isso que o arranjo const do flash continua idêntico.
Atividade
Montagem:
LEDda placa noGPIO2, com o resistor de 330 ohm e o jumper para oGND.- Cabo USB conectado, monitor serial em 115200.
- Nenhum sensor nesta aula: o objeto do teste é o armazenamento, e um sensor só adicionaria variação ao tempo.
- Terminal do Node aberto, com o arquivo de sessão do projeto em uma aba.
- Escreva no papel as três perguntas da sessão — quem autenticou, quando autenticou, como revogo — e ao lado de cada uma escreva onde a resposta está guardada no seu projeto hoje. Marque com um círculo as perguntas que hoje não têm resposta.
- No seu projeto, ache a estrutura que guarda a sessão. Ela é um objeto na memória do processo ou uma tabela no banco? Escreva o nome do arquivo e a linha onde ela é criada. Diga em uma frase o que acontece com ela quando o processo reinicia.
- Grave o sketch da resolução e rode por pelo menos vinte segundos, o que cobre os cinco passos. Copie no caderno a linha do passo 1, a do passo 4 e a do passo 5.
- Anote o que o passo 4 imprime depois da palavra "sobrou". Agora leia a expressão que produz esse texto e escreva quantas Information diferentes ela pode produzir. QuantasInformation você esperava?
- Troque a expressão por uma que teste de verdade os dois arranjos, um na RAM e outro no flash, e grave de novo. O que mudou no passo 4? Escreva as duas Information que agora aparecem.
- Desligue e reconecte o cabo USB da placa com o sketch rodando. O que a placa imprime no banner que mostra que ela reiniciou? No seu projeto, o que acontece com a sessão da pessoa nesse mesmo instante?
- Escreva no papel a diferença entre duas coisas: sair da tela de login e encerrar a sessão. Para cada uma, escreva o que acontece com o token da pessoa e com a linha da sessão no servidor.
- Escreva em uma frase por linha as respostas de duas perguntas: por que o cookie
httpOnlynão protege o token no caminho da rede, e por que osecurenão protege o token de um script na página.
Nota: 10 pontos. Critério de fim: as três linhas do monitor copiadas dos passos 1, 4 e 5, e as duas informações do passo 4 anotadas antes e depois da correção da expressão.
Resolucao
O sketch da aula está em codigo/t3/dia02/aula2.ino.
// Aula 2 do 3o trimestre: sessao e token na placa. // // A placa guarda o token em memoria RAM, e a RAM some quando a energia acaba. // Quando ela volta, a sessao acabou. E limitacao, nao defeito: e o que acontece // com todo dispositivo que nao tem armazenamento. A placa mostra as duas // situacoes lado a lado. #include <Arduino.h> #include <time.h> const int PIN_LED = 2; const unsigned long PASSO_MS = 3000; // Token que o servidor entregou no login. uint8_t tokenEmMemoria[16] = { 0x51, 0x2c, 0x88, 0xf0, 0x64, 0xb3, 0x1d, 0x8a, 0xe5, 0x07, 0xc9, 0x42, 0x76, 0xbe, 0x30, 0x5d }; // O MESMO token gravado em memoria nao volatil: sobrevive ao corte de energia. // A placa nao tem isso nativamente; o sketch mostra a diferenca de conceito. const uint8_t TOKEN_PERSISTIDO[16] = { 0x51, 0x2c, 0x88, 0xf0, 0x64, 0xb3, 0x1d, 0x8a, 0xe5, 0x07, 0xc9, 0x42, 0x76, 0xbe, 0x30, 0x5d }; const unsigned long TOKEN_EMITIDO_EM = 1758000000UL; const unsigned long VALIDADE_SEGUNDOS = 2UL * 60UL * 60UL; // Cookie httpOnly e uma coisa do navegador. Na placa nao existe cookie: // quem decide o que a placa pode fazer e o SERVIDOR, quando o token chega. bool tokenPresente = true; unsigned long restantesDe(time_t agora) { unsigned long idade = (unsigned long)agora - TOKEN_EMITIDO_EM; if (idade >= VALIDADE_SEGUNDOS) return 0; return VALIDADE_SEGUNDOS - idade; } void mostrarToken(const uint8_t* t, const char* rotulo) { Serial.print(rotulo); Serial.print(": "); for (int i = 0; i < 16; i++) { if (t[i] < 0x10) Serial.print('0'); Serial.print(t[i], HEX); } Serial.println(); } // Simula o corte de energia: zera a RAM, mantem o flash. void simularReinicio() { Serial.println(); Serial.println(">>> CORTE DE ENERGIA. a placa reinicia em 2 segundos <<<"); Serial.println(); // Na placa de verdade, o loop para e o setup roda de novo. Aqui o que // acontece e: o token em RAM e perdido. O persistido continua. memset(tokenEmMemoria, 0, sizeof(tokenEmMemoria)); tokenPresente = false; } int passo = 0; int totalPassos = 5; void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); Serial.println(); Serial.println("Dia 02 aula 2 — sessao e token: manter o usuario logado"); Serial.println(); Serial.println("Duas formas de guardar o mesmo token:"); Serial.println(" RAM (tokenEmMemoria) — se perde quando a energia acaba"); Serial.println(" flash (TOKEN_PERSISTIDO) — sobrevive ao corte de energia"); Serial.println(); Serial.println("O que muda para o usuario: com token em RAM, cada corte de luz"); Serial.println("e um login novo. Com token em flash, so expira em 2 horas."); Serial.println(); Serial.println("O que muda para o SERVIDOR: nada. Ele so valida o token."); Serial.println("Guardar o token e responsabilidade da placa; conferir e do servidor."); } void loop() { time_t agora = time(nullptr); unsigned long rest = restantesDe(agora); switch (passo) { case 0: Serial.println("--- 1. a placa acorda com a sessao ativa ---"); mostrarToken(tokenEmMemoria, " token em RAM"); mostrarToken(TOKEN_PERSISTIDO, " token no flash"); Serial.printf(" validade restante: %lus\n", rest); Serial.println(" a placa envia o token em toda requisicao; o servidor compara."); break; case 1: Serial.println("--- 2. o que acontece se o token for roubado ---"); Serial.println(" alguem le o token do trafego HTTP (que NAO e HTTPS)."); Serial.println(" o ladrão tem 2 horas de acesso com aquele token."); Serial.println(" e por isso que HTTPS nao e opcional: sem ele o token viaja a vista."); Serial.println(" o que o HTTPS protege: o token em transito, nao o token guardado."); break; case 2: Serial.println("--- 3. o token expira ---"); Serial.println(" apos 2 horas o servidor devolve 401, mesmo com token valido na placa."); Serial.println(" a placa apaga o token e volta a tela de login."); Serial.println(" isso e RENOVACAO: a placa pega um token novo e continua."); break; case 3: Serial.println("--- 4. corte de energia ---"); if (tokenPresente) { Serial.println(" a energia acabou com o token na RAM."); Serial.printf(" depois do corte, em RAM sobrou: %s\n", tokenPresente ? "nada" : "nada"); Serial.println(" no flash o token continua la. Se ainda nao expirou, a placa"); Serial.println(" volta conectada sem pedir login de novo."); simularReinicio(); } else { Serial.println(" (a placa ja reiniciou e perdeu o token da RAM)"); } break; case 4: Serial.println("--- 5. a placa volta a pedir login ---"); Serial.println(" sem token em RAM, a placa envia uma requisicao SEM token."); Serial.println(" o servidor responde 401. A placa sabe que precisa de login."); digitalWrite(PIN_LED, LOW); Serial.println(); Serial.println("CONCLUSAO: a placa guarda token, nunca senha."); Serial.println(" A limitation real de IoT e a memoria: token em RAM nao sobrevive"); Serial.println(" a queda de energia. Guardar em flash e possivel, mas quem decide"); Serial.println(" ainda e o servidor, pela validade. A placa nao confia em si mesma."); break; } digitalWrite(PIN_LED, (tokenPresente && rest > 0) ? HIGH : LOW); passo = (passo + 1) % totalPassos; delay(PASSO_MS); }
Por que assim e não de outro jeito. O sketch existe para mostrar as duasformas de guardar o mesmo token, e ele faz isso declarando os dois em vez de escolher um. tokenEmMemoria é um arranjo de bytes comum, e TOKEN_PERSISTIDO é um arranjo const. Os dois têm o mesmo conteúdo — o professor pode comparar os bytes no monitor e ver que são idênticos — e o que muda é o que acontece com eles quando a energia acaba. É a demonstração mais direta possível de que a diferença entre perder e não perder não é de código, é de armazenamento.
A linha de comentário que diz que a placa não tem memória persistente nativamente é verdadeira e o professor a completa em voz alta, porque é o que vai aparecer na aula do dia 6: a placa tem memória não volátil, e ela se usa pela biblioteca Preferences, que grava em uma área da flash separada do programa. O sketch desta aula não usa a biblioteca para manter a aula sem rede e sem setup longo, e o resultado visual é o mesmo: os dois tokens continuam impressos na tela depois do corte.
A função restantesDe é uma cópia quase idêntica da segundosRestantes do sketch da aula 1, e a duplicação é deliberada. As duas aulas são independentes no eixo, e o material não promete que um arquivo dependa do anterior. O que o professor aponta é a consequência: essa mesma função tem o mesmo defeito de unsigned nas duas aulas, e por isso o item 4 da atividade existe nas duas.
O switch de cinco casos é a estrutura da aula, e cada caso corresponde a um momento do roteiro: acorda com sessão ativa, token roubado, token expirado, corte de energia, e volta a pedir login. A ordem importa e o professor explica: o corte de energia vem antes da volta a pedir login, porque é o corte que produz essa necessidade. Com a ordem trocada, o roteiro contaria a consequência antes da causa, e a turma leria um conjunto de Symptoms e não uma sequência.
O digitalWrite no fim do loop acende o LED quando há token presente e ainda resta validade. Esse AND entre as duas condições é a regra de exibição do painel escrita em uma linha: sem token não há painel, e com token vencido também não há painel. E é a mesma regra que a aula 1 do dia 3 vai pedir na checagem de permissão: estar autenticado e ter permissão são duas coisas, e o painel só aparece quando as duas são verdadeiras.
O desvio medido, e a linha que imprime "nada" sem testar nada. Rodando o sketch como está, o quarto passo imprime a linha depois do corte, em RAM sobrou: nada — e a expressão que produz esse texto é tokenPresente ? "nada" : "nada". As duas alternativas da condição são idênticas, então o operador ternário devolve "nada" nos dois casos. O programa não está errado do ponto de vista do compilador, que aceita a expressão sem aviso, e o resultado na tela parece correto: depois de um corte de energia, o token que estava na RAM realmente não sobreviveu.
O que o texto mente é sobre o motivo. A linha sugere que a placa conferiu que não sobrou nada na RAM, e ela não conferiu nada — ela imprimiu uma constante. Se alguém mudar o código para que o token sobreviva à RAM, ou se o corte passar a acontecer depois da placa ter guardado o token em outro lugar, a frase continua dizendo "nada" e o diagnóstico passa a ser falso. É a mesma classe do token vencido da aula anterior: um programa que funciona e informa errado.
A correção que o professor escreve na frente da turma usa a biblioteca, e não um laço. memcmp compara dois blocos de memória e devolve zero quando são iguais, o que transforma a comparação em uma pergunta booleana legível. Com ramVazio e flashIntacto calculados antes da impressão, o passo passa a mostrar as duas informações, e a frase "depois do corte, em RAM sobrou" volta a ser uma afirmação que o professor pode usar para corrigir alguém.
O que o LED acende e apaga ao longo dos cinco passos é a parte que o professor usa para fechar sem olhar o monitor: ele acende no primeiro passo, apaga depois do corte, e fica apagado no quinto. A curva da luz ao longo do roteiro é o resumo visual da aula — o que está na memória do processo acaba, e o que está gravado fica.
Criterios de correcao
| Critério | Pontos |
|---|---|
| As três perguntas da sessão escritas, com o lugar onde a resposta está guardada | 2 pontos |
| A estrutura de sessão localizada no projeto, com arquivo, linha e o efeito do reinício | 2 pontos |
| As três linhas do monitor copiadas, dos passos 1, 4 e 5 | 1 ponto |
| A quantidade de informações que a expressão do passo 4 pode produzir, com o número esperado | 2 pontos |
| As duas informações do passo 4 depois da correção da expressão | 1 ponto |
| Item 7: a diferença entre sair da tela e encerrar a sessão, com o destino do token | 1 ponto |
Item 8: as duas frases sobre httpOnly e secure | 1 ponto |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
Sessão em Map na memória do processo | const sessoes = new Map(); e todo mundo desloga quando o servidor reinicia | "Sessão em memória funciona até o primeiro reinício. Grave em tabela: identificador, dono, criação e expiração. O custo é uma consulta por requisição, e o preço de um logout em massa é maior." |
| Token com validade de um mês, para não deslogar | Todo mundo continua logado por semanas | "Validade longa multiplica o estrago de um roubo. Com renovação automática, a validade curta não incomoda ninguém: a pessoa nunca percebe." |
Cookie sem httpOnly | document.cookie e o token legível no console | "httpOnly tira o token do alcance do JavaScript da página. Sem ele, o primeiro XSS que roda na sua página lê o cookie e manda para o servidor de quem quiser." |
Cookie sem secure | Token viaja em claro quando alguém abre a página por http | "secure prende o token ao transporte criptografado. httpOnly e secure resolvem problemas diferentes: um é o script da página, o outro é a rede." |
| Revogar token só no navegador | O logout limpa o cookie e não toca no servidor | "Logout que não apaga a sessão no servidor deixa acesso válido até expirar. Apague a linha da sessão, e invalidate o token no servidor." |
| Reenviar o mesmo token depois do 401 | A placa entra em laço de reenvio e acaba a bateria | "401 é recusa, não atraso. Apague o token, avise que precisa de login e pare. Gastar bateria refazendo pergunta que já tem resposta é a pior forma de esperar." |
| Renovar a cada requisição | Toda requisição recebe token novo e o servidor gasta uma escrita a cada chamada | "Renove só perto do fim da validade. Renovar a cada requisição troca um custo pequeno por um custo grande sem ganho nenhum." |
| Comparar variável com ela mesma | condicao ? "nada" : "nada" e a tela sempre mostra o mesmo texto | "Os dois braços iguais quer dizer que a condição não decide nada. Escreva a pergunta de verdade — o arranjo está vazio? — e compare com o valor esperado." |
Desafio extra
Meça, no seu projeto, quanto tempo uma consulta de sessão na tabela custa em relação a uma busca no Map da memória. Rode o mesmo caminho duzentas vezes e anote os dois números em milissegundos. Depois rode duzentas requisições ao mesmo tempo, com dez usuários, e veja o que acontece com o número. Escreva na sua frente uma frase decidindo se vale trocar a sessão em tabela pela sessão em JWT assinado, e o que você precisaria acrescentar ao sistema para poder revogar um JWT antes de ele expirar. A pergunta que o professor espera de resposta: se o servidor pode revogar instantaneamente, o JWT ainda economiza alguma coisa, e qual?
A resolucao, compilada
// Aula 2 do 3o trimestre: sessao e token na placa. // // A placa guarda o token em memoria RAM, e a RAM some quando a energia acaba. // Quando ela volta, a sessao acabou. E limitacao, nao defeito: e o que acontece // com todo dispositivo que nao tem armazenamento. A placa mostra as duas // situacoes lado a lado. #include <Arduino.h> #include <time.h> const int PIN_LED = 2; const unsigned long PASSO_MS = 3000; // Token que o servidor entregou no login. uint8_t tokenEmMemoria[16] = { 0x51, 0x2c, 0x88, 0xf0, 0x64, 0xb3, 0x1d, 0x8a, 0xe5, 0x07, 0xc9, 0x42, 0x76, 0xbe, 0x30, 0x5d }; // O MESMO token gravado em memoria nao volatil: sobrevive ao corte de energia. // A placa nao tem isso nativamente; o sketch mostra a diferenca de conceito. const uint8_t TOKEN_PERSISTIDO[16] = { 0x51, 0x2c, 0x88, 0xf0, 0x64, 0xb3, 0x1d, 0x8a, 0xe5, 0x07, 0xc9, 0x42, 0x76, 0xbe, 0x30, 0x5d }; const unsigned long TOKEN_EMITIDO_EM = 1758000000UL; const unsigned long VALIDADE_SEGUNDOS = 2UL * 60UL * 60UL; // Cookie httpOnly e uma coisa do navegador. Na placa nao existe cookie: // quem decide o que a placa pode fazer e o SERVIDOR, quando o token chega. bool tokenPresente = true; unsigned long restantesDe(time_t agora) { unsigned long idade = (unsigned long)agora - TOKEN_EMITIDO_EM; if (idade >= VALIDADE_SEGUNDOS) return 0; return VALIDADE_SEGUNDOS - idade; } void mostrarToken(const uint8_t* t, const char* rotulo) { Serial.print(rotulo); Serial.print(": "); for (int i = 0; i < 16; i++) { if (t[i] < 0x10) Serial.print('0'); Serial.print(t[i], HEX); } Serial.println(); } // Simula o corte de energia: zera a RAM, mantem o flash. void simularReinicio() { Serial.println(); Serial.println(">>> CORTE DE ENERGIA. a placa reinicia em 2 segundos <<<"); Serial.println(); // Na placa de verdade, o loop para e o setup roda de novo. Aqui o que // acontece e: o token em RAM e perdido. O persistido continua. memset(tokenEmMemoria, 0, sizeof(tokenEmMemoria)); tokenPresente = false; } int passo = 0; int totalPassos = 5; void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); Serial.println(); Serial.println("Dia 02 aula 2 — sessao e token: manter o usuario logado"); Serial.println(); Serial.println("Duas formas de guardar o mesmo token:"); Serial.println(" RAM (tokenEmMemoria) — se perde quando a energia acaba"); Serial.println(" flash (TOKEN_PERSISTIDO) — sobrevive ao corte de energia"); Serial.println(); Serial.println("O que muda para o usuario: com token em RAM, cada corte de luz"); Serial.println("e um login novo. Com token em flash, so expira em 2 horas."); Serial.println(); Serial.println("O que muda para o SERVIDOR: nada. Ele so valida o token."); Serial.println("Guardar o token e responsabilidade da placa; conferir e do servidor."); } void loop() { time_t agora = time(nullptr); unsigned long rest = restantesDe(agora); switch (passo) { case 0: Serial.println("--- 1. a placa acorda com a sessao ativa ---"); mostrarToken(tokenEmMemoria, " token em RAM"); mostrarToken(TOKEN_PERSISTIDO, " token no flash"); Serial.printf(" validade restante: %lus\n", rest); Serial.println(" a placa envia o token em toda requisicao; o servidor compara."); break; case 1: Serial.println("--- 2. o que acontece se o token for roubado ---"); Serial.println(" alguem le o token do trafego HTTP (que NAO e HTTPS)."); Serial.println(" o ladrão tem 2 horas de acesso com aquele token."); Serial.println(" e por isso que HTTPS nao e opcional: sem ele o token viaja a vista."); Serial.println(" o que o HTTPS protege: o token em transito, nao o token guardado."); break; case 2: Serial.println("--- 3. o token expira ---"); Serial.println(" apos 2 horas o servidor devolve 401, mesmo com token valido na placa."); Serial.println(" a placa apaga o token e volta a tela de login."); Serial.println(" isso e RENOVACAO: a placa pega um token novo e continua."); break; case 3: Serial.println("--- 4. corte de energia ---"); if (tokenPresente) { Serial.println(" a energia acabou com o token na RAM."); Serial.printf(" depois do corte, em RAM sobrou: %s\n", tokenPresente ? "nada" : "nada"); Serial.println(" no flash o token continua la. Se ainda nao expirou, a placa"); Serial.println(" volta conectada sem pedir login de novo."); simularReinicio(); } else { Serial.println(" (a placa ja reiniciou e perdeu o token da RAM)"); } break; case 4: Serial.println("--- 5. a placa volta a pedir login ---"); Serial.println(" sem token em RAM, a placa envia uma requisicao SEM token."); Serial.println(" o servidor responde 401. A placa sabe que precisa de login."); digitalWrite(PIN_LED, LOW); Serial.println(); Serial.println("CONCLUSAO: a placa guarda token, nunca senha."); Serial.println(" A limitation real de IoT e a memoria: token em RAM nao sobrevive"); Serial.println(" a queda de energia. Guardar em flash e possivel, mas quem decide"); Serial.println(" ainda e o servidor, pela validade. A placa nao confia em si mesma."); break; } digitalWrite(PIN_LED, (tokenPresente && rest > 0) ? HIGH : LOW); passo = (passo + 1) % totalPassos; delay(PASSO_MS); }
Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia02 aula2.
