Cifra de César em Zig — Tutorial Passo a Passo

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

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

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.