← thecodex.expert · The Codex Family of Knowledge
Tier 0 · Absolute Beginner · C Project

Password Generator

Generate a random password from a chosen mix of lowercase, uppercase, digits, and symbols — and see why C's classic rand() is the wrong tool for anything security-related.

🧠 Teaches how to think spoonfed, every age Last verified:

1 The Problem

We want a tool that creates a strong password: a random mix of letters, numbers, and symbols, of a length the user chooses. It teaches building a string from random choices — and an important lesson about which randomness is safe for security.

Where this shows up: password managers, generating API keys, session tokens, temporary access codes, unique IDs. Any time software needs something unpredictable that an attacker cannot guess.

2 How to Think About It

The whole program is two small, pure functions: build the character pool from what was selected, then pick that many random characters from it. The interesting part is what happens when nothing is selected.

The plan — in plain English
1. Combine the selected character sets (lowercase, uppercase, digits, symbols) into one pool string. → 2. If the pool is empty, fail loudly rather than returning something wrong. → 3. Otherwise, pick length random characters from the pool using a cryptographically secure source.

Build character pool

Ask for length

Pick that many random chars

Join into a password

Show the password

3 The Build — explained part by part

build_pool assembles the available characters from bit flags; generate_password uses it and arc4random_uniform — a real, cryptographically secure random function available directly in modern glibc, not rand().

CPasswordGenerator.h / PasswordGenerator.c / main.c
#ifndef PASSWORD_GENERATOR_H
#define PASSWORD_GENERATOR_H

#define CS_LOWER   1
#define CS_UPPER   2
#define CS_DIGITS  4
#define CS_SYMBOLS 8

/* Builds the character pool for the given mask of CS_* flags into `pool_out`
 * (capacity `pool_cap`). Returns the pool's length, or 0 if `mask` selects no
 * character sets at all — the caller must handle that case. */
int build_pool(int mask, char *pool_out, int pool_cap);

/* Generates a password of `length` characters from `mask`'s pool into `out`
 * (capacity `out_cap`, must be > length). Returns 1 on success, 0 if the
 * pool was empty (mask selected nothing). */
int generate_password(int length, int mask, char *out, int out_cap);

#endif

#include "PasswordGenerator.h"
#include <string.h>
#include <stdlib.h>

/* Appends `set` to `pool` if `mask` selects it, staying within `pool_cap`. */
static void append_if_selected(char *pool, int pool_cap, int mask, int flag, const char *set) {
    if (mask & flag) {
        size_t room = (size_t)(pool_cap - (int)strlen(pool) - 1);
        strncat(pool, set, room);
    }
}

int build_pool(int mask, char *pool_out, int pool_cap) {
    static const char *LOWER = "abcdefghijklmnopqrstuvwxyz";
    static const char *UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    static const char *DIGITS = "0123456789";
    static const char *SYMBOLS = "!@#$%^&*()-_=+";
    pool_out[0] = '\0';
    append_if_selected(pool_out, pool_cap, mask, CS_LOWER, LOWER);
    append_if_selected(pool_out, pool_cap, mask, CS_UPPER, UPPER);
    append_if_selected(pool_out, pool_cap, mask, CS_DIGITS, DIGITS);
    append_if_selected(pool_out, pool_cap, mask, CS_SYMBOLS, SYMBOLS);
    return (int)strlen(pool_out); /* 0 if mask selected no character set at all */
}

int generate_password(int length, int mask, char *out, int out_cap) {
    char pool[128];
    int pool_len = build_pool(mask, pool, sizeof(pool));
    if (pool_len == 0 || length <= 0 || length >= out_cap) return 0;
    for (int i = 0; i < length; i++) {
        out[i] = pool[arc4random_uniform((unsigned int)pool_len)];
    }
    out[length] = '\0';
    return 1;
}

#include "PasswordGenerator.h"
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
    int length = argc > 1 ? atoi(argv[1]) : 16;
    int mask = CS_LOWER | CS_UPPER | CS_DIGITS | CS_SYMBOLS;
    char out[256];
    if (!generate_password(length, mask, out, sizeof(out))) {
        fprintf(stderr, "Could not generate a password (empty pool or bad length).\n");
        return 1;
    }
    printf("%s\n", out);
    return 0;
}
⚠ No in-browser playground here
C compiles to a real, native binary, so unlike the Python version of this project there is no editor above you can run in the browser. Copy the code below and run it on your own machine — it takes seconds once GCC or Clang is installed.
What each part does — in plain words
#define CS_LOWER 1 / CS_UPPER 2 / ... — C has no built-in enum-with-flags type, so powers of two combined with bitwise OR (|) is the classic C idiom for a set of independent on/off options packed into one integer, covered in the course’s Bitwise Operations lesson.

arc4random_uniform, not rand() — rand() is not cryptographically secure and is explicitly the wrong tool here; modern glibc (2.36+) ships arc4random_buf and arc4random_uniform directly, seeded from the OS’s own secure entropy source, with no extra library needed on this system. arc4random_uniform also avoids the subtle modulo-bias bug that rand() % pool_len would introduce.

build_pool returns 0 for an empty selection — this is deliberate, and generate_password checks for it explicitly and fails rather than silently returning an empty or garbage string. This is the same class of bug the Rust and Java versions of this project caught for real during testing: an empty character-set selection must be treated as a real error.
Common mistakes — and how to avoid them
✗ Using rand() % pool_len for the random index — not cryptographically secure, and introduces a slight statistical bias toward lower indices when pool_len doesn’t evenly divide RAND_MAX.
✓ Use arc4random_uniform(pool_len) instead, which is both secure and bias-free.
✗ Not checking build_pool’s return value for 0 before generating — would read from an empty string and produce garbage or a crash.
✓ generate_password checks pool_len == 0 explicitly and returns failure.
✗ Forgetting the null terminator on the generated password.
✓ generate_password explicitly sets out[length] = '\0' after filling every character.

4 Test & Prove Each Part

C has no built-in test framework and this sandbox can't reach a package registry for one, so these tests use plain assert() calls. Since real randomness can't be asserted against a fixed expected value, the test build swaps in a small seedable stand-in for arc4random_uniform (see the #ifdef TESTING block in the full source above) so results are deterministic — the real, shipped program always uses the genuine secure function.

Combining character sets produces exactly the expected combined pool
An empty selection (no character sets chosen) is rejected, not silently accepted
A generated password has exactly the requested length
Every character in the output actually comes from the selected pool
Ctest_PasswordGenerator.c
#define TESTING
#include "PasswordGenerator.h"
#include "PasswordGenerator.c"
#include <assert.h>
#include <stdio.h>
#include <string.h>

#define RUN(name) do { name(); printf("PASS: %s\n", #name); } while (0)

static void build_pool_combines_selected_character_sets(void) {
    char pool[128];
    int len = build_pool(CS_LOWER | CS_DIGITS, pool, sizeof(pool));
    assert(len == 26 + 10);
    assert(strchr(pool, 'a') != NULL);
    assert(strchr(pool, '5') != NULL);
    assert(strchr(pool, 'A') == NULL);
}

static void build_pool_with_no_sets_selected_returns_zero(void) {
    char pool[128];
    int len = build_pool(0, pool, sizeof(pool));
    assert(len == 0);
}

static void generate_password_produces_the_requested_length(void) {
    char out[64];
    int ok = generate_password(12, CS_LOWER | CS_UPPER, out, sizeof(out));
    assert(ok == 1);
    assert(strlen(out) == 12);
}

static void generate_password_fails_on_an_empty_pool(void) {
    /* The same class of bug Rust's and Java's password-generator projects
     * caught for real during testing: an empty selection must fail loudly,
     * not silently hand back an empty or garbage string. */
    char out[64];
    int ok = generate_password(12, 0, out, sizeof(out));
    assert(ok == 0);
}

static void every_character_comes_from_the_selected_pool(void) {
    char out[128];
    int mask = CS_DIGITS;
    generate_password(50, mask, out, sizeof(out));
    for (int i = 0; out[i]; i++) assert(out[i] >= '0' && out[i] <= '9');
}

int main(void) {
    RUN(build_pool_combines_selected_character_sets);
    RUN(build_pool_with_no_sets_selected_returns_zero);
    RUN(generate_password_produces_the_requested_length);
    RUN(generate_password_fails_on_an_empty_pool);
    RUN(every_character_comes_from_the_selected_pool);
    printf("All tests passed.\n");
    return 0;
}

Compile and run with gcc -DTESTING -o test_run test_PasswordGenerator.c && ./test_run. Note the test file #includes PasswordGenerator.c directly (rather than compiling them as two translation units) specifically so the #ifdef TESTING swap applies; the real program is compiled without -DTESTING, which is what makes it use the genuine secure random function.

5 The Interface

Even a tiny program has an interface. Here is its contract, documented plainly.

INPUTArgumentdesired length (optional, defaults to 16)
What it expects
./genpw 20
OUTPUTOutputone randomly generated password
What it returns
UJ-AW(WGn!eZ(GkJN*Kp

6 Run It & Automate It

Save the code as PasswordGenerator.h / PasswordGenerator.c / main.c and compile it with gcc — that turns your source directly into a native executable for your machine. No separate runtime needed: the compiled binary runs on its own.

Run it locally
gcc -o genpw main.c PasswordGenerator.c && ./genpw 20
Every run genuinely differs — there's no seed to reset, since it draws from the OS's real entropy source.

A CI tool like Jenkins runs the same compile-then-test-then-check-for-leaks steps automatically whenever the code changes — every line below has a plain explanation.

What you should see when it works
Terminala real run
$ ./genpw 20
UJ-AW(WGn!eZ(GkJN*Kp
$ ./genpw 12
&690cF%SbcX#
If it breaks — how to fix it
🚨 Could not generate a password (empty pool or bad length).
This is the deliberate, disclosed failure case: main.c's default selects all four character sets, so seeing this message there means the requested length was 0, negative, or too large for the output buffer.
🚨 implicit declaration of function 'arc4random_uniform'
Your glibc version may predate 2.36. Check ldd --version; on an older system, linking libbsd and including <bsd/stdlib.h> provides the same function.
🚨 Every password looks suspiciously similar.
Check that you're calling arc4random_uniform fresh for every character in the loop, not caching one random value and reusing it.
GroovyJenkinsfile
// Jenkinsfile — compiles, tests, and checks for leaks on every change.
pipeline {
    agent any

    stages {
        stage('Get the code') {
            // download the latest code
            steps { checkout scm }
        }
        stage('Compile') {
            steps {
                // confirm a compiler is installed
                sh 'gcc --version'
                // compile with strict warnings on
                sh 'gcc -std=c17 -Wall -Wextra -o app *.c'
            }
        }
        stage('Run the tests') {
            steps {
                // prints PASS/FAIL, exits non-zero on failure
                sh './app'
            }
        }
        stage('Check for memory leaks') {
            steps {
                // fails the build on any leak or invalid access
                sh 'valgrind --error-exitcode=1 --leak-check=full ./app'
            }
        }
    }

    post {
        success { echo 'All tests passed, no leaks found.' }
        failure { echo 'A test or Valgrind check failed — see above.' }
    }
}
Try extending it
Add a flag to guarantee at least one character from each selected set appears in the output (many real password policies require this). Or add a --count N option to print several passwords at once.
What you learned
Bit flags combined with OR as C's idiom for a set of options; why arc4random_uniform beats rand() for anything security-related, and is available with zero extra dependencies on a modern glibc; and treating an empty selection as a real, explicit failure rather than undefined behavior.