Seguranca além de SQL injection — Arduino e IoT — semana 9 do 3o trimestre
Semana 9 de 15· 3o trimestre · 05/09 a 10/12
Seguranca além de SQL injection
O resto do que uma coisa conectada precisa e ainda não tem.
Aula 1 — Entrada não confiavel em toda parte
Objetivos
- Apontar, num sistema do ano, os quatro lugares onde a entrada não confiável entra: sensor, rede, formulário e resposta do servidor.
- Reconhecer no painel do projeto o ponto exato em que uma leitura vira
innerHTML, trocar portextContente nomear o defeito: XSS. - Escapar a saída em dois destinos diferentes, o texto que vira
JSONe o texto que vira página, e explicar por que a lista de caracteres muda de um para o outro. - Tratar a recusa do servidor por código de status, em vez de repetir o envio como se nada tivesse acontecido.
- Rodar
npm auditno projeto do trimestre e dizer, em uma frase, o que um número de vulnerabilidade muda no seu trabalho.
Material
- 1 ESP32 DevKit V1 por aluno, com o cabo USB
- 1 computador por dupla, com Node 20 ou superior
- Projeto do trimestre 3, com
package.jsonepackage-lock.jsonversionados - Terminal com
npminstalado, para onpm auditdo item 5 - O painel do projeto aberto em uma aba e o servidor em outra, para o teste de
CORSe deCSRF
Conceitos
Quatro origens, uma única regra
A entrada não confiável não é um problema do navegador nem do banco. É um problema de segurança de todo lugar por onde o dado entra, e o curso inteiro converge aqui. No segundo trimestre a SQL injection fechou a porta do banco: consulta parametrizada, e o banco deixou de interpretar dado como comando. O que sobrou foram as outras três portas.
| Origem | Quem manda | O que pode acontecer |
|---|---|---|
| Sensor | o device, com defeito | número fora do limite, texto no lugar de número, dado gigante |
| Formulário | a pessoa na tela | qualquer string, inclusive "; DROP TABLE e <script> |
| Resposta de servidor | a máquina do outro lado | resposta enorme, JSON inválido, código de erro inesperado |
| Cabeçalho | a rede intermediária | valor adulterado em caminho não confiável |
A regra é uma só, e ela vale para as quatro linhas: quem produz o dado não pode ser quem decide que o dado é válido. No banco isso virou consulta parametrizada. No JSON vira escapar. No HTML vira textContent.
XSS: injeção de HTML no navegador de quem está logado
O nome completo é cross-site scripting, a sigla é XSS, e o defeito tem um caráter anatômico: ele não acontece no servidor. O servidor recebe o dado, grava no banco, devolve a página e sai tudo certo do ponto de vista dele. O código malicioso roda depois, no navegador de outra pessoa, no momento em que o navegador le a resposta. Por isso ele escapa de todo teste que roda no servidor: o servidor, sozinho, não consegue ver o defeito.
O caminho tem quatro elos, e quebrar um basta:
- A pessoa escreve um valor no formulário.
- O servidor guarda sem validar o formato.
- O painel do projeto lê o valor e escreve num
innerHTML. - O navegador encontra
<script>e executa.
O elo 3 é o que se chama injeção de HTML, e o nome vem de ser exatamente o que é: o dado entrou dentro do HTML como se fosse markup. A defesa padrão do navegador é não depender do item 3: textContent escreve o valor como texto, e o navegador não interpreta nada. Quando o servidor precisa montar a página em string, ai entra escapar saída: cada caractere especial vira entidade, e o resultado é visualmente igual e semanticamente inerte.
Duas palavras que o aluno sempre troca. Validar decide se o dado tem o formato aceito. Sanitizar é reescrever o dado para tirar o que é perigoso. Validar e escapar são passos diferentes, com ordem: validar na entrada, escapar na saída. Quem só valida deixa passar o valor bem formado e ainda perigoso, como sala-01"><script>alert(1)</script>, que passa em qualquer regra de comprimento e de formato.
Regra do dia, e ela cabe numa linha: dado de usuário nunca entra em innerHTML. Nem "por enquanto", nem "esse campo é interno", nem "esse valor já passou pelo banco".
CORS: a origem, e CSRF: a requisição forjada
CORS não é proteção do servidor. É um pedido do navegador ao servidor, perguntando se outra origem pode ler a resposta. Quem protege o servidor é a checagem de permissão no próprio caminho do recurso, e a aula 2 do dia 3 já mostrou isso com o token.
O erro clássico é o asterisco. Access-Control-Allow-Origin: * junto com Access-Control-Allow-Credentials: true e recusado pelo navegador, porque significa "qualquer site do planeta, com o seu cookie de login". A forma correta tem duas partes: a origem permitida vem de configuração, CORS_ORIGIN=http://localhost:3000, e o servidor devolve exatamente aquele valor. localhost no desenvolvimento e o domínio próprio na produção, e a variável muda entre os dois sem tocar no código.
CSRF é o outro lado, e o nome completo dele é requisição forjada: um site que a pessoa abriu numa outra aba dispara um POST no seu servidor usando o cookie de sessão que o navegador já tem. O navegador age, e a pessoa não pediu nada. O token anti-CSRF resolve porque a requisição forjada não tem o token: o formulário traz o token no corpo e no cabeçalho, e o servidor compara. A segunda defesa é o cookie SameSite, que é o atributo do cabeçalho Set-Cookie que decide se o navegador acompanha o pedido para fora do próprio site. Lax sozinho resolve quase tudo; o token resolve o resto.
Dependência vulnerável, versão antiga e npm audit
npm audit lê o package-lock.json e cruza as versões com o banco de vulnerabilidades público. O comando que roda no projeto do trimestre é npm audit --omit=dev, porque devDependencies não vão para produção: um problema ali não affects o servidor em si.
O segundo item da saída importa mais que o primeiro. found 0 vulnerabilities e found 3 vulnerabilities dizem o que existe; o que muda o seu trabalho é a linha de cada pacote, com a versão antiga apontada e o caminho de correção. Uma dependência vulnerável em versão antiga não é problema de teoria: é a única forma de um resumo de CVE entrar no seu servidor, e ninguém te avisou.
O caminho de correção tem duas saídas. npm audit fix troca a versão e não olha seu código, então pode subir a versão maior e quebrar a API. A alternativa manual é trocar a linha do package.json para a faixa corrigida, rodar os testes, e só então aceitar. O professor roda o comando na frente da turma de propósito, mesmo com zero: o aluno precisa ver a ferramenta funcionando num projeto limpo antes de precisar dela num projeto sujo.
Atividade
Montagem: nenhuma. Esta aula roda inteira no monitor serial e no terminal, sem circuito. A placa só precisa do cabo USB.
- Abra o projeto do trimestre 3 e liste, no caderno, os quatro pontos de entrada da tabela do roteiro. Para cada um, escreva o nome do arquivo onde ele entra. Um ponto que você não conseguir nomear está quebrado.
- Grave o sketch da resolução e rode no monitor serial a 115200. Anote as três formas do mesmo valor: o texto cru, o texto depois de
escaparparaJSONe o texto depois deescaparHtmlpara página. - No painel do projeto, ache uma ocorrência de
innerHTMLque receba dado de usuário e troque portextContent. Se não houver nenhuma, escreva onde a saída do banco entra no HTML e diga por que lá ela não é perigosa. - Adicione um
escapeHtmlno servidor, no ponto exato em que a leitura vira linha de log, e mostre a diferença do log com uma leitura que contenha<b>. - Rode
npm audit --omit=devno projeto e anote o número de vulnerabilidades. Se for zero, escreva o comando que encontra uma dependência com problema conhecido. - Em três linhas: por que
Access-Control-Allow-Origin: *não protege o servidor, e o que o professor deveria checar no lugar dele. - Em três linhas: o que é uma requisição forjada, e qual defesa a placa do projeto não tem hoje.
Nota: 12 pontos. Critério de fim: o serial mostra o mesmo valor em três formas, uma crua e duas escapadas, e o npm audit rodou com o número anotado.
Resolucao
// Aula 1 do dia 9: entrada nao confiavel em toda parte. // // A placa e a fronteira do sistema. Tudo que chega nela vem de um sensor que // pode estar com defeito, de uma rede que qualquer um pode usar e de um // firmware que o dono le da loja. Nesta aula o firmware para de tratar a // entrada como se fosse boa. // // O codigo mostra a placa montando o pedido HTTP na mao, caractere a // caractere. E assim porque o aluno precisa VER onde entra a entrada nao // confiavel: se a biblioteca faz tudo, ele nao tem nada para escapar. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> // Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum // segredo real entra em sketch de exemplo, nem em material, nem no git. #define WIFI_SSID "lab-esp32-aula" #define WIFI_PASSWORD "troque-esta-pela-sua" // Endereco que existe so para o exemplo. A aula roda sem servidor: o que // importa e a montagem do pedido e o tratamento da resposta. const char* URL_SERVIDOR = "http://exemplo.invalido/leitura"; // O que a placa INVENTA no pedido: o identificador e a versao do firmware. const char* ID_DISPOSITIVO = "esp32-bancada-01"; const char* VERSAO_FIRMWARE = "1.4.0"; // Numero maximo de bytes que aceitamos na resposta. Um servidor que devolve // 50 MB de uma vez acaba com a memoria de uma placa que tem 320 KB de RAM. const unsigned long TAMANHO_MAXIMO_RESPOSTA = 2048; // Limite de bytes para qualquer campo que venha de fora. E o primeiro filtro: // antes de perguntar se o dado e valido, a placa recusa o que e grande demais // para caber nela. const unsigned int TAMANHO_MAXIMO_CAMPO = 32; WiFiClient cliente; // Um numero de sensor precisa ter digito, e no maximo um separador, e nenhum // espaco no meio. Escrever o check na mao e o ponto da aula: a biblioteca // faria mais rapido e o aluno nao veria o que esta sendo recusado. bool ehNumeroDeSensor(const String& texto) { if (texto.length() == 0 || texto.length() > TAMANHO_MAXIMO_CAMPO) { return false; } int separadores = 0; for (unsigned int i = 0; i < texto.length(); i++) { char c = texto.charAt(i); if (c == '.' || c == ',') { separadores++; if (separadores > 1) { return false; } continue; } if (c < '0' || c > '9') { return false; } } return true; } // ---------------------------------------------------------------- utilidades // Escapa um valor que veio de fora antes de entrar num texto que vai virar // JSON. Sem isto, um sensor que devolve 23.5" ou um campo chamado // <script> entra no arquivo de log como codigo, e nao como texto. String escapar(const String& entrada) { String saida; saida.reserve(entrada.length() + 16); for (unsigned int i = 0; i < entrada.length(); i++) { char c = entrada.charAt(i); if (c == '"' || c == '\\') { saida += '\\'; saida += c; } else if (c == '\n') { saida += "\\n"; } else if (c == '\r') { saida += "\\r"; } else if (c == '\t') { saida += "\\t"; } else if (c < 0x20) { saida += ' '; } else { saida += c; } } return saida; } // Segunda funcao, e a que o professor vai pedir no dia: escapar a saida de // HTML. E a defesa contra o defeito que o painel executa no NAVEGADOR de quem // esta logado, sem tocar no servidor. Repare que a lista de caracteres e maior // que a do JSON: entra tambem o apostrofo, porque o valor pode cair dentro de // um atributo com aspas simples. String escaparHtml(const String& entrada) { String saida; saida.reserve(entrada.length() + 32); for (unsigned int i = 0; i < entrada.length(); i++) { char c = entrada.charAt(i); switch (c) { case '<': saida += "<"; break; case '>': saida += ">"; break; case '&': saida += "&"; break; case '"': saida += """; break; case '\'': saida += "'"; break; default: saida += c; break; } } return saida; } // Monta o JSON na mao, campo a campo, escapando o que veio do sensor. A // biblioteca JSONResolver tambem escapa, e por isso ela e a escolha melhor no // projeto final; aqui a montagem e manual de proposito, para o aluno ver // exatamente onde a entrada entra. String montarPayload(const String& leituraBruta) { String payload = "{"; payload += "\"device_id\":\"" + String(ID_DISPOSITIVO) + "\","; payload += "\"firmware\":\"" + String(VERSAO_FIRMWARE) + "\","; payload += "\"leitura\":\"" + escapar(leituraBruta) + "\","; payload += "\"unidade\":\"C\""; payload += "}"; return payload; } // ------------------------------------------------------------------- a placa void setup() { Serial.begin(115200); delay(200); Serial.println(); Serial.println("=== dia 9, aula 1: entrada nao confiavel ==="); Serial.println("Nenhuma rede precisa estar de pe para rodar esta aula."); Serial.println(); // O sketch nao abre socket: instancia o cliente, que e o transporte HTTP do // exemplo, e configura o timeout. Tudo mais no codigo e sobre o que fazer // com o texto que chega depois. Serial.println("[placa] WiFi Client ativado."); cliente.setTimeout(3000); Serial.println("[placa] timeout do cliente: 3 s"); Serial.println(); // 1. Entrada que vem do sensor: tem formato esperado e tem limite. String sensorBom = "23.5"; String sensorRuim = "23.5C ou erro"; Serial.println("[sensor] caso bom: " + sensorBom + " -> aceito: " + String(ehNumeroDeSensor(sensorBom) ? "sim" : "nao")); Serial.println("[sensor] caso ruim: " + sensorRuim + " -> aceito: " + String(ehNumeroDeSensor(sensorRuim) ? "sim" : "nao")); Serial.println("[sensor] texto gigante de 40 bytes -> aceito: " + String(ehNumeroDeSensor(String(40, '9')) ? "sim" : "nao")); Serial.println(); // 2. Entrada que vem da rede: e a que o aluno nao controla. String campoDoUsuario = "sala-01\"><script>alert(1)</script>"; Serial.println("[rede] campo recebido de um formulario:"); Serial.println("[rede] " + campoDoUsuario); Serial.println("[rede] contem aspa: " + String(campoDoUsuario.indexOf("\"") >= 0 ? "sim" : "nao")); Serial.print("[rede] depois de escapar: "); Serial.println(escapar(campoDoUsuario)); Serial.print("[navegador] HTML injection, mesmo valor depois de escapar saida: "); Serial.println(escaparHtml(campoDoUsuario)); Serial.println("[navegador] o <script> virou texto: o navegador nao executa mais."); Serial.println(); // 3. O payload final, ja com tudo escapado. String payload = montarPayload(campoDoUsuario); Serial.println("[payload] JSON que iria para o servidor:"); Serial.println("[payload] " + payload); Serial.println("[payload] tamanho: " + String(payload.length()) + " bytes"); Serial.println(); // 4. O que a placa faz se o servidor nao responde. Esta e a parte que // o aluno esquece: construir o pedido e facil, tratar a falha e a aula. Serial.println("[envio] nenhuma requisicao real e feita nesta aula."); Serial.println("[envio] URL alvo: " + String(URL_SERVIDOR)); Serial.println("[envio] o servidor, na aula real, responderia com codigo de status."); Serial.println("[envio] 401 e 403 significam token recusado: a placa guarda o token"); Serial.println("[envio] e o servidor e que decidiu que ele nao vale mais."); Serial.println("[envio] a placa nao tem como corrigir isso sozinha: so avisar."); Serial.println(); // 5. O limite de resposta e a defesa contra a placa travar. Serial.println("[memoria] limite de resposta: " + String(TAMANHO_MAXIMO_RESPOSTA) + " bytes"); Serial.println("[memoria] sem esse limite, uma resposta enorme esvazia a RAM da placa."); Serial.println(); Serial.println("[placa] heap livre: " + String(ESP.getFreeHeap()) + " bytes"); Serial.println("[fim]"); } void loop() { delay(5000); Serial.println("[placa] viva. heap livre: " + String(ESP.getFreeHeap()) + " bytes"); }
Por que assim e não de outro jeito. O sketch não faz requisição nenhuma. Isso é deliberado, e o professor precisa dizer em voz alta: se a aula depende de um servidor no ar, metade da turma fica esperando e a outra metade desiste no primeiro erro de rede. O que o sketch faz é mostrar os três estados do mesmo dado, cru, recusado e escapado, e isso não depende de ninguém.
A montagem do JSON na mão é o segundo ponto. Existindo JSONResolver na plataforma, a escolha "correta" é usar a biblioteca; a escolha didática é montar na mão, porque o aluno precisa ver o ponto exato onde a entrada entra no texto. O professor diz isso explicitamente e aponta que no projeto final o JSONResolver volta.
A escaparHtml entra pelo mesmo motivo e por um segundo motivo pedagógico: as duas listas de caracteres são diferentes, e isso é o que a turma costuma errar. O JSON precisa de cinco escapes, e o HTML precisa de cinco também, mas não são os cinco mesmos: entra o apóstrofo, porque o valor pode cair dentro de um atributo com aspas simples. Mesmo código de estrutura, contexto diferente, resposta diferente.
O ehNumeroDeSensor existe pelo mesmo motivo que as duas funções de escape: nenhum dos três é a solução de produção, e os três são a única forma de o aluno ver a regra funcionando. A função tem um limite de tamanho antes da checagem de formato, e essa ordem é o ponto: o filtro barato vem antes do caro.
O TAMANHO_MAXIMO_RESPOSTA não é lenda: uma placa com 320 KB de RAM que aceita resposta sem limite trava quando o servidor errar a mão e devolver um HTML de página de erro de 2 MB. Limite de entrada é segurança, e vale para os dois lados.
A função escapar reserva memória com reserve porque String realoca a cada +=, e realocar cinco vezes num dispositivo com pouca RAM é o que faz o programa ficar lento sem erro nenhum.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Os quatro pontos de entrada listados com o nome do arquivo de cada um | 3 pontos |
| Serial mostra o valor cru e as duas versões escapadas, com a diferença explicada | 3 pontos |
innerHTML com dado de usuário trocado por textContent, ou justificativa de por que não havia | 2 pontos |
escapeHtml no servidor, com a diferença do log mostrada em uma leitura que contêm <b> | 2 pontos |
npm audit --omit=dev executado e número de vulnerabilidades anotado | 1 ponto |
Itens 6 e 7 respondidos: CORS não protege o servidor, e a requisição forjada está nomeada | 1 ponto |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
| Escapar só quando o valor "parece" perigoso | Código com if (texto.contains("<")) antes de escapar | "Escapa sempre. Se você precisa decidir se o valor é perigoso, você já perdeu: o valor novo vai burlar a sua condição." |
| Validar e não escapar, achando que validar protege a saída | O formulário aceita sala-01"><script>alert(1)</script> porque o valor "está no formato certo" | "Validar decide o formato, escapar decide a sobrevivencia no sistema que vai imprimir. O seu valor está no formato certo e continua sendo um ataque." |
escapeHtml aplicado duas vezes | O painel mostra <b> em vez de <b> | "Escapar duas vezes produz texto que parece escapado e não é. Escape uma vez, na saída." |
innerHTML porque "o dado já vem do banco" | O aluno argumenta que o banco não guarda <script> | "O dado passa por um formulário antes de chegar no banco, e esse é o caminho. Além disso: quem escreve no banco pode não ser quem você pensa." |
CSRF tratado como "assunto do navegador" | O aluno desabilita o token e diz que o navegador resolve | "O token anti-CSRF não é opção do navegador: ele não existe na requisição forjada. É o servidor que compara. Sem ele, o cookie sozinho entrega a sessão." |
Access-Control-Allow-Origin: * como "liberar o acesso" | O aluno pergunta como liberar a API para o painel | "Asterisco e navegador recusando credencial. O que você quer dizer é 'aceito o painel do domínio X', e o domínio X vai em variável de ambiente." |
npm audit sem --omit=dev | O aluno conta vulnerabilidade do nodemon como se fosse do servidor | "nodemon não sobe para produção. Use npm audit --omit=dev, ou o número não descreve o que roda no servidor." |
npm audit fix sem rodar os testes | O package.json sobe de versão maior e o painel quebra em outra tela | "O fix troca versão sem olhar seu código. Troque a faixa, rode os testes, e só então aceite." |
| Ignorar a resposta e repetir o envio | while (!resposta) { enviar(); } no sketch | "Gastar a bateria reenviando para um servidor que já disse não é o pior caso. 401 e recusa: pare e avise." |
| Nunca ouvir o status HTTP | O código lê o corpo e ignora o 201 e o 401 | "O status é a resposta. 201 gravou, 409 regra de negócio, 401 token recusado, 500 foi o servidor. O corpo explica; o status decide." |
Desafio extra
A escapar desta aula trata cinco caracteres do JSON, e a escaparHtml trata cinco do HTML, e nenhuma das duas é a lista completa. Escreva a versão que trata os dois conjuntos e calcule, no caderno, quantos bytes a mais o log ocupa por leitura quando todos os caracteres são escapados. Depois mude o servidor para textContent e meca quanto o painel carregou a menos. O número da segunda medição é o que você vai defender na apresentação do projeto.
A resolucao, compilada
// Aula 1 do dia 9: entrada nao confiavel em toda parte. // // A placa e a fronteira do sistema. Tudo que chega nela vem de um sensor que // pode estar com defeito, de uma rede que qualquer um pode usar e de um // firmware que o dono le da loja. Nesta aula o firmware para de tratar a // entrada como se fosse boa. // // O codigo mostra a placa montando o pedido HTTP na mao, caractere a // caractere. E assim porque o aluno precisa VER onde entra a entrada nao // confiavel: se a biblioteca faz tudo, ele nao tem nada para escapar. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> // Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum // segredo real entra em sketch de exemplo, nem em material, nem no git. #define WIFI_SSID "lab-esp32-aula" #define WIFI_PASSWORD "troque-esta-pela-sua" // Endereco que existe so para o exemplo. A aula roda sem servidor: o que // importa e a montagem do pedido e o tratamento da resposta. const char* URL_SERVIDOR = "http://exemplo.invalido/leitura"; // O que a placa INVENTA no pedido: o identificador e a versao do firmware. const char* ID_DISPOSITIVO = "esp32-bancada-01"; const char* VERSAO_FIRMWARE = "1.4.0"; // Numero maximo de bytes que aceitamos na resposta. Um servidor que devolve // 50 MB de uma vez acaba com a memoria de uma placa que tem 320 KB de RAM. const unsigned long TAMANHO_MAXIMO_RESPOSTA = 2048; // Limite de bytes para qualquer campo que venha de fora. E o primeiro filtro: // antes de perguntar se o dado e valido, a placa recusa o que e grande demais // para caber nela. const unsigned int TAMANHO_MAXIMO_CAMPO = 32; WiFiClient cliente; // Um numero de sensor precisa ter digito, e no maximo um separador, e nenhum // espaco no meio. Escrever o check na mao e o ponto da aula: a biblioteca // faria mais rapido e o aluno nao veria o que esta sendo recusado. bool ehNumeroDeSensor(const String& texto) { if (texto.length() == 0 || texto.length() > TAMANHO_MAXIMO_CAMPO) { return false; } int separadores = 0; for (unsigned int i = 0; i < texto.length(); i++) { char c = texto.charAt(i); if (c == '.' || c == ',') { separadores++; if (separadores > 1) { return false; } continue; } if (c < '0' || c > '9') { return false; } } return true; } // ---------------------------------------------------------------- utilidades // Escapa um valor que veio de fora antes de entrar num texto que vai virar // JSON. Sem isto, um sensor que devolve 23.5" ou um campo chamado // <script> entra no arquivo de log como codigo, e nao como texto. String escapar(const String& entrada) { String saida; saida.reserve(entrada.length() + 16); for (unsigned int i = 0; i < entrada.length(); i++) { char c = entrada.charAt(i); if (c == '"' || c == '\\') { saida += '\\'; saida += c; } else if (c == '\n') { saida += "\\n"; } else if (c == '\r') { saida += "\\r"; } else if (c == '\t') { saida += "\\t"; } else if (c < 0x20) { saida += ' '; } else { saida += c; } } return saida; } // Segunda funcao, e a que o professor vai pedir no dia: escapar a saida de // HTML. E a defesa contra o defeito que o painel executa no NAVEGADOR de quem // esta logado, sem tocar no servidor. Repare que a lista de caracteres e maior // que a do JSON: entra tambem o apostrofo, porque o valor pode cair dentro de // um atributo com aspas simples. String escaparHtml(const String& entrada) { String saida; saida.reserve(entrada.length() + 32); for (unsigned int i = 0; i < entrada.length(); i++) { char c = entrada.charAt(i); switch (c) { case '<': saida += "<"; break; case '>': saida += ">"; break; case '&': saida += "&"; break; case '"': saida += """; break; case '\'': saida += "'"; break; default: saida += c; break; } } return saida; } // Monta o JSON na mao, campo a campo, escapando o que veio do sensor. A // biblioteca JSONResolver tambem escapa, e por isso ela e a escolha melhor no // projeto final; aqui a montagem e manual de proposito, para o aluno ver // exatamente onde a entrada entra. String montarPayload(const String& leituraBruta) { String payload = "{"; payload += "\"device_id\":\"" + String(ID_DISPOSITIVO) + "\","; payload += "\"firmware\":\"" + String(VERSAO_FIRMWARE) + "\","; payload += "\"leitura\":\"" + escapar(leituraBruta) + "\","; payload += "\"unidade\":\"C\""; payload += "}"; return payload; } // ------------------------------------------------------------------- a placa void setup() { Serial.begin(115200); delay(200); Serial.println(); Serial.println("=== dia 9, aula 1: entrada nao confiavel ==="); Serial.println("Nenhuma rede precisa estar de pe para rodar esta aula."); Serial.println(); // O sketch nao abre socket: instancia o cliente, que e o transporte HTTP do // exemplo, e configura o timeout. Tudo mais no codigo e sobre o que fazer // com o texto que chega depois. Serial.println("[placa] WiFi Client ativado."); cliente.setTimeout(3000); Serial.println("[placa] timeout do cliente: 3 s"); Serial.println(); // 1. Entrada que vem do sensor: tem formato esperado e tem limite. String sensorBom = "23.5"; String sensorRuim = "23.5C ou erro"; Serial.println("[sensor] caso bom: " + sensorBom + " -> aceito: " + String(ehNumeroDeSensor(sensorBom) ? "sim" : "nao")); Serial.println("[sensor] caso ruim: " + sensorRuim + " -> aceito: " + String(ehNumeroDeSensor(sensorRuim) ? "sim" : "nao")); Serial.println("[sensor] texto gigante de 40 bytes -> aceito: " + String(ehNumeroDeSensor(String(40, '9')) ? "sim" : "nao")); Serial.println(); // 2. Entrada que vem da rede: e a que o aluno nao controla. String campoDoUsuario = "sala-01\"><script>alert(1)</script>"; Serial.println("[rede] campo recebido de um formulario:"); Serial.println("[rede] " + campoDoUsuario); Serial.println("[rede] contem aspa: " + String(campoDoUsuario.indexOf("\"") >= 0 ? "sim" : "nao")); Serial.print("[rede] depois de escapar: "); Serial.println(escapar(campoDoUsuario)); Serial.print("[navegador] HTML injection, mesmo valor depois de escapar saida: "); Serial.println(escaparHtml(campoDoUsuario)); Serial.println("[navegador] o <script> virou texto: o navegador nao executa mais."); Serial.println(); // 3. O payload final, ja com tudo escapado. String payload = montarPayload(campoDoUsuario); Serial.println("[payload] JSON que iria para o servidor:"); Serial.println("[payload] " + payload); Serial.println("[payload] tamanho: " + String(payload.length()) + " bytes"); Serial.println(); // 4. O que a placa faz se o servidor nao responde. Esta e a parte que // o aluno esquece: construir o pedido e facil, tratar a falha e a aula. Serial.println("[envio] nenhuma requisicao real e feita nesta aula."); Serial.println("[envio] URL alvo: " + String(URL_SERVIDOR)); Serial.println("[envio] o servidor, na aula real, responderia com codigo de status."); Serial.println("[envio] 401 e 403 significam token recusado: a placa guarda o token"); Serial.println("[envio] e o servidor e que decidiu que ele nao vale mais."); Serial.println("[envio] a placa nao tem como corrigir isso sozinha: so avisar."); Serial.println(); // 5. O limite de resposta e a defesa contra a placa travar. Serial.println("[memoria] limite de resposta: " + String(TAMANHO_MAXIMO_RESPOSTA) + " bytes"); Serial.println("[memoria] sem esse limite, uma resposta enorme esvazia a RAM da placa."); Serial.println(); Serial.println("[placa] heap livre: " + String(ESP.getFreeHeap()) + " bytes"); Serial.println("[fim]"); } void loop() { delay(5000); Serial.println("[placa] viva. heap livre: " + String(ESP.getFreeHeap()) + " bytes"); }
Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia09 aula1.
Aula 2 — Segredo, transporte e o dispositivo na rede
Objetivos
- Mapear, para cada segredo do projeto, os lugares onde ele pode existir e quem escreve em cada um: sketch versionado, variável de ambiente em produção, servidor de deploy e memória persistente da placa.
- Tirar o segredo do firmware, gravá-lo na memória persistente pela porta do pareamento e dizer por que o
.inocontinua podendo ir para o repositório inteiro. - Ler no monitor serial a sequência pareamento, boot,
201e401, e dizer em qual dos quatro estados a placa para de enviar. - Reescrever uma linha de log real do projeto sem cabeçalho
Authorizatione sem log com IP do solicitante, mantendo o que ainda serve para diagnosticar. - Justificar por que cifrar o transporte resolve a leitura no caminho da rede e não resolve o log, nem o
Serial, nem o repositório.
Material
- 1
ESP32DevKit V1 por aluno, com o caboUSB - Computador com o projeto do servidor do trimestre 3 aberto, com o terminal e o arquivo de ambiente na tela
- Navegador com o painel do provedor de deploy aberto, em modo somente leitura
Conceitos
Senha e token não são o mesmo objeto
A diferença não é de nome. É de onde cada uma pode estar e de o que acontece quando vaza.
| senha | token | |
|---|---|---|
| de quem é | da pessoa | do dispositivo |
| onde vive | no servidor, como hash | no cliente e no servidor |
| pode ser lida de volta | não | sim, e por isso é revogável |
| se vazar | troca-se a senha de todo mundo | revoga-se aquele token só |
| quem decide que vale | o servidor, sempre | o servidor, sempre |
O token é revogável porque é um valor que o servidor guarda. Enquanto o valor estiver na lista, vale; quando sai da lista, não vale mais — e a placa, que tem o texto, não tem opinião. Essa é a assimetria que o aluno precisa internalizar: guardar a senha na placa é o que torna o problema insolúvel; guardar o token é o que torna o problema banal.
O outro lado da moeda é que o token não serve para nada sozinho. Ele não tem senha, não tem e-mail e não abre painel. Quem tem o token é a placa, e a placa faz só o que o firmware dela faz.
Onde o segredo pode existir: do GitHub à memória da placa
Um segredo em produção é um valor que algum servidor reconhece de verdade. Esse é o único critério: se o valor abre alguma coisa em algum lugar, ele é segredo em produção — mesmo que o lugar seja a rede do laboratório e o servidor seja o computador do professor. Um valor que só funciona no papel não vaza nada, porque não abre nada.
A tabela que o aluno leva da aula:
| Onde o valor aparece | Quem escreve | O que o professor confere |
|---|---|---|
sketch versionado no GitHub | o aluno, na bancada | nunca: aqui não morou segredo |
| variável de ambiente em produção | quem opera o servidor, no deploy | sempre, e nunca no repositório |
| servidor de deploy | o painel do provedor ou o arquivo de ambiente da máquina | sempre, e com permissão só do serviço |
| memória persistente da placa | o pareamento, no primeiro acesso | sempre, e nunca no sketch |
Serial e log | ninguém, de propósito | nunca |
A variável de ambiente em produção é o lugar padrão de todo segredo do lado do servidor. O código lê process.env — ou os.environ, no Python — e o valor vem de fora. O repositório guarda o nome da variável e nada mais: um arquivo de exemplo com as linhas vazias já ensina quem vai configurar, e nenhuma delas precisa trazer o valor. A regra do projeto cabe numa frase: quem escreve no repositório escreve o nome da variável e o mecanismo, nunca o valor.
O segredo no servidor de deploy tem três origens possíveis, e o aluno precisa saber as três: o painel do provedor, que grava o valor no serviço e o entrega ao processo na hora do start; o arquivo de ambiente da máquina, que existe em disco e precisa de permissão só para o usuário do serviço; e o cofre do orquestrador, quando o serviço roda em container. Nenhuma delas é o repositório. Quando alguém escreve o valor direto no código do servidor, o problema deixa de ser o servidor e passa a ser o histórico do GitHub.
Provisionamento é o primeiro acesso, e ele vem de fora do sketch. A placa sai da fábrica sem nada: sem nome de rede, sem senha, sem token. Quem dá o primeiro dado é uma pessoa, na página de setup, ou é a fábrica, pela porta física. A consequência é o oposto de "coloquei no código": o segredo chega depois que o firmware já está gravado, por um caminho que não passa pelo arquivo.
É por isso que senha do WiFi em texto no firmware é o erro mais comum da semana e o mais fácil de não enxergar: ela funciona na bancada e funciona na rua, e só não funciona como segredo. O arquivo inteiro vai para um repositório que é espaço restrito com a credencial dentro, o que equivale a dizer que não é espaço restrito: qualquer pessoa com leitura no repositório lê a rede da escola.
Rota de fuga é o nome do caminho que o segredo percorre até chegar em alguém que vai usá-lo. Não é acaso, e por isso dá para fechar cada etapa:
| Rota de fuga | Quem lê sem pedir | Como fechar |
|---|---|---|
| histórico do repositório | quem tem leitura no repositório | o valor nunca entra no arquivo; se já entrou, troque o valor no servidor e trate o histórico como leitura pública |
| arquivo de ambiente do servidor de deploy | quem entra na máquina | arquivo ignorado pelo Git, permissão só do usuário do serviço |
| linha de log com o cabeçalho completo | quem tem leitura de log | auth: [REDIGIDO] e nada mais |
Serial da placa | quem olha a bancada ou o vídeo da aula | imprimir tamanho e estado, nunca o valor |
senha do WiFi no firmware | quem clona o repositório | provisionamento, ou constante de exemplo com o comentário dizendo que é fictícia |
Preferences: gravar no primeiro acesso, descartar no 401
A memória RAM da placa é apagada no instante em que falta alimentação. A memória não volátil mantém o conteúdo, e é onde o firmware, a configuração do WiFi e o token ficam gravados. Preferences é a interface para essa área, com três operações que resolvem a aula inteira:
Preferences memoria; memoria.begin("dispositivo", false); memoria.putString("tk", token); memoria.end();
begin abre o espaço de nome, e false significa "abrindo para escrever". putString grava. end fecha e sincroniza: sem ele a gravação pode ficar pendente na cache, e o token some justamente na placa que foi desligada mal. A leitura é idêntica com true, e getString devolve um valor padrão quando a chave não existe.
O ponto que o professor precisa gritar: o token não está no sketch. Ele está num lugar da placa que se apaga com o git checkout e não vai para o repositório. Por isso o .ino pode ir para o GitHub inteiro sem vazar nada. Um segredo escrito no código é um segredo publicado, e o nome técnico disso é segredo no firmware.
Quando o servidor recusa o token, ele responde 401 Unauthorized com um corpo que costuma dizer algo como token revogado. O que a placa faz com isso é a aula inteira, e a resposta errada é quase sempre a mesma:
while (status != 201) { // ERRADO enviarLeitura(token); }
Esse while é um dispositivo que gasta bateria drenando a rede, e em cima disso nunca vai conseguir: o token é inválido, e não é uma rede instável. Não há retry que resolva, porque não houve falha temporária — houve uma decisão.
O tratamento certo tem três partes: parar, limpar o estado local e avisar. Limpar significa apagar o token da memória, para que a placa pare de tentar e para que o próximo estado não seja um estado com credencial morta. Avisar significa colocar algo no log, porque quem resolve isso é uma pessoa.
E por isso que o token vira um estado nomeado e não uma variável solta: com SEM_TOKEN, TOKEN_VALIDO, TOKEN_RECEBIDO e TOKEN_REVOGADO, o professor aponta a linha do código que decide o comportamento, e o aluno imprime o estado em vez de tentar adivinhar.
HTTPS resolve o caminho, e o resto continua aberto
HTTP sem criptografia é o estado padrão de um servidor de projeto: o token viaja no cabeçalho, em texto que qualquer pessoa no caminho da rede lê sem esforço — inclusive o roteador do laboratório, um ponto de acesso falso e o provedor. Cifrar o transporte é o que fecha essa porta, e o mecanismo é o TLS: as duas pontas acordam uma chave de sessão e o tráfego inteiro passa cifrado por ela. O nome HTTPS é só o HTTP dentro desse túnel; o que protege é a sessão, não o esquema do endereço.
O que o TLS resolve: a leitura por terceiro no caminho. O que ele não resolve: um token vazado continua valendo até ser revogado, e um servidor com https:// errado continua sendo um servidor.
O detalhe que separa cifrar de fingir que cifrou é o certificado. É ele que diz à placa para quem ela está falando, e é a conferência dele que impede que um intermediário finja ser o seu servidor. Uma placa que desliga a validação do certificado tem um túnel honesto aberto para o lugar errado.
Na placa, NetworkClientSecure é o objeto que faz isso, com um custo que o aluno precisa conhecer: a conexão cifrada leva vários segundos para subir numa placa de 240 MHz, e precisa de memória para o buffer. A escolha entre uma conexão nova por envio e uma conexão mantida aberta é o tradeoff real, e ele reaparece no dia 11.
Depois que o caminho está cifrado, sobra tudo o que o túnel não alcança. E o que mais sobra é o log.
Uma linha de log com IP do solicitante é um dado pessoal identificável, e a LGPD trata dele como trata de qualquer outro: ele tem finalidade declarada, prazo de guarda e alguém responsável. Guardar o endereço de quem chamou a API "porque sempre teve assim" não tem finalidade nenhuma, e é o registro mais fácil de eliminar sem quebrar o produto. A mesma linha costuma trazer o dado pessoal do usuário — nome, e-mail, endereço, o valor da leitura — e cada campo tem o mesmo tratamento.
A prática que fecha a aula: log de entrada, log de saída, log de erro, e nenhuma credencial, nenhum endereço de origem e nenhum campo identificável entre eles. Onde o log precisa falar do token, ele fala de uma forma só: auth: [REDIGIDO]. Onde precisa falar de quem chamou, ele fala do que a chamada fez, não de quem foi.
E vale para o Serial da placa: a aula de hoje não imprime o valor do token, nem em bancada, porque o hábito que se cria no laboratório é o hábito que vai para o produto. Privacidade não é uma etapa do projeto, é o formato padrão do registro. E a placa fica na casa de quem comprou: ela passa o dia inteiro vendo temperatura, horário e uso.
Atividade
Montagem:
Esta aula não pede circuito: o sketch não usa nenhum GPIO, e o instrumento da atividade é o monitor serial. O professor deixa a placa ligada pelo cabo USB e lê o que ela escreve, que é a mesma coisa que o Serial mostra.
- Grave o sketch da resolução e rode no monitor serial a 115200. Identifique, no log, o número do
firmwaree o estado da placa em cada bloco[estado]. - Desligue a placa do cabo, volte a ligar e rode de novo. O que aconteceu com o token? Onde ele estava guardado?
- Troque
TOKEN_DE_EXEMPLOpor qualquer outro valor de exemplo, grave, e veja que o token dosetupe o token lido no boot são o mesmo. Explique o que aconteceria se a placa lesse o token de uma variável comum. - Ligue
REVOGADO_PARA_DEMOparatrueantes do primeiro envio e grave. O que o serial mostra no[envio 1]? Qual é a diferença de comportamento entre tentar de novo e limpar o estado? - No servidor do projeto, procure no log qualquer linha com o token do dispositivo. Se houver, apague a linha e escreva no caderno qual foi a origem dela: log de requisição,
console.logde depuração ou resposta de erro. - Escreva, em quatro linhas, o que a placa faz em cada um destes casos:
201recebido,401recebido, e placa sem rede depois de gravar a leitura no buffer. - Abra no computador o arquivo de ambiente do servidor do projeto e liste, por linha, o nome de cada variável de ambiente em produção que o servidor usa. Coloque um
Xna que já tem valor preenchido. Nenhum valor entra no caderno: só o nome, e onde ele está gravado hoje. - No repositório do projeto, procure a senha do
WiFie o token em todo lugar, inclusive no histórico de commits, e escreva o que apareceu. Diga, para cada achado, em qual etapa da rota de fuga ele está. - Escolha uma linha de log real do projeto, corte o cabeçalho
Authorizatione o endereço do solicitante, e escreva a linha final no caderno. Diga o que você perdeu de diagnosticar e o que não pode perder. - Escreva o caminho do segredo do dispositivo, do servidor de deploy até a memória da placa, marcando cada salto e dizendo em qual deles o valor aparece em texto claro.
Nota: 12 pontos. Critério de fim: o serial mostra os quatro estados, o professor viu o 401 com a placa limpando o token em vez de reenviar, e o caderno tem o caminho do segredo desenhado sem nenhum valor de verdade escrito nele.
Resolucao
// Aula 2 do dia 9: segredo, transporte e o dispositivo na rede. // // A placa guarda um TOKEN, nunca uma senha. A diferenca nao e vocabulario: // senha e a credencial da pessoa, e ela nunca sai do servidor; token e a // credencial da placa, e ela pode ser revogada sem tocar em senha nenhuma. // // O codigo tem um servidor de exemplo rodando DENTRO da propria placa. Ele // responde 401 quando o token foi revogado, e o firmware tem que saber o que // fazer com esse 401: repetir o envio seria o defeito. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> #include <NetworkClientSecure.h> #include <Preferences.h> // Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum // segredo real entra em sketch de exemplo, nem em material, nem no git. #define WIFI_SSID "lab-esp32-aula" #define WIFI_PASSWORD "troque-esta-pela-sua" // Transporte. HTTPS e o estado final do projeto: o token viaja no cabecalho // de autenticacao, e em HTTP em claro ele viaja para qualquer pessoa na rede. #define USAR_HTTPS false #define URL_SERVIDOR "https://api.exemplo.invalido/v1/leitura" // Nome do espaco de memoria persistente. A placa tem memoria nao volatil: o // token continua valendo depois de desligar o cabo de alimentacao. #define ESPACO_NVS "dispositivo" // O token do dispositivo. Este valor e de EXEMPLO e nao vale em lugar nenhum: // o token de verdade vem do servidor, na resposta do pareamento. #define TOKEN_DE_EXEMPLO "tk-lab-bancada01" // Chave sob a qual o token fica guardado na memoria persistente. #define CHAVE_TOKEN "tk" // Versao do firmware. Este e o numero que o professor le no serial para saber // qual versao esta rodando depois de um deploy. #define VERSAO_FIRMWARE "1.4.0" Preferences memoria; // Esta flag liga e desliga o "token revogado" do servidor de exemplo. E o que // o professor alterna na frente da turma para mostrar o 401 chegando. bool REVOGADO_PARA_DEMO = false; // Os quatro estados que a placa pode estar. Uma string solta no meio do codigo // e o que produz "token invalido" em um `if` e "token expirado" em outro: o // estado precisa ter nome, porque o professor precisa conseguir apontar. enum Estado { SEM_TOKEN, TOKEN_VALIDO, TOKEN_RECEBIDO, TOKEN_REVOGADO }; Estado estadoAtual = SEM_TOKEN; // ------------------------------------------------------------------ a placa // Guarda o token na memoria persistente. Preferencias e o caminho certo: o // token nao vai para o sketch, e por isso nao entra no git nem no log. void guardarToken(const String& token) { memoria.begin(ESPACO_NVS, false); memoria.putString(CHAVE_TOKEN, token); memoria.end(); Serial.println("[token] gravado na memoria persistente da placa"); } // Le o token de volta. E o que faz a placa lembrar do dispositivo depois que // alguem desligou o cabo de alimentacao. String lerToken() { memoria.begin(ESPACO_NVS, true); String token = memoria.getString(CHAVE_TOKEN, ""); memoria.end(); return token; } // Apaga o token. E o que a placa faz quando recebe 401: ela nao pode se // reautenticar sozinha, entao limpa o estado e espera o dono parear de novo. void esquecerToken() { memoria.begin(ESPACO_NVS, false); memoria.remove(CHAVE_TOKEN); memoria.end(); estadoAtual = SEM_TOKEN; Serial.println("[token] apagado da placa apos recusa do servidor"); } String nomeDoEstado(Estado estado) { switch (estado) { case SEM_TOKEN: return "SEM_TOKEN"; case TOKEN_VALIDO: return "TOKEN_VALIDO"; case TOKEN_RECEBIDO: return "TOKEN_RECEBIDO"; default: return "TOKEN_REVOGADO"; } } // ------------------------------------------------- servidor de exemplo // // Roda dentro da propria placa para que a aula nao dependa de rede. Nao e um // servidor de verdade: e o recorte do comportamento que o firmware precisa // tratar. As tres respostas que importam sao 201, 401 e 503. int responderServidorDeExemplo(bool tokenRevogado, bool servidorLento) { if (servidorLento) { return 503; } if (tokenRevogado) { return 401; } return 201; } // ------------------------------------------------------------------ envio // Envia uma leitura e devolve o codigo de status. Separar "enviar" de // "decidir o que fazer com a resposta" e o que permite testar os dois. int enviarLeitura(const String& token) { Serial.println("[http] abrindo conexao " + String(USAR_HTTPS ? "TLS" : "em claro")); if (USAR_HTTPS) { Serial.println("[tls] sem certificado validado: aceitavel so em laboratorio,"); Serial.println("[tls] em producao a placa precisa conferir o certificado do servidor."); } else { Serial.println("[http] token vai no cabecalho em texto legivel. Nao use assim."); } int status = responderServidorDeExemplo(REVOGADO_PARA_DEMO, false); Serial.println("[http] resposta: " + String(status)); return status; } // ------------------------------------------------------------------- setup void setup() { Serial.begin(115200); delay(200); Serial.println(); Serial.println("=== dia 9, aula 2: segredo, transporte e dispositivo ==="); Serial.println("firmware: " + String(VERSAO_FIRMWARE)); Serial.println(); Serial.println("[placa] token em memoria volatil: NAO. Va para Preferences."); Serial.println("[placa] senha da pessoa: NUNCA entra aqui. Isso nao existe na placa."); Serial.println(); // 1. O pareamento: o servidor entrega o token, a placa guarda. Serial.println("[pareamento] token de exemplo recebido do servidor:"); Serial.println("[pareamento] " + String(TOKEN_DE_EXEMPLO)); guardarToken(TOKEN_DE_EXEMPLO); estadoAtual = TOKEN_RECEBIDO; Serial.println("[estado] " + nomeDoEstado(estadoAtual)); Serial.println(); // 2. O token sobrevive ao desligamento. String tokenRecuperado = lerToken(); Serial.println("[boot] token lido da memoria persistente, " + String(tokenRecuperado.length()) + " bytes: " + String(tokenRecuperado == TOKEN_DE_EXEMPLO ? "presente" : "ausente")); Serial.println("[estado] " + nomeDoEstado(estadoAtual)); Serial.println(); // 3. Envio normal. Serial.println("[envio 1] token aceito pelo servidor:"); int status = enviarLeitura(tokenRecuperado); if (status == 201) { estadoAtual = TOKEN_VALIDO; Serial.println("[decisao] 201: gravado. O token serve."); } Serial.println("[estado] " + nomeDoEstado(estadoAtual)); Serial.println(); // 4. O servidor revoga o token. A placa ainda tem o valor na mao. Serial.println("[servidor] dono da conta revoga o token no painel."); Serial.println("[servidor] o valor continua gravado na placa. Isso e o ponto."); REVOGADO_PARA_DEMO = true; Serial.println(); // 5. A placa tenta de novo e leva 401. Serial.println("[envio 2] a placa reenvia com o mesmo token:"); status = enviarLeitura(tokenRecuperado); if (status == 401) { Serial.println("[decisao] 401: token recusado. Nao reenvio, nao espero, nao insisto."); esquecerToken(); } else if (status == 201) { Serial.println("[decisao] o token revogado ainda funcionou. Isso e um defeito do servidor."); } Serial.println("[estado] " + nomeDoEstado(estadoAtual)); Serial.println(); // 6. O que a placa faz depois: fica parada e avisa. Serial.println("[recuperacao] sem token, a placa so faz duas coisas:"); Serial.println("[recuperacao] 1. avisar no display que precisa de pareamento"); Serial.println("[recuperacao] 2. tentar parear quando alguem abrir a pagina de setup"); Serial.println("[recuperacao] ela nao tenta adivinhar a senha. Ela nunca a viu."); Serial.println(); // 7. O que o professor precisa ver quando o log vaza. Serial.println("[log] a linha de log NUNCA leva o token:"); Serial.println("[log] leitura recebida de esp32-bancada-01"); Serial.println("[log] auth: [REDIGIDO] status=201"); Serial.println("[log] levar o token inteiro no log e entregar a credencial do dispositivo."); Serial.println(); Serial.println("[placa] heap livre: " + String(ESP.getFreeHeap()) + " bytes"); Serial.println("[fim]"); } void loop() { delay(8000); Serial.println("[placa] viva | estado: " + nomeDoEstado(estadoAtual) + " | heap: " + String(ESP.getFreeHeap()) + " bytes"); }
Por que assim e não de outro jeito. A enum Estado com quatro estados é a decisão de design da aula. A alternativa — um if (token == "") espalhado por cinco lugares — funciona igual e é ilegível na semana seguinte. Com estado nomeado, o Serial.println do loop imprime o que a placa está fazendo, e o professor lê de longe.
A função enviarLeitura não faz HTTP de verdade: ela chama responderServidorDeExemplo, que devolve o código de status. Isso é deliberado, pelo mesmo motivo da aula 1 — uma aula que depende de rede ao vivo perde metade da turma no primeiro timeout. O que o sketch ensina é o if que vem depois da chamada, e esse if é o objeto da aula.
O REVOGADO_PARA_DEMO fica como flag global e não como parâmetro porque a aula precisa de dois estados no mesmo setup: enviar com o token aceito, depois revogar, depois enviar de novo. Com parâmetro, seria preciso reescrever a ordem do roteiro.
O USAR_HTTPS fica false de propósito, com o comentário explicando que token em HTTP em claro é defeito. O professor liga para true na frente da turma e a placa demora vários segundos a mais na conexão — esse atraso é a aula, e ele não existe num comentário.
O TOKEN_DE_EXEMPLO está no sketch, e ainda assim o sketch está certo: é um valor de demonstração, e o comentário diz isso na linha de cima. A distinção que o professor faz em voz alta é entre "segredo de exemplo, com valor inventado e comentado" e "segredo real, com valor que funciona em algum lugar". O segundo não entra no material nunca.
Criterios de correcao
| Critério | Pontos |
|---|---|
Token guardado em Preferences e lido de volta no boot, com begin, putString e end na ordem | 3 pontos |
401 tratado sem novo envio, com o token apagado e o estado virando SEM_TOKEN | 3 pontos |
| Item 7 entregue só com nome das variáveis de ambiente em produção, sem nenhum valor | 2 pontos |
Serial sem valor de token, sem senha do WiFi e sem endereço de origem em nenhum bloco | 2 pontos |
Item 9: linha reescrita sem cabeçalho Authorization, com o status preservado | 1 ponto |
| Item 10: todos os saltos do caminho escritos, e nenhum deles com o valor no sketch | 1 ponto |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
| Token guardado em variável comum | O token funciona até a próxima reinicialização | "RAM é apagada no desligamento. Preferences grava em memória não volátil e o dispositivo sobrevive ao corte de energia." |
begin sem end | Token às vezes some, "às vezes" | "O end sincroniza a cache. Sem ele a gravação fica pendente e o token se perde justamente quando a placa foi desligada mal." |
| Reenviar em laço até dar certo | while (status != 201) enviarLeitura(); | "Token recusado não é falha temporária. Não há espera que resolva. Pare, limpe o estado, avise a pessoa." |
| Imprimir o token para "verificar se gravou" | [token] gravado: tk-lab-bancada01 | "Verifique o tamanho e o estado, nunca o valor. É um hábito: o que você imprime no laboratório, você imprime no produto." |
Senha do WiFi no topo do sketch | #define WIFI_PASSWORD "..." sem nenhuma nota | "O valor é fictício e comentado como tal. Sem o comentário, ninguém sabe se aquilo é exemplo ou é a rede da escola." |
git add . na pasta do servidor, com o arquivo de ambiente junto | Arquivo de ambiente versionado no GitHub | "Ignore o arquivo de ambiente antes do primeiro add e versione só o arquivo de exemplo, com os nomes das variáveis e nenhum valor." |
| Achar que o repositório é espaço restrito | "Só a equipe tem acesso, então está protegido" | "Restringir quem entra não protege o arquivo: protege quem chega depois do vazamento. O segredo não entra no repositório." |
| Contar o repositório como cofre da placa | "A placa tem memória protegida, então o token está seguro" | "A memória é espaço restrito do firmware, não do operador. Quem tem a placa e o valor de pareamento lê o token." |
HTTPS como configuração de uma flag | Aluno troca https:// e acha que resolveu | "Trocar o esquema é papel. O que protege é a sessão inteira e o certificado que a placa precisa conferir. São coisas diferentes." |
| Log com o endereço do solicitante "porque sempre teve assim" | req de 203.0.113.7 status=201 | "Endereço de origem é dado pessoal e não tem finalidade declarada. A LGPD trata igual: quem registra, por quê e até quando." |
| Apagar o log inteiro em vez da linha | "rm log.txt" | "Apague a linha com a credencial. O resto do log é a única forma de achar o erro amanhã." |
Desafio extra
Implemente a página de pareamento: quando a placa está em SEM_TOKEN, ela sobe um WebServer em 192.168.4.1 e mostra um código de seis dígitos. O professor, em outro celular, conecta nessa rede, digita o código numa rota do servidor, e o servidor devolve o token. Meça quanto tempo leva do SEM_TOKEN até o TOKEN_VALIDO e escreva por que esse tempo é o que a pessoa vive como "o dispositivo não funciona". Depois some a rota de devolução do token do log do servidor e escreva o que um técnico deixa de conseguir diagnosticar quando isso acontece.
A resolucao, compilada
// Aula 2 do dia 9: segredo, transporte e o dispositivo na rede. // // A placa guarda um TOKEN, nunca uma senha. A diferenca nao e vocabulario: // senha e a credencial da pessoa, e ela nunca sai do servidor; token e a // credencial da placa, e ela pode ser revogada sem tocar em senha nenhuma. // // O codigo tem um servidor de exemplo rodando DENTRO da propria placa. Ele // responde 401 quando o token foi revogado, e o firmware tem que saber o que // fazer com esse 401: repetir o envio seria o defeito. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> #include <NetworkClientSecure.h> #include <Preferences.h> // Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum // segredo real entra em sketch de exemplo, nem em material, nem no git. #define WIFI_SSID "lab-esp32-aula" #define WIFI_PASSWORD "troque-esta-pela-sua" // Transporte. HTTPS e o estado final do projeto: o token viaja no cabecalho // de autenticacao, e em HTTP em claro ele viaja para qualquer pessoa na rede. #define USAR_HTTPS false #define URL_SERVIDOR "https://api.exemplo.invalido/v1/leitura" // Nome do espaco de memoria persistente. A placa tem memoria nao volatil: o // token continua valendo depois de desligar o cabo de alimentacao. #define ESPACO_NVS "dispositivo" // O token do dispositivo. Este valor e de EXEMPLO e nao vale em lugar nenhum: // o token de verdade vem do servidor, na resposta do pareamento. #define TOKEN_DE_EXEMPLO "tk-lab-bancada01" // Chave sob a qual o token fica guardado na memoria persistente. #define CHAVE_TOKEN "tk" // Versao do firmware. Este e o numero que o professor le no serial para saber // qual versao esta rodando depois de um deploy. #define VERSAO_FIRMWARE "1.4.0" Preferences memoria; // Esta flag liga e desliga o "token revogado" do servidor de exemplo. E o que // o professor alterna na frente da turma para mostrar o 401 chegando. bool REVOGADO_PARA_DEMO = false; // Os quatro estados que a placa pode estar. Uma string solta no meio do codigo // e o que produz "token invalido" em um `if` e "token expirado" em outro: o // estado precisa ter nome, porque o professor precisa conseguir apontar. enum Estado { SEM_TOKEN, TOKEN_VALIDO, TOKEN_RECEBIDO, TOKEN_REVOGADO }; Estado estadoAtual = SEM_TOKEN; // ------------------------------------------------------------------ a placa // Guarda o token na memoria persistente. Preferencias e o caminho certo: o // token nao vai para o sketch, e por isso nao entra no git nem no log. void guardarToken(const String& token) { memoria.begin(ESPACO_NVS, false); memoria.putString(CHAVE_TOKEN, token); memoria.end(); Serial.println("[token] gravado na memoria persistente da placa"); } // Le o token de volta. E o que faz a placa lembrar do dispositivo depois que // alguem desligou o cabo de alimentacao. String lerToken() { memoria.begin(ESPACO_NVS, true); String token = memoria.getString(CHAVE_TOKEN, ""); memoria.end(); return token; } // Apaga o token. E o que a placa faz quando recebe 401: ela nao pode se // reautenticar sozinha, entao limpa o estado e espera o dono parear de novo. void esquecerToken() { memoria.begin(ESPACO_NVS, false); memoria.remove(CHAVE_TOKEN); memoria.end(); estadoAtual = SEM_TOKEN; Serial.println("[token] apagado da placa apos recusa do servidor"); } String nomeDoEstado(Estado estado) { switch (estado) { case SEM_TOKEN: return "SEM_TOKEN"; case TOKEN_VALIDO: return "TOKEN_VALIDO"; case TOKEN_RECEBIDO: return "TOKEN_RECEBIDO"; default: return "TOKEN_REVOGADO"; } } // ------------------------------------------------- servidor de exemplo // // Roda dentro da propria placa para que a aula nao dependa de rede. Nao e um // servidor de verdade: e o recorte do comportamento que o firmware precisa // tratar. As tres respostas que importam sao 201, 401 e 503. int responderServidorDeExemplo(bool tokenRevogado, bool servidorLento) { if (servidorLento) { return 503; } if (tokenRevogado) { return 401; } return 201; } // ------------------------------------------------------------------ envio // Envia uma leitura e devolve o codigo de status. Separar "enviar" de // "decidir o que fazer com a resposta" e o que permite testar os dois. int enviarLeitura(const String& token) { Serial.println("[http] abrindo conexao " + String(USAR_HTTPS ? "TLS" : "em claro")); if (USAR_HTTPS) { Serial.println("[tls] sem certificado validado: aceitavel so em laboratorio,"); Serial.println("[tls] em producao a placa precisa conferir o certificado do servidor."); } else { Serial.println("[http] token vai no cabecalho em texto legivel. Nao use assim."); } int status = responderServidorDeExemplo(REVOGADO_PARA_DEMO, false); Serial.println("[http] resposta: " + String(status)); return status; } // ------------------------------------------------------------------- setup void setup() { Serial.begin(115200); delay(200); Serial.println(); Serial.println("=== dia 9, aula 2: segredo, transporte e dispositivo ==="); Serial.println("firmware: " + String(VERSAO_FIRMWARE)); Serial.println(); Serial.println("[placa] token em memoria volatil: NAO. Va para Preferences."); Serial.println("[placa] senha da pessoa: NUNCA entra aqui. Isso nao existe na placa."); Serial.println(); // 1. O pareamento: o servidor entrega o token, a placa guarda. Serial.println("[pareamento] token de exemplo recebido do servidor:"); Serial.println("[pareamento] " + String(TOKEN_DE_EXEMPLO)); guardarToken(TOKEN_DE_EXEMPLO); estadoAtual = TOKEN_RECEBIDO; Serial.println("[estado] " + nomeDoEstado(estadoAtual)); Serial.println(); // 2. O token sobrevive ao desligamento. String tokenRecuperado = lerToken(); Serial.println("[boot] token lido da memoria persistente, " + String(tokenRecuperado.length()) + " bytes: " + String(tokenRecuperado == TOKEN_DE_EXEMPLO ? "presente" : "ausente")); Serial.println("[estado] " + nomeDoEstado(estadoAtual)); Serial.println(); // 3. Envio normal. Serial.println("[envio 1] token aceito pelo servidor:"); int status = enviarLeitura(tokenRecuperado); if (status == 201) { estadoAtual = TOKEN_VALIDO; Serial.println("[decisao] 201: gravado. O token serve."); } Serial.println("[estado] " + nomeDoEstado(estadoAtual)); Serial.println(); // 4. O servidor revoga o token. A placa ainda tem o valor na mao. Serial.println("[servidor] dono da conta revoga o token no painel."); Serial.println("[servidor] o valor continua gravado na placa. Isso e o ponto."); REVOGADO_PARA_DEMO = true; Serial.println(); // 5. A placa tenta de novo e leva 401. Serial.println("[envio 2] a placa reenvia com o mesmo token:"); status = enviarLeitura(tokenRecuperado); if (status == 401) { Serial.println("[decisao] 401: token recusado. Nao reenvio, nao espero, nao insisto."); esquecerToken(); } else if (status == 201) { Serial.println("[decisao] o token revogado ainda funcionou. Isso e um defeito do servidor."); } Serial.println("[estado] " + nomeDoEstado(estadoAtual)); Serial.println(); // 6. O que a placa faz depois: fica parada e avisa. Serial.println("[recuperacao] sem token, a placa so faz duas coisas:"); Serial.println("[recuperacao] 1. avisar no display que precisa de pareamento"); Serial.println("[recuperacao] 2. tentar parear quando alguem abrir a pagina de setup"); Serial.println("[recuperacao] ela nao tenta adivinhar a senha. Ela nunca a viu."); Serial.println(); // 7. O que o professor precisa ver quando o log vaza. Serial.println("[log] a linha de log NUNCA leva o token:"); Serial.println("[log] leitura recebida de esp32-bancada-01"); Serial.println("[log] auth: [REDIGIDO] status=201"); Serial.println("[log] levar o token inteiro no log e entregar a credencial do dispositivo."); Serial.println(); Serial.println("[placa] heap livre: " + String(ESP.getFreeHeap()) + " bytes"); Serial.println("[fim]"); } void loop() { delay(8000); Serial.println("[placa] viva | estado: " + nomeDoEstado(estadoAtual) + " | heap: " + String(ESP.getFreeHeap()) + " bytes"); }
Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia09 aula2.
