Gerador de Senhas em Zig — Tutorial Passo a Passo

Gerador de Senhas em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir um gerador de senhas seguras em Zig. Este projeto é ideal para explorar o gerador de números aleatórios criptográficos da stdlib, manipulação de caracteres e design de API configurável.

O Que Vamos Construir

Nosso gerador vai:

  • Gerar senhas com comprimento e critérios configuráveis
  • Usar entropia criptográfica do sistema operacional
  • Permitir inclusão/exclusão de tipos de caracteres (maiúsculas, números, símbolos)
  • Avaliar a força da senha gerada
  • Gerar múltiplas senhas de uma vez

Por Que Este Projeto?

Geradores de senhas parecem triviais, mas revelam decisões importantes: como obter aleatoriedade criptográfica versus pseudo-aleatória, como garantir que a senha atenda a critérios (pelo menos um dígito, um símbolo, etc.) sem comprometer a uniformidade, e como estimar entropia. Zig expõe essas decisões de forma clara.

Passo 1: Conjuntos de Caracteres

const std = @import("std");
const crypto = std.crypto;
const io = std.io;
const fmt = std.fmt;
const mem = std.mem;

/// Configuração para geração de senhas.
/// Usamos uma struct com valores padrão para permitir
/// construção parcial — o usuário pode personalizar apenas
/// o que precisa, como no padrão Builder.
const ConfigSenha = struct {
    comprimento: u32 = 16,
    usar_maiusculas: bool = true,
    usar_minusculas: bool = true,
    usar_numeros: bool = true,
    usar_simbolos: bool = true,
    excluir_ambiguos: bool = false, // Exclui 0, O, l, 1, I

    /// Retorna o conjunto de caracteres disponíveis baseado na configuração.
    pub fn caracteres(self: ConfigSenha) []const u8 {
        // Construímos o charset em comptime não é possível porque
        // depende de campos runtime. Então usamos constantes pré-definidas.
        const minusculas = "abcdefghijklmnopqrstuvwxyz";
        const minusculas_sem_ambiguo = "abcdefghjkmnpqrstuvwxyz";
        const maiusculas = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        const maiusculas_sem_ambiguo = "ABCDEFGHJKLMNPQRSTUVWXYZ";
        const numeros = "0123456789";
        const numeros_sem_ambiguo = "23456789";
        const simbolos = "!@#$%^&*()-_=+[]{}|;:,.<>?";

        // Para simplificar, retornamos o conjunto mais completo
        // e filtramos durante a geração. Em produção, seria melhor
        // construir o charset uma vez e reutilizá-lo.
        if (self.excluir_ambiguos) {
            if (self.usar_simbolos) return minusculas_sem_ambiguo ++ maiusculas_sem_ambiguo ++ numeros_sem_ambiguo ++ simbolos;
            if (self.usar_numeros) return minusculas_sem_ambiguo ++ maiusculas_sem_ambiguo ++ numeros_sem_ambiguo;
            return minusculas_sem_ambiguo ++ maiusculas_sem_ambiguo;
        }

        if (self.usar_simbolos) return minusculas ++ maiusculas ++ numeros ++ simbolos;
        if (self.usar_numeros and self.usar_maiusculas) return minusculas ++ maiusculas ++ numeros;
        if (self.usar_numeros) return minusculas ++ numeros;
        return minusculas;
    }
};

Passo 2: Geração com Entropia Criptográfica

/// Gera uma senha aleatória baseada na configuração.
///
/// Decisão de design: usamos std.crypto.random em vez de
/// um PRNG como DefaultPrng. Para senhas, a qualidade da
/// aleatoriedade importa — um PRNG determinístico poderia
/// gerar senhas previsíveis se a seed fosse comprometida.
fn gerarSenha(buf: []u8, config: ConfigSenha) []u8 {
    const charset = config.caracteres();
    const len = @min(config.comprimento, @as(u32, @intCast(buf.len)));

    for (0..len) |i| {
        // crypto.random é alimentado pelo OS (urandom/getrandom)
        // e é criptograficamente seguro.
        const idx = crypto.random.uintLessThan(usize, charset.len);
        buf[i] = charset[idx];
    }

    return buf[0..len];
}

/// Garante que a senha contenha pelo menos um caractere de cada
/// tipo solicitado. Se não contiver, substitui posições aleatórias.
///
/// Decisão: fazemos isso DEPOIS da geração para manter a
/// uniformidade máxima. Forçar posições fixas (ex: primeiro char
/// sempre maiúsculo) reduziria a entropia efetiva.
fn garantirCriterios(senha: []u8, config: ConfigSenha) void {
    if (senha.len < 4) return; // Muito curta para garantir todos os tipos

    var tem_maiuscula = false;
    var tem_minuscula = false;
    var tem_numero = false;
    var tem_simbolo = false;

    for (senha) |c| {
        if (std.ascii.isUpper(c)) tem_maiuscula = true;
        if (std.ascii.isLower(c)) tem_minuscula = true;
        if (std.ascii.isDigit(c)) tem_numero = true;
        if (!std.ascii.isAlphanumeric(c)) tem_simbolo = true;
    }

    // Substitui posições aleatórias se necessário
    if (config.usar_maiusculas and !tem_maiuscula) {
        const pos = crypto.random.uintLessThan(usize, senha.len);
        const maiusculas = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        senha[pos] = maiusculas[crypto.random.uintLessThan(usize, maiusculas.len)];
    }
    if (config.usar_minusculas and !tem_minuscula) {
        const pos = crypto.random.uintLessThan(usize, senha.len);
        const minusculas = "abcdefghijklmnopqrstuvwxyz";
        senha[pos] = minusculas[crypto.random.uintLessThan(usize, minusculas.len)];
    }
    if (config.usar_numeros and !tem_numero) {
        const pos = crypto.random.uintLessThan(usize, senha.len);
        const numeros = "0123456789";
        senha[pos] = numeros[crypto.random.uintLessThan(usize, numeros.len)];
    }
    if (config.usar_simbolos and !tem_simbolo) {
        const pos = crypto.random.uintLessThan(usize, senha.len);
        const simbolos = "!@#$%^&*()-_=+";
        senha[pos] = simbolos[crypto.random.uintLessThan(usize, simbolos.len)];
    }
}

Passo 3: Avaliação de Força

/// Nível de força da senha.
const ForcaSenha = enum {
    fraca,
    razoavel,
    forte,
    muito_forte,

    pub fn descricao(self: ForcaSenha) []const u8 {
        return switch (self) {
            .fraca => "Fraca",
            .razoavel => "Razoável",
            .forte => "Forte",
            .muito_forte => "Muito Forte",
        };
    }

    pub fn barra(self: ForcaSenha) []const u8 {
        return switch (self) {
            .fraca => "[##--------]",
            .razoavel => "[#####-----]",
            .forte => "[########--]",
            .muito_forte => "[##########]",
        };
    }
};

/// Calcula a entropia em bits de uma senha.
/// Entropia = log2(charset_size ^ length) = length * log2(charset_size)
fn calcularEntropia(senha: []const u8) f64 {
    var tem_lower = false;
    var tem_upper = false;
    var tem_digit = false;
    var tem_symbol = false;

    for (senha) |c| {
        if (std.ascii.isLower(c)) tem_lower = true;
        if (std.ascii.isUpper(c)) tem_upper = true;
        if (std.ascii.isDigit(c)) tem_digit = true;
        if (!std.ascii.isAlphanumeric(c)) tem_symbol = true;
    }

    var tamanho_charset: f64 = 0;
    if (tem_lower) tamanho_charset += 26;
    if (tem_upper) tamanho_charset += 26;
    if (tem_digit) tamanho_charset += 10;
    if (tem_symbol) tamanho_charset += 30;

    if (tamanho_charset == 0) return 0;

    const len_f: f64 = @floatFromInt(senha.len);
    return len_f * @log2(tamanho_charset);
}

/// Avalia a força da senha baseado na entropia.
fn avaliarForca(senha: []const u8) ForcaSenha {
    const entropia = calcularEntropia(senha);

    if (entropia < 40) return .fraca;
    if (entropia < 60) return .razoavel;
    if (entropia < 80) return .forte;
    return .muito_forte;
}

Passo 4: Interface Principal

pub fn main() !void {
    const stdout = io.getStdOut().writer();
    const stdin = io.getStdIn().reader();

    try stdout.print(
        \\
        \\ Gerador de Senhas Zig v1.0
        \\ ══════════════════════════
        \\
    , .{});

    var buf_entrada: [256]u8 = undefined;
    var buf_senha: [128]u8 = undefined;

    while (true) {
        try stdout.print(
            \\
            \\ [1] Gerar senha padrão (16 chars, todos os tipos)
            \\ [2] Gerar senha personalizada
            \\ [3] Gerar múltiplas senhas
            \\ [4] Sair
            \\
            \\ Opção:
        , .{});

        const linha = stdin.readUntilDelimiterOrEof(&buf_entrada, '\n') catch continue orelse break;
        const opcao = mem.trim(u8, linha, " \t\r\n");

        if (mem.eql(u8, opcao, "4") or mem.eql(u8, opcao, "sair")) break;

        if (mem.eql(u8, opcao, "1")) {
            const config = ConfigSenha{};
            const senha = gerarSenha(&buf_senha, config);
            garantirCriterios(senha, config);
            const forca = avaliarForca(senha);

            try stdout.print("\n  Senha: {s}\n", .{senha});
            try stdout.print("  Força: {s} {s}\n", .{ forca.barra(), forca.descricao() });
            try stdout.print("  Entropia: {d:.1} bits\n", .{calcularEntropia(senha)});
        } else if (mem.eql(u8, opcao, "2")) {
            try stdout.print("  Comprimento (8-64): ", .{});
            const comp_linha = stdin.readUntilDelimiterOrEof(&buf_entrada, '\n') catch continue orelse continue;
            const comp = fmt.parseInt(u32, mem.trim(u8, comp_linha, " \t\r\n"), 10) catch {
                try stdout.print("  Número inválido.\n", .{});
                continue;
            };

            if (comp < 8 or comp > 64) {
                try stdout.print("  Comprimento deve ser entre 8 e 64.\n", .{});
                continue;
            }

            const config = ConfigSenha{
                .comprimento = comp,
                .usar_simbolos = true,
                .usar_numeros = true,
                .usar_maiusculas = true,
            };

            const senha = gerarSenha(&buf_senha, config);
            garantirCriterios(senha, config);
            const forca = avaliarForca(senha);

            try stdout.print("\n  Senha: {s}\n", .{senha});
            try stdout.print("  Força: {s} {s}\n", .{ forca.barra(), forca.descricao() });
            try stdout.print("  Entropia: {d:.1} bits\n", .{calcularEntropia(senha)});
        } else if (mem.eql(u8, opcao, "3")) {
            try stdout.print("  Quantas senhas (1-20): ", .{});
            const qtd_linha = stdin.readUntilDelimiterOrEof(&buf_entrada, '\n') catch continue orelse continue;
            const qtd = fmt.parseInt(u32, mem.trim(u8, qtd_linha, " \t\r\n"), 10) catch {
                try stdout.print("  Número inválido.\n", .{});
                continue;
            };

            if (qtd < 1 or qtd > 20) {
                try stdout.print("  Quantidade deve ser entre 1 e 20.\n", .{});
                continue;
            }

            const config = ConfigSenha{};
            try stdout.print("\n", .{});

            var i: u32 = 0;
            while (i < qtd) : (i += 1) {
                const senha = gerarSenha(&buf_senha, config);
                garantirCriterios(senha, config);
                try stdout.print("  {d:>2}. {s}\n", .{ i + 1, senha });
            }
        }
    }

    try stdout.print("\nAté logo!\n", .{});
}

Passo 5: Testes

test "senha gerada tem comprimento correto" {
    var buf: [128]u8 = undefined;
    const config = ConfigSenha{ .comprimento = 20 };
    const senha = gerarSenha(&buf, config);
    try std.testing.expectEqual(@as(usize, 20), senha.len);
}

test "entropia de senha forte" {
    const entropia = calcularEntropia("aB3!xY9@kL2#");
    try std.testing.expect(entropia > 60);
}

test "avaliação de força" {
    try std.testing.expectEqual(ForcaSenha.fraca, avaliarForca("abc"));
    try std.testing.expectEqual(ForcaSenha.muito_forte, avaliarForca("aB3!xY9@kL2#mN5$pQ7&"));
}

test "charset varia com configuração" {
    const config_simples = ConfigSenha{ .usar_simbolos = false, .usar_numeros = false, .usar_maiusculas = false };
    const config_completa = ConfigSenha{};
    try std.testing.expect(config_completa.caracteres().len > config_simples.caracteres().len);
}

Compilando e Executando

zig build test
zig build run

Conceitos Aprendidos

  • Uso de std.crypto.random para aleatoriedade criptográfica
  • Structs com valores padrão como padrão Builder
  • Cálculo de entropia e segurança de senhas
  • Manipulação de caracteres ASCII com std.ascii
  • Design de API configurável

Próximos Passos

Continue aprendendo Zig

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