Cifra de César em Zig — Tutorial Passo a Passo
Neste tutorial, vamos implementar a Cifra de César, um dos algoritmos de criptografia mais antigos e conhecidos. Apesar de ser simples, este projeto nos ensina conceitos importantes sobre manipulação de caracteres, aritmética modular e processamento de texto em Zig.
O Que Vamos Construir
Nossa implementação vai:
- Encriptar e decriptar texto com deslocamento configurável
- Preservar maiúsculas/minúsculas e caracteres não-alfabéticos
- Quebrar a cifra por força bruta (testar todos os deslocamentos)
- Analisar frequência de letras para quebra inteligente
- Funcionar como ferramenta CLI com argumentos
Por Que Este Projeto?
A Cifra de César é perfeita para aprender manipulação de caracteres em Zig. Diferente de linguagens com strings Unicode complexas, Zig trabalha com bytes (u8) diretamente, o que torna operações de deslocamento naturais e eficientes. Além disso, a aritmética modular que usamos aqui é fundamental em criptografia real.
Pré-requisitos
- Zig 0.13+ instalado (guia de instalação)
- Familiaridade com tipos inteiros em Zig (fundamentos)
Passo 1: Estrutura do Projeto
mkdir cifra-cesar
cd cifra-cesar
zig init
Passo 2: Função de Deslocamento
O coração da Cifra de César é o deslocamento de cada letra por um número fixo de posições no alfabeto.
const std = @import("std");
const io = std.io;
const mem = std.mem;
const fmt = std.fmt;
/// Desloca um único caractere pela quantidade especificada.
/// Preserva maiúsculas/minúsculas. Não-letras passam inalteradas.
///
/// A aritmética modular garante que após 'z' voltamos para 'a'.
/// Exemplo com deslocamento 3: 'a' -> 'd', 'x' -> 'a', 'Z' -> 'C'
fn deslocarCaractere(c: u8, deslocamento: u8) u8 {
if (c >= 'a' and c <= 'z') {
// Normaliza para 0-25, aplica deslocamento, volta para 'a'-'z'
return 'a' + (c - 'a' + deslocamento) % 26;
} else if (c >= 'A' and c <= 'Z') {
return 'A' + (c - 'A' + deslocamento) % 26;
}
// Caracteres não-alfabéticos passam sem alteração
return c;
}
/// Desloca um caractere na direção inversa (para decriptação).
/// Usamos a propriedade: deslocar por (26 - n) é o mesmo que
/// deslocar por -n no módulo 26.
fn deslocarCaractereInverso(c: u8, deslocamento: u8) u8 {
return deslocarCaractere(c, 26 - (deslocamento % 26));
}
Por que aritmética modular? O operador % garante que o resultado sempre fica no intervalo 0-25. Sem ele, ‘x’ + 3 daria um valor fora do alfabeto. Essa é a mesma matemática usada em criptografia moderna (RSA, curvas elípticas), só que em escala muito maior.
Passo 3: Encriptação e Decriptação
/// Resultado de uma operação de cifra.
/// Usamos um buffer fixo para evitar alocação dinâmica.
const ResultadoCifra = struct {
dados: [4096]u8,
len: usize,
pub fn texto(self: *const ResultadoCifra) []const u8 {
return self.dados[0..self.len];
}
};
/// Encripta o texto usando a Cifra de César com o deslocamento dado.
fn encriptar(texto: []const u8, deslocamento: u8) ResultadoCifra {
var resultado = ResultadoCifra{ .dados = undefined, .len = 0 };
const len = @min(texto.len, resultado.dados.len);
for (texto[0..len], 0..) |c, i| {
resultado.dados[i] = deslocarCaractere(c, deslocamento % 26);
}
resultado.len = len;
return resultado;
}
/// Decripta o texto cifrado usando o deslocamento dado.
fn decriptar(texto_cifrado: []const u8, deslocamento: u8) ResultadoCifra {
var resultado = ResultadoCifra{ .dados = undefined, .len = 0 };
const len = @min(texto_cifrado.len, resultado.dados.len);
for (texto_cifrado[0..len], 0..) |c, i| {
resultado.dados[i] = deslocarCaractereInverso(c, deslocamento % 26);
}
resultado.len = len;
return resultado;
}
Passo 4: Quebra por Força Bruta
Como a Cifra de César tem apenas 25 possíveis deslocamentos, podemos tentar todos:
/// Tenta todos os 25 deslocamentos e exibe os resultados.
/// Na Cifra de César, 26 deslocamentos possíveis (1-25 úteis) tornam
/// a força bruta trivial — esse é o motivo pelo qual ela não é
/// segura para uso real.
fn quebraForcaBruta(texto_cifrado: []const u8, writer: anytype) !void {
try writer.print("\n--- Quebra por Força Bruta ---\n", .{});
try writer.print("Testando todos os 25 deslocamentos:\n\n", .{});
var desl: u8 = 1;
while (desl <= 25) : (desl += 1) {
const resultado = decriptar(texto_cifrado, desl);
// Mostra apenas os primeiros 60 caracteres para legibilidade
const preview_len = @min(resultado.len, 60);
try writer.print(" Desl. {d:>2}: {s}", .{ desl, resultado.texto()[0..preview_len] });
if (resultado.len > 60) try writer.print("...", .{});
try writer.print("\n", .{});
}
}
Passo 5: Análise de Frequência
Uma abordagem mais inteligente é analisar a frequência de letras. Em português, a letra mais comum é ‘a’ (~14.6%), seguida de ’e’ (~12.6%).
/// Frequência esperada das letras em português brasileiro (%).
/// Fonte: análise de corpora de texto em português.
const freq_portugues = [26]f64{
14.63, 1.04, 3.88, 4.99, 12.57, 1.02, 1.30, 1.28, 6.18, 0.40,
0.02, 2.78, 4.74, 5.05, 10.73, 2.52, 1.20, 6.53, 7.81, 4.34,
4.63, 1.67, 0.01, 0.21, 0.01, 0.47,
};
/// Calcula a frequência de cada letra no texto (case-insensitive).
fn calcularFrequencia(texto: []const u8) [26]f64 {
var contagem = [_]u32{0} ** 26;
var total: u32 = 0;
for (texto) |c| {
if (c >= 'a' and c <= 'z') {
contagem[c - 'a'] += 1;
total += 1;
} else if (c >= 'A' and c <= 'Z') {
contagem[c - 'A'] += 1;
total += 1;
}
}
var freq = [_]f64{0.0} ** 26;
if (total == 0) return freq;
const total_f: f64 = @floatFromInt(total);
for (&freq, contagem) |*f, cnt| {
f.* = @as(f64, @floatFromInt(cnt)) / total_f * 100.0;
}
return freq;
}
/// Calcula a distância qui-quadrado entre duas distribuições de frequência.
/// Menor valor = distribuições mais similares.
fn distanciaQuiQuadrado(observada: [26]f64, esperada: [26]f64) f64 {
var chi2: f64 = 0.0;
for (observada, esperada) |obs, esp| {
if (esp > 0.0) {
const diff = obs - esp;
chi2 += (diff * diff) / esp;
}
}
return chi2;
}
/// Quebra a cifra usando análise de frequência.
/// Testa todos os deslocamentos e retorna o que produz
/// a distribuição de letras mais próxima do português.
fn quebraFrequencia(texto_cifrado: []const u8) u8 {
var melhor_desl: u8 = 0;
var melhor_score: f64 = std.math.floatMax(f64);
var desl: u8 = 0;
while (desl < 26) : (desl += 1) {
const tentativa = decriptar(texto_cifrado, desl);
const freq = calcularFrequencia(tentativa.texto());
const score = distanciaQuiQuadrado(freq, freq_portugues);
if (score < melhor_score) {
melhor_score = score;
melhor_desl = desl;
}
}
return melhor_desl;
}
Por que qui-quadrado? O teste qui-quadrado mede o quão diferente uma distribuição observada é de uma esperada. É mais robusto do que simplesmente procurar a letra mais frequente, pois considera a distribuição inteira.
Passo 6: Interface CLI
/// Exibe a tabela de frequência de um texto.
fn exibirFrequencia(texto: []const u8, writer: anytype) !void {
const freq = calcularFrequencia(texto);
try writer.print("\n--- Frequência de Letras ---\n", .{});
for (freq, 0..) |f, i| {
if (f > 0.0) {
const letra: u8 = @intCast(i + 'a');
const barras: usize = @intFromFloat(f);
try writer.print(" {c}: {d:>5.1}% ", .{ letra, f });
var b: usize = 0;
while (b < barras) : (b += 1) {
try writer.print("#", .{});
}
try writer.print("\n", .{});
}
}
}
pub fn main() !void {
const stdout = io.getStdOut().writer();
const stdin = io.getStdIn().reader();
try stdout.print(
\\
\\ ====================================
\\ Cifra de Cesar - Zig
\\ ====================================
\\
\\ [1] Encriptar texto
\\ [2] Decriptar texto
\\ [3] Quebrar cifra (forca bruta)
\\ [4] Quebrar cifra (frequencia)
\\ [5] Analisar frequencia
\\ [6] Sair
\\
, .{});
var buf_opcao: [64]u8 = undefined;
var buf_texto: [4096]u8 = undefined;
var buf_desl: [64]u8 = undefined;
while (true) {
try stdout.print("\n Opcao: ", .{});
const opcao_raw = stdin.readUntilDelimiterOrEof(&buf_opcao, '\n') catch continue orelse break;
const opcao = mem.trim(u8, opcao_raw, " \t\r\n");
if (mem.eql(u8, opcao, "6") or mem.eql(u8, opcao, "sair")) break;
if (mem.eql(u8, opcao, "1") or mem.eql(u8, opcao, "2")) {
try stdout.print(" Texto: ", .{});
const texto_raw = stdin.readUntilDelimiterOrEof(&buf_texto, '\n') catch continue orelse continue;
const texto = mem.trim(u8, texto_raw, " \t\r\n");
try stdout.print(" Deslocamento (1-25): ", .{});
const desl_raw = stdin.readUntilDelimiterOrEof(&buf_desl, '\n') catch continue orelse continue;
const desl_str = mem.trim(u8, desl_raw, " \t\r\n");
const desl = fmt.parseInt(u8, desl_str, 10) catch {
try stdout.print(" Numero invalido!\n", .{});
continue;
};
if (desl == 0 or desl > 25) {
try stdout.print(" Deslocamento deve ser entre 1 e 25.\n", .{});
continue;
}
if (mem.eql(u8, opcao, "1")) {
const resultado = encriptar(texto, desl);
try stdout.print("\n Encriptado: {s}\n", .{resultado.texto()});
} else {
const resultado = decriptar(texto, desl);
try stdout.print("\n Decriptado: {s}\n", .{resultado.texto()});
}
} else if (mem.eql(u8, opcao, "3")) {
try stdout.print(" Texto cifrado: ", .{});
const texto_raw = stdin.readUntilDelimiterOrEof(&buf_texto, '\n') catch continue orelse continue;
const texto = mem.trim(u8, texto_raw, " \t\r\n");
try quebraForcaBruta(texto, stdout);
} else if (mem.eql(u8, opcao, "4")) {
try stdout.print(" Texto cifrado: ", .{});
const texto_raw = stdin.readUntilDelimiterOrEof(&buf_texto, '\n') catch continue orelse continue;
const texto = mem.trim(u8, texto_raw, " \t\r\n");
const desl = quebraFrequencia(texto);
const resultado = decriptar(texto, desl);
try stdout.print("\n Deslocamento provavel: {d}\n", .{desl});
try stdout.print(" Texto decriptado: {s}\n", .{resultado.texto()});
} else if (mem.eql(u8, opcao, "5")) {
try stdout.print(" Texto para analisar: ", .{});
const texto_raw = stdin.readUntilDelimiterOrEof(&buf_texto, '\n') catch continue orelse continue;
const texto = mem.trim(u8, texto_raw, " \t\r\n");
try exibirFrequencia(texto, stdout);
} else {
try stdout.print(" Opcao invalida.\n", .{});
}
}
try stdout.print("\n Ate logo!\n", .{});
}
Passo 7: Testes
test "deslocar caractere basico" {
try std.testing.expectEqual(@as(u8, 'd'), deslocarCaractere('a', 3));
try std.testing.expectEqual(@as(u8, 'a'), deslocarCaractere('x', 3));
try std.testing.expectEqual(@as(u8, 'D'), deslocarCaractere('A', 3));
}
test "deslocar preserva nao-letras" {
try std.testing.expectEqual(@as(u8, ' '), deslocarCaractere(' ', 5));
try std.testing.expectEqual(@as(u8, '!'), deslocarCaractere('!', 10));
try std.testing.expectEqual(@as(u8, '3'), deslocarCaractere('3', 7));
}
test "encriptar e decriptar sao inversas" {
const original = "Hello World";
const cifrado = encriptar(original, 13);
const decifrado = decriptar(cifrado.texto(), 13);
try std.testing.expectEqualStrings(original, decifrado.texto());
}
test "ROT13 aplicado duas vezes retorna original" {
const original = "Zig e incrivel";
const rot13a = encriptar(original, 13);
const rot13b = encriptar(rot13a.texto(), 13);
try std.testing.expectEqualStrings(original, rot13b.texto());
}
test "deslocamento 0 nao altera" {
const original = "Teste";
const resultado = encriptar(original, 0);
try std.testing.expectEqualStrings(original, resultado.texto());
}
test "deslocamento 26 nao altera" {
const original = "Teste";
const resultado = encriptar(original, 26);
try std.testing.expectEqualStrings(original, resultado.texto());
}
test "quebra por frequencia" {
const original = "a programacao em zig e muito interessante e poderosa";
const cifrado = encriptar(original, 7);
const desl_encontrado = quebraFrequencia(cifrado.texto());
try std.testing.expectEqual(@as(u8, 7), desl_encontrado);
}
Compilando e Executando
zig build test
zig build run
Exemplo de Uso
Opcao: 1
Texto: Zig e uma linguagem incrivel
Deslocamento (1-25): 3
Encriptado: Clj h xpd olqjxdjhp lqfulyho
Opcao: 2
Texto: Clj h xpd olqjxdjhp lqfulyho
Deslocamento (1-25): 3
Decriptado: Zig e uma linguagem incrivel
Conceitos Aprendidos
- Aritmética modular com tipos inteiros de Zig
- Manipulação de caracteres byte a byte (
u8) - Buffers de tamanho fixo para evitar alocação
- Análise estatística (frequência, qui-quadrado)
- Testes que verificam propriedades inversas
Próximos Passos
- Aprenda mais sobre manipulação de strings em Zig
- Explore o projeto Gerador de Senhas para criptografia mais robusta
- Veja como ler arquivos para encriptar arquivos inteiros