Every time you type your password to log in to Linux, the system does not compare your typed password directly against a stored copy. Instead, it encrypts what you typed and compares that encrypted version against the stored encrypted version. If they match, you are in.
This part explains the one-way encryption algorithm behind Linux passwords, how the crypt() function works, what salts are and why they matter, and how to write a complete password authentication program in C — the same logic that the standard login program uses.
Imagine a special paper shredder. You put a document in, it produces a unique pile of confetti. Given the same document, it always produces the exact same confetti pattern. But given the confetti, you cannot reconstruct the original document. That is one-way encryption.
Linux password authentication works exactly on this principle:
/* The authentication flow */
/* Step 1: When you set your password */
your_password = "MySecurePass123"
stored_hash = one_way_encrypt(your_password + random_salt)
/* stored_hash is what goes into /etc/shadow */
/* Step 2: When you log in later */
typed_password = "MySecurePass123" /* (or whatever you type) */
candidate_hash = one_way_encrypt(typed_password + same_salt)
/* Step 3: Compare */
if (candidate_hash == stored_hash):
LOGIN SUCCESSFUL
else:
ACCESS DENIED
/* The system NEVER decrypts the stored hash back to plaintext */
/* It has no way to — and that is the whole point */
Even the system administrator cannot find out your password. They can only reset it. This is fundamentally different from encryption-based storage where a key could theoretically decrypt the data.
A salt is a random string mixed into your password before hashing. Without a salt, a weakness emerges: if two users have the same password, their hashes would be identical. An attacker who sees the hash file could immediately spot duplicate passwords.
Worse, an attacker could precompute the hashes of millions of common passwords (a rainbow table) and then look up any stolen hash instantly.
/* WITHOUT salt — two users with same password have same hash */
alice: hash("password123") = "a7b9c3..."
bob: hash("password123") = "a7b9c3..."
/* An attacker sees these are the same immediately */
/* WITH salt — same password produces completely different hashes */
alice: salt="Kx8r2m", hash("password123" + "Kx8r2m") = "f3a9b1..."
bob: salt="Pq7nYt", hash("password123" + "Pq7nYt") = "2c8e5d..."
/* Completely different hashes — no pattern visible */
The salt is not secret — it is stored alongside the hash in /etc/shadow. Its purpose is to make precomputation impractical, not to add secrecy. An attacker must crack each password individually, from scratch, because the salt is different for every account.
The crypt() function is the standard POSIX interface for password hashing. It takes the plaintext key and a salt string, and returns the hash.
#define _XOPEN_SOURCE /* Required to expose crypt() declaration */
#include <unistd.h>
char *crypt(const char *key, const char *salt);
/* key = the plaintext password to hash */
/* salt = the salt string (format depends on algorithm) */
/* Returns: pointer to a static string containing the hash */
/* or NULL on error */
The salt format tells crypt() which algorithm to use:
/* Salt format: $id$saltvalue$ */
/* Old DES: just 2 printable characters from [a-zA-Z0-9./] */
crypt("mypassword", "aB")
/* Returns: 13-character string starting with "aB" */
/* MD5: $1$ (outdated, avoid in new code) */
crypt("mypassword", "$1$randomsalt")
/* Returns: string starting with $1$randomsalt$ */
/* SHA-256: $5$ */
crypt("mypassword", "$5$randomsalt")
/* SHA-512: $6$ (recommended for modern Linux) */
crypt("mypassword", "$6$randomsalt")
/* Returns: 86-character hash prefixed with $6$randomsalt$ */
A crucial trick: the stored hash in /etc/shadow already contains the salt as its prefix. So when authenticating, you can pass the stored hash itself as the salt argument, and crypt() will extract the salt automatically from the first few characters.
/* Authenticating a user: pass the stored hash as the salt */
const char *stored_hash = "$6$Kx8r2mYp$LongHashString...";
const char *typed_password = "what_user_typed";
/* crypt() reads the $6$Kx8r2mYp$ prefix from stored_hash
and uses it as the salt — same salt = same hash if password matches */
char *computed = crypt(typed_password, stored_hash);
if (strcmp(computed, stored_hash) == 0)
printf("Password correct!\n");
else
printf("Wrong password!\n");
On Linux, crypt() is in a separate library. You must link against it with -lcrypt:
/* Compile command — note the -lcrypt flag */
$ gcc -o auth_program auth_program.c -lcrypt
/* Without -lcrypt you get a linker error like: */
/* undefined reference to `crypt' */
/* On newer glibc (2.28+), crypt() moved to libxcrypt */
/* The -lcrypt flag still works as libxcrypt is backward-compatible */
/* On Ubuntu/Debian you may need to install it first */
$ sudo apt install libxcrypt-dev
When a user types their password, you do not want it to appear on the screen. The getpass() function handles this by temporarily disabling terminal echo, reading the password, and then restoring normal terminal settings.
#include <unistd.h>
char *getpass(const char *prompt);
/* prompt = the string to display (e.g. "Password: ") */
/* Returns: pointer to static buffer containing the password (no newline) */
/* or NULL on error */
/* Usage */
char *typed = getpass("Enter your password: ");
/* The cursor waits, nothing is echoed as user types */
/* Returns the typed string once user presses Enter */
There is an important security responsibility that comes with getpass(). The returned buffer holds the plaintext password in memory. You must zero it out immediately after use, before losing the pointer.
/* Secure pattern: zero the cleartext immediately after hashing */
char *typed = getpass("Password: ");
/* Hash it right away */
char *hash = crypt(typed, stored_hash);
/* IMMEDIATELY overwrite the plaintext in memory */
char *p = typed;
while (*p != '\0')
*p++ = '\0';
/* Now compare the hash */
if (strcmp(hash, stored_hash) == 0)
printf("Authentication successful\n");
Why bother zeroing? The plaintext sits in memory even after you stop using it. A privileged process reading /dev/mem, or a swap file on disk, could expose it if not cleared. Zeroing immediately minimizes the window of exposure.
Here is a complete, working user authentication program that reads a username, retrieves the shadow password record, prompts for the password, and validates it using crypt(). This is essentially what the standard login(1) program does at its core.
#define _XOPEN_SOURCE /* For crypt() */
#define _BSD_SOURCE /* For getpass() on older glibc */
#define _DEFAULT_SOURCE /* For getpass() on newer glibc */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pwd.h>
#include <shadow.h>
#include <errno.h>
int main(void) {
char username[256];
struct passwd *pwd;
struct spwd *spwd;
char *typed_pw;
char *encrypted;
int auth_ok;
char *p;
/* Step 1: Ask for username */
printf("Username: ");
fflush(stdout);
if (fgets(username, sizeof(username), stdin) == NULL)
return 1;
/* Remove trailing newline from fgets */
size_t len = strlen(username);
if (len > 0 && username[len - 1] == '\n')
username[len - 1] = '\0';
/* Step 2: Get public user info from /etc/passwd */
pwd = getpwnam(username);
if (pwd == NULL) {
fprintf(stderr, "Error: no such user '%s'\n", username);
return 1;
}
/* Step 3: Get shadow password record from /etc/shadow */
errno = 0;
spwd = getspnam(username);
if (spwd == NULL) {
if (errno == EACCES) {
fprintf(stderr, "Error: permission denied reading shadow file\n");
fprintf(stderr, "Hint: run this program as root\n");
return 1;
}
/* Shadow not found — fall back to passwd field */
/* (only works if shadow is disabled, rare on modern systems) */
}
/* Use shadow password if available */
if (spwd != NULL)
pwd->pw_passwd = spwd->sp_pwdp;
/* Step 4: Read password without echoing to terminal */
typed_pw = getpass("Password: ");
if (typed_pw == NULL) {
fprintf(stderr, "Error reading password\n");
return 1;
}
/* Step 5: Encrypt the typed password using the stored hash as salt */
/* crypt() extracts the algorithm and salt from pwd->pw_passwd prefix */
encrypted = crypt(typed_pw, pwd->pw_passwd);
/* Step 6: Zero the cleartext immediately — security requirement */
for (p = typed_pw; *p != '\0'; )
*p++ = '\0';
if (encrypted == NULL) {
perror("crypt");
return 1;
}
/* Step 7: Compare encrypted result with stored hash */
auth_ok = (strcmp(encrypted, pwd->pw_passwd) == 0);
if (!auth_ok) {
printf("Authentication failed: incorrect password\n");
return 1;
}
/* Success */
printf("Authentication successful\n");
printf("Welcome %s (UID=%d)\n", pwd->pw_name, (int)pwd->pw_uid);
/* From here: do whatever privileged work needs doing */
return 0;
}
/* Compile */
$ gcc -o auth auth.c -lcrypt
/* Run as root (needed to read /etc/shadow) */
$ sudo ./auth
Username: alice
Password: (nothing echoed)
Authentication successful
Welcome alice (UID=1001)
/* Wrong password */
$ sudo ./auth
Username: alice
Password:
Authentication failed: incorrect password
Let us trace through a concrete example to make the salt mechanism crystal clear:
/* Suppose alice's shadow entry is: */
alice:$6$Kx8r2mYpABCD$VeryLongHashOf86Characters...:19500:0:90:7:::
/* Breaking down the password field */
$6$ = algorithm: SHA-512
Kx8r2mYpABCD = the salt (random, generated when password was set)
$ = separator
VeryLong... = the 86-character SHA-512 hash of ("password" + salt)
/* When alice types "wrongpassword": */
input = crypt("wrongpassword", "$6$Kx8r2mYpABCD$VeryLong...")
= "$6$Kx8r2mYpABCD$TOTALLYDIFFERENTHASH..."
compare("$6$Kx8r2mYpABCD$TOTALLYDIFFERENTHASH...",
"$6$Kx8r2mYpABCD$VeryLong...")
= NOT EQUAL → wrong password
/* When alice types "correctpassword" (the real one): */
input = crypt("correctpassword", "$6$Kx8r2mYpABCD$VeryLong...")
= "$6$Kx8r2mYpABCD$VeryLong..." /* same output */
compare(same, same) = EQUAL → login granted
After calling getpass() and then crypt(), the plaintext password is briefly alive in RAM. Several attack vectors can expose it if it is not erased immediately:
/* BAD: password sits in memory until the variable goes out of scope */
char *pw = getpass("Password: ");
char *hash = crypt(pw, stored);
/* ...pw still contains "MySecret123" here... */
if (strcmp(hash, stored) == 0) { /* ... */ }
/* ...still in memory here... */
return 0; /* ...and even here until overwritten by OS */
/* GOOD: zero immediately after hashing */
char *pw = getpass("Password: ");
char *hash = crypt(pw, stored);
for (char *p = pw; *p; p++) *p = '\0'; /* wipe NOW */
/* From this point, "MySecret123" is gone from RAM */
if (strcmp(hash, stored) == 0) { /* ... */ }
Note: some compilers may optimize away a simple memset to zero if they detect the buffer is not used afterwards. Use explicit_bzero() or SecureZeroMemory() on platforms that support it:
#include <string.h>
/* explicit_bzero is guaranteed NOT to be optimized away */
explicit_bzero(pw, strlen(pw));
/* Algorithm evolution and characteristics */
/* DES (legacy) */
salt = 2 characters from [a-zA-Z0-9./]
output = 13-character string
status = AVOID — considered cryptographically weak
/* MD5 ($1$) */
salt = up to 8 characters, prefixed with $1$
output = 34-character string starting with $1$
status = WEAK — fast to compute, easy to brute-force today
/* SHA-256 ($5$) */
salt = up to 16 characters, prefixed with $5$
output = 43-character string
status = ACCEPTABLE — much stronger than MD5
/* SHA-512 ($6$) */
salt = up to 16 characters, prefixed with $6$
output = 86-character string
status = RECOMMENDED — standard on most modern Linux distros
/* yescrypt ($y$) — newest */
status = BEST — memory-hard, designed to resist GPU cracking
Used on Ubuntu 22.04+, Fedora 35+
You can verify which algorithm your system uses by looking at the hash prefix in /etc/shadow:
$ sudo grep alice /etc/shadow | cut -d: -f2 | cut -c1-10
$6$Kx8r2m (SHA-512 — the $ 6 $ prefix tells you)
/* Or check the system default */
$ grep ENCRYPT_METHOD /etc/login.defs
ENCRYPT_METHOD SHA512
Here is everything covered across all four parts of this chapter:
/* Complete authentication recipe — summarized */
#include <pwd.h>
#include <shadow.h>
#include <unistd.h>
#include <string.h>
/* 1. Get username from user */
/* 2. getpwnam(username) — get public info */
/* 3. getspnam(username) — get encrypted hash */
/* 4. getpass("Password: ") — read without echo */
/* 5. crypt(typed, stored_hash) — hash with same salt */
/* 6. Immediately zero the cleartext */
/* 7. strcmp(result, stored_hash) — compare hashes */
/* Compile: gcc program.c -lcrypt */
/* Starter: Implement getpwnam() yourself using iteration */
struct passwd *my_getpwnam(const char *target) {
struct passwd *pwd;
setpwent();
while ((pwd = getpwent()) != NULL) {
if (strcmp(pwd->pw_name, target) == 0) {
endpwent();
return pwd;
}
}
endpwent();
return NULL; /* not found */
}
/* Test it */
struct passwd *p = my_getpwnam("alice");
if (p) printf("Found: UID=%d\n", p->pw_uid);
You have finished all four parts of Chapter 8 on Linux Users and Groups. You now understand the full stack — from configuration files to C library functions to cryptographic authentication.
