Build an Encrypted File Container in C

Build a TrueCrypt-style encrypted container tool in C using AES-256-XTS.

Introduction

30 Second Summary

You have sensitive files on your computer right now. Tax documents, credentials, personal notes. If someone accessed your hard drive, those files are sitting there in plain text. Disk encryption tools like VeraCrypt solve this by locking files inside password-protected containers that look like random noise to anyone without the key.

In this project, you will build your own encrypted file container tool in C from scratch. You will implement the same cryptographic architecture used by TrueCrypt and VeraCrypt to create, store files into, and extract files from password-protected containers.

What You'll Build

A command-line tool called vault that creates encrypted file containers using industry-standard cryptography: AES-256-XTS for data encryption and PBKDF2-HMAC-SHA256 for key derivation.

By the end of this project, you'll have:

  • A working CLI tool that creates, stores files into, and extracts files from password-protected encrypted containers.
  • A two-key cryptographic architecture matching real-world disk encryption systems, with a password-derived header key and a random master key.
  • A crypto module linked against OpenSSL's libcrypto library, using production-grade encryption and key derivation.
  • Secret Mission: a change-password command that re-encrypts the volume header without touching the data area, proving you understand why the two-key architecture exists.

Are there any prerequisites?

No prior C experience is required. This guide walks you through installing every tool you need, including MSYS2, GCC, and OpenSSL on Windows. Familiarity with basic programming concepts (variables, functions, loops) in any language will help you follow along.

Set Up Your C Development Environment

To build an encrypted container tool, you need a C compiler and a cryptography library. GCC compiles your C source code into an executable, and OpenSSL's libcrypto provides the encryption functions your tool calls at runtime.

In this step, you will verify that your toolchain is working, confirm VS Code can access the compiler, and create the project folder with all source files. By the end, you will have a test build that proves OpenSSL links correctly.

In this step, get ready to:
  • Verify your GCC compiler and OpenSSL library are working.
  • Create the project folder with all source files.
  • Confirm the OpenSSL library links correctly with a test build.
Verify your toolchain installation

Your MSYS2 UCRT64 environment is already installed with GCC and OpenSSL. Before writing any code, confirm both tools respond to version checks.

  • Open the MSYS2 UCRT64 terminal from your Start menu (look for the shortcut labelled MSYS2 UCRT64).
  • Verify GCC is installed by running this command:
gcc --version
  • You should see output starting with gcc.exe (Rev..., Built by MSYS2 project) followed by a version number like 16.1.0.
  • Verify OpenSSL is installed by running:
openssl version
  • You should see output like OpenSSL 3.6.3 followed by a date.

Command not found?

Make sure you opened the MSYS2 UCRT64 terminal, not the plain MSYS2 terminal. The UCRT64 environment is where GCC and OpenSSL were installed.

If you accidentally opened the wrong terminal, close it and search your Start menu for UCRT64.

Help me troubleshoot my MSYS2 UCRT64 terminal not finding gcc or openssl.

Confirm VS Code can access the compiler

Your system PATH already includes C:\msys64\ucrt64\bin, so VS Code's integrated terminal should also find gcc. This is important because you will build your project from VS Code going forward.

  • Switch back to VS Code.
  • Open the integrated terminal by pressing Ctrl+` (backtick key, usually above Tab).
  • Confirm GCC is accessible from VS Code by running:
gcc --version
  • You should see the same GCC version output as before.

VS Code terminal does not find gcc?

If you just added C:\msys64\ucrt64\bin to your system PATH, you may need to close and reopen VS Code for the change to take effect. Restart VS Code, open a new terminal, and try gcc --version again.

Help me get gcc working in my VS Code terminal on Windows.

Create the project structure and verify with a test build

Your project needs four files: a header for the crypto interface, an implementation file, the main program, and a Makefile for building. You will create them now and write a minimal test to confirm OpenSSL links correctly.

  • In the VS Code terminal, move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir vault-project
cd vault-project
  • Create the four project files by running:
touch crypto.h crypto.c vault.c Makefile
  • Confirm the files exist by running:
ls
  • You should see crypto.c, crypto.h, Makefile, and vault.c in the output.
  • Open the vault-project folder in VS Code by running:
code .

What does code . do?

The code . command opens the current directory as a workspace in VS Code. You will see all four files listed in the Explorer sidebar on the left.

  • In the VS Code Explorer sidebar, click on vault.c to open it.
  • Write a minimal test program by pasting the following code into vault.c:
#include <openssl/evp.h>
#include <stdio.h>
int main(void) {
    printf("Vault ready\n");
    return 0;
}

What does this code do?

The #include <openssl/evp.h> line pulls in OpenSSL's high-level cryptography interface. If this header is missing or the library cannot be found, the build will fail.

The printf call simply confirms the program compiled and linked correctly. This is a smoke test to prove your environment is ready before writing real crypto code.

  • Save vault.c by pressing Ctrl+S.
  • Build the test program by running this command in the VS Code terminal:
gcc -o vault vault.c -lcrypto

What does this build command do?

  • gcc -o vault compiles and names the output executable vault.
  • vault.c is the source file to compile.
  • -lcrypto tells the linker to link against OpenSSL's libcrypto library, which provides the EVP functions your tool will use.
  • If the build succeeds (no output, no errors), run the program:
./vault
  • You should see Vault ready printed in the terminal.

Seeing linker errors about -lcrypto?

This means the OpenSSL library was not found by the linker. Make sure you are running the build command in a terminal that has C:\msys64\ucrt64\bin in its PATH.

If you are using the VS Code terminal and it cannot find the library, try running the build from the MSYS2 UCRT64 terminal instead. Navigate to your project folder with cd ~/Desktop/vault-project and retry the gcc command.

Help me fix OpenSSL linker errors when building with gcc on Windows MSYS2.

✔️ Awesome, I've got everything!

Great. Double check you have saved vault.c and that ./vault prints Vault ready with no errors.

ⓧ I'd like to double check the full code

Here is the complete vault.c file at this point:

#include <openssl/evp.h>
#include <stdio.h>
int main(void) {
    printf("Vault ready\n");
    return 0;
}

The other three files (crypto.h, crypto.c, Makefile) are empty for now. You will fill them in during the next steps.

Your environment is ready and OpenSSL links without errors. Next up, you will build the cryptographic module that powers your encrypted container.

Build the Cryptographic Module

Your development environment is ready. Now it is time to build the cryptographic engine that powers your entire vault tool.

Real encryption tools never invent their own crypto algorithms. Instead, they wrap battle-tested libraries into a clean interface. In this step, you will build a crypto module that wraps OpenSSL's libcrypto library, giving your vault access to PBKDF2 key derivation and AES-256-XTS sector encryption.

In this step, get ready to:
  • Define the crypto interface with constants and function declarations.
  • Implement PBKDF2 key derivation and AES-256-XTS encrypt/decrypt functions.
  • Run a self-test that proves encrypt/decrypt roundtrips successfully.
Define the crypto interface

A header file in C declares what functions exist and what constants your code can use. Other files include this header to access those declarations without needing to see the implementation.

  • Open crypto.h in VS Code and replace its contents with the following code:
#ifndef CRYPTO_H
#define CRYPTO_H

#include <stdint.h>
#include <stddef.h>

/* Size of the random salt in bytes */
#define SALT_SIZE 64
/* XTS requires 512-bit key (two 256-bit keys internally) */
#define KEY_SIZE 64
/* Standard disk sector size */
#define SECTOR_SIZE 512
/* XTS tweak is 16 bytes (128 bits) */
#define TWEAK_SIZE 16
/* Iterations for PBKDF2 key derivation */
#define PBKDF2_ITERATIONS 200000
/* Total header size (salt + encrypted header data) */
#define HEADER_SIZE 512
/* Magic bytes to verify correct decryption */
#define MAGIC "VAULT1"
#define MAGIC_SIZE 6

int derive_key(const char *password, const unsigned char *salt,
               unsigned char *key_out);

int xts_encrypt(const unsigned char *key, uint64_t sector_num,
                const unsigned char *plaintext, size_t len,
                unsigned char *ciphertext);

int xts_decrypt(const unsigned char *key, uint64_t sector_num,
                const unsigned char *ciphertext, size_t len,
                unsigned char *plaintext);

int generate_random(unsigned char *buf, size_t len);

#endif

What does this code do?

The #ifndef CRYPTO_H / #define CRYPTO_H / #endif pattern is called an include guard. It prevents the compiler from processing this file twice if multiple source files include it.

KEY_SIZE is 64 bytes (512 bits) because AES-256-XTS internally splits the key into two 256-bit halves. One half encrypts data and the other encrypts the tweak value that makes each sector unique.

PBKDF2_ITERATIONS is set to 200,000. This controls how expensive it is to derive a key from a password. Higher iterations make brute-force attacks slower.

  • Save crypto.h.
  • You will not see any output yet. This header defines the contract that crypto.c will implement in the next substep.

✔️ Awesome, I've got everything!

Great. Make sure you have saved crypto.h before moving on.

ⓧ I'd like to double check the full code

#ifndef CRYPTO_H
#define CRYPTO_H

#include <stdint.h>
#include <stddef.h>

#define SALT_SIZE 64
#define KEY_SIZE 64
#define SECTOR_SIZE 512
#define TWEAK_SIZE 16
#define PBKDF2_ITERATIONS 200000
#define HEADER_SIZE 512
#define MAGIC "VAULT1"
#define MAGIC_SIZE 6

int derive_key(const char *password, const unsigned char *salt,
               unsigned char *key_out);

int xts_encrypt(const unsigned char *key, uint64_t sector_num,
                const unsigned char *plaintext, size_t len,
                unsigned char *ciphertext);

int xts_decrypt(const unsigned char *key, uint64_t sector_num,
                const unsigned char *ciphertext, size_t len,
                unsigned char *plaintext);

int generate_random(unsigned char *buf, size_t len);

#endif
Implement the crypto functions

Now you will write the implementations that back those declarations. Each function wraps a specific OpenSSL call so the rest of your project never touches the library directly.

  • Open crypto.c in VS Code and add the includes and the key derivation function:
#include "crypto.h"
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <string.h>

int derive_key(const char *password, const unsigned char *salt,
               unsigned char *key_out)
{
    /* Use PBKDF2 with HMAC-SHA256 to stretch the password into a full-length key */
    return PKCS5_PBKDF2_HMAC(password, (int)strlen(password),
                             salt, SALT_SIZE,
                             PBKDF2_ITERATIONS,
                             EVP_sha256(),
                             KEY_SIZE, key_out);
}

What does this code do?

PKCS5_PBKDF2_HMAC takes a human-readable password and runs it through 200,000 rounds of HMAC-SHA256 with a random salt. The output is a 64-byte key that is computationally expensive to brute-force.

The salt ensures that two users with the same password produce different derived keys. You will generate a fresh salt each time a container is created.

  • Save crypto.c. You will not see output yet. The next chunk adds the encryption function that uses this derived key.
  • Below derive_key() in crypto.c, add the XTS encryption function:
int xts_encrypt(const unsigned char *key, uint64_t sector_num,
                const unsigned char *plaintext, size_t len,
                unsigned char *ciphertext)
{
    EVP_CIPHER_CTX *ctx;
    unsigned char tweak[TWEAK_SIZE];
    int outlen;

    /* Build the tweak from the sector number (little-endian) */
    memset(tweak, 0, TWEAK_SIZE);
    memcpy(tweak, &sector_num, sizeof(sector_num));

    ctx = EVP_CIPHER_CTX_new();
    if (!ctx) return 0;

    /* Initialize AES-256-XTS encryption with the key and sector tweak */
    if (EVP_EncryptInit_ex(ctx, EVP_aes_256_xts(), NULL, key, tweak) != 1)
        goto err;
    /* XTS requires one single Update call per sector */
    if (EVP_EncryptUpdate(ctx, ciphertext, &outlen, plaintext, (int)len) != 1)
        goto err;

    EVP_CIPHER_CTX_free(ctx);
    return 1;

err:
    EVP_CIPHER_CTX_free(ctx);
    return 0;
}

What is XTS mode and why use a tweak?

XTS is a disk encryption mode designed for sector-level encryption. The tweak is a 16-byte value (built from the sector number) that makes every sector produce different ciphertext even if the plaintext is identical. This prevents an attacker from spotting duplicate sectors.

OpenSSL's XTS requires exactly one EVP_EncryptUpdate call per encryption initialization. You cannot stream multiple sectors through the same context without reinitializing.

  • Save crypto.c. The matching decryption function comes next.
  • Below xts_encrypt() in crypto.c, add the decryption function and the random byte generator:
int xts_decrypt(const unsigned char *key, uint64_t sector_num,
                const unsigned char *ciphertext, size_t len,
                unsigned char *plaintext)
{
    EVP_CIPHER_CTX *ctx;
    unsigned char tweak[TWEAK_SIZE];
    int outlen;

    /* Build the same tweak used during encryption */
    memset(tweak, 0, TWEAK_SIZE);
    memcpy(tweak, &sector_num, sizeof(sector_num));

    ctx = EVP_CIPHER_CTX_new();
    if (!ctx) return 0;

    /* Initialize AES-256-XTS decryption */
    if (EVP_DecryptInit_ex(ctx, EVP_aes_256_xts(), NULL, key, tweak) != 1)
        goto err;
    if (EVP_DecryptUpdate(ctx, plaintext, &outlen, ciphertext, (int)len) != 1)
        goto err;

    EVP_CIPHER_CTX_free(ctx);
    return 1;

err:
    EVP_CIPHER_CTX_free(ctx);
    return 0;
}

int generate_random(unsigned char *buf, size_t len)
{
    /* RAND_bytes produces cryptographically secure random data */
    return RAND_bytes(buf, (int)len);
}

What does this code do?

xts_decrypt() mirrors xts_encrypt() exactly, using EVP_DecryptInit_ex and EVP_DecryptUpdate instead. The same key and sector number must be provided for decryption to succeed.

generate_random() calls OpenSSL's RAND_bytes to produce cryptographically secure random data. You will use this for salts and master keys.

  • Save crypto.c.
  • Verify the file compiles without errors by running this in your terminal:
gcc -Wall -O2 -std=c11 -c crypto.c
  • You should see no output. If the command returns silently, your crypto module compiled successfully.

Seeing compile errors?

  • If you see fatal error: openssl/evp.h: No such file or directory, make sure C:\msys64\ucrt64\bin is in your system PATH and you installed mingw-w64-ucrt-x86_64-openssl in Step 1.
  • If you see undeclared identifier errors on functions like PKCS5_PBKDF2_HMAC, check that #include <openssl/evp.h> is present at the top of crypto.c.
  • If you see type errors on uint64_t, verify that crypto.h includes <stdint.h>.

Help me fix OpenSSL compile errors in my crypto.c file

✔️ Awesome, I've got everything!

Great. Make sure crypto.c is saved before continuing.

ⓧ I'd like to double check the full code

#include "crypto.h"
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <string.h>

int derive_key(const char *password, const unsigned char *salt,
               unsigned char *key_out)
{
    return PKCS5_PBKDF2_HMAC(password, (int)strlen(password),
                             salt, SALT_SIZE,
                             PBKDF2_ITERATIONS,
                             EVP_sha256(),
                             KEY_SIZE, key_out);
}

int xts_encrypt(const unsigned char *key, uint64_t sector_num,
                const unsigned char *plaintext, size_t len,
                unsigned char *ciphertext)
{
    EVP_CIPHER_CTX *ctx;
    unsigned char tweak[TWEAK_SIZE];
    int outlen;

    memset(tweak, 0, TWEAK_SIZE);
    memcpy(tweak, &sector_num, sizeof(sector_num));

    ctx = EVP_CIPHER_CTX_new();
    if (!ctx) return 0;

    if (EVP_EncryptInit_ex(ctx, EVP_aes_256_xts(), NULL, key, tweak) != 1)
        goto err;
    if (EVP_EncryptUpdate(ctx, ciphertext, &outlen, plaintext, (int)len) != 1)
        goto err;

    EVP_CIPHER_CTX_free(ctx);
    return 1;

err:
    EVP_CIPHER_CTX_free(ctx);
    return 0;
}

int xts_decrypt(const unsigned char *key, uint64_t sector_num,
                const unsigned char *ciphertext, size_t len,
                unsigned char *plaintext)
{
    EVP_CIPHER_CTX *ctx;
    unsigned char tweak[TWEAK_SIZE];
    int outlen;

    memset(tweak, 0, TWEAK_SIZE);
    memcpy(tweak, &sector_num, sizeof(sector_num));

    ctx = EVP_CIPHER_CTX_new();
    if (!ctx) return 0;

    if (EVP_DecryptInit_ex(ctx, EVP_aes_256_xts(), NULL, key, tweak) != 1)
        goto err;
    if (EVP_DecryptUpdate(ctx, plaintext, &outlen, ciphertext, (int)len) != 1)
        goto err;

    EVP_CIPHER_CTX_free(ctx);
    return 1;

err:
    EVP_CIPHER_CTX_free(ctx);
    return 0;
}

int generate_random(unsigned char *buf, size_t len)
{
    return RAND_bytes(buf, (int)len);
}
Test the crypto module

The best way to confirm your crypto functions work is a roundtrip test. You will encrypt a known buffer, decrypt it, and check that the output matches the original.

  • Open vault.c and replace its entire contents with this self-test program:
/* crypto self-test version */
#include <stdio.h>
#include <string.h>
#include "crypto.h"

int main(void) {
    unsigned char salt[SALT_SIZE];
    unsigned char key[KEY_SIZE];
    unsigned char plaintext[SECTOR_SIZE];
    unsigned char ciphertext[SECTOR_SIZE];
    unsigned char recovered[SECTOR_SIZE];

    /* Generate a random salt for key derivation */
    generate_random(salt, SALT_SIZE);
    /* Fill a sector with a known pattern */
    memset(plaintext, 0xAB, SECTOR_SIZE);
    /* Derive a key from a test password */
    derive_key("testpassword", salt, key);
    /* Encrypt the sector, then decrypt it */
    xts_encrypt(key, 1, plaintext, SECTOR_SIZE, ciphertext);
    xts_decrypt(key, 1, ciphertext, SECTOR_SIZE, recovered);
    /* Compare original and recovered data */
    if (memcmp(plaintext, recovered, SECTOR_SIZE) == 0)
        printf("Crypto self-test PASSED\n");
    else
        printf("Crypto self-test FAILED\n");
    return 0;
}

What does this code do?

The test fills a 512-byte buffer with the byte 0xAB (a recognizable pattern), encrypts it with XTS using sector number 1, then decrypts the ciphertext using the same key and sector number.

If memcmp returns 0, the recovered data is byte-for-byte identical to the original. This proves the entire chain works: key derivation, encryption, and decryption.

  • Save vault.c.
  • Build the full program by running:
gcc -Wall -O2 -std=c11 -o vault vault.c crypto.c -lcrypto
  • Run the self-test:
./vault
  • You should see Crypto self-test PASSED printed in the terminal.

Seeing FAILED or a linker error?

  • If you see a linker error mentioning undefined references to PKCS5_PBKDF2_HMAC or EVP_ functions, make sure -lcrypto is at the end of the build command (after the source files).
  • If the test prints Crypto self-test FAILED, double check that xts_decrypt uses EVP_DecryptInit_ex and EVP_DecryptUpdate (not the encrypt versions).
  • If you see warnings about implicit function declarations, ensure vault.c includes "crypto.h" with quotes (not angle brackets).

Help me fix build errors in my vault crypto module

✔️ Awesome, I've got everything!

Great. Your crypto module is working. Make sure all three files are saved.

ⓧ I'd like to double check the full code

/* crypto self-test version */
#include <stdio.h>
#include <string.h>
#include "crypto.h"

int main(void) {
    unsigned char salt[SALT_SIZE];
    unsigned char key[KEY_SIZE];
    unsigned char plaintext[SECTOR_SIZE];
    unsigned char ciphertext[SECTOR_SIZE];
    unsigned char recovered[SECTOR_SIZE];

    generate_random(salt, SALT_SIZE);
    memset(plaintext, 0xAB, SECTOR_SIZE);
    derive_key("testpassword", salt, key);
    xts_encrypt(key, 1, plaintext, SECTOR_SIZE, ciphertext);
    xts_decrypt(key, 1, ciphertext, SECTOR_SIZE, recovered);
    if (memcmp(plaintext, recovered, SECTOR_SIZE) == 0)
        printf("Crypto self-test PASSED\n");
    else
        printf("Crypto self-test FAILED\n");
    return 0;
}

Your cryptographic foundation is solid. Next up, you will use these functions to build encrypted containers with a proper volume header and two-key architecture.

Implement Container Creation

Your crypto module can derive keys, encrypt sectors, and generate random data. Now you need a way to package all of that into a single protected file.

In this step, you will build the create command. It generates an encrypted container file with a volume header that stores a randomly generated master key, protected by the user's password.

In this step, get ready to:
  • Define the VaultHeader struct that holds encrypted metadata.
  • Implement the cmd_create() function that writes a complete container file.
  • Test container creation by building a 1 MB encrypted vault.
Define the volume header format

Every encrypted container needs a header that stores the information required to unlock it. Your container uses a two-key architecture, just like VeraCrypt.

What is the two-key architecture?

The user's password never directly encrypts the data. Instead, it derives a header key (via PBKDF2) that encrypts and decrypts the volume header. Inside the header sits a randomly generated master key that encrypts and decrypts the actual data.

This means you can change your password without re-encrypting the entire data area. Only the header needs re-encryption with the new password-derived key.

The container layout on disk is:

  • Salt: 64 bytes of random data used for key derivation.
  • Encrypted Header: 448 bytes containing the master key and metadata, encrypted with the header key.
  • Data Area: user-specified size, filled with random bytes at creation time.
  • In vault.c, select all the existing self-test code and replace it with the following includes and struct definition:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "crypto.h"

/* Pack the struct to ensure no padding bytes are inserted */
#pragma pack(push, 1)
typedef struct {
    char magic[MAGIC_SIZE];           /* "VAULT1" identifier */
    uint16_t version;                 /* format version number */
    unsigned char master_key[KEY_SIZE]; /* 64-byte random master key */
    uint64_t data_size;               /* size of the data area in bytes */
    unsigned char reserved[368];      /* padding to fill 448 bytes total */
} VaultHeader;
#pragma pack(pop)

/* Helper to read a password from stdin */
static int read_password(const char *prompt, char *buf, size_t bufsize)
{
    printf("%s", prompt);
    if (!fgets(buf, (int)bufsize, stdin)) return 0;
    size_t len = strlen(buf);
    if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0';
    return 1;
}

What does this code do?

The VaultHeader struct defines the 448-byte metadata block stored inside every container. The #pragma pack(push, 1) directive tells the compiler to use no padding between fields. This is critical because the struct is encrypted as a raw byte buffer and must be exactly 448 bytes.

The read_password() helper prints a prompt and reads a line from stdin, stripping the trailing newline. You will use this whenever the tool needs a password from the user.

  • Save vault.c.

You will not see any output yet. The next chunk adds the function that uses this struct.

Implement cmd_create()

The cmd_create() function is the heart of container creation. It asks the user for a password, generates random cryptographic material, encrypts the header, and writes the entire container to disk.

  • In vault.c, add the following cmd_create() function and main() below read_password():
static int cmd_create(const char *filepath, uint64_t size_mb)
{
    char password[256];
    char confirm[256];
    unsigned char salt[SALT_SIZE];
    unsigned char header_key[KEY_SIZE];
    unsigned char master_key[KEY_SIZE];
    VaultHeader header;
    unsigned char header_buf[HEADER_SIZE - SALT_SIZE];
    unsigned char encrypted_header[HEADER_SIZE - SALT_SIZE];
    FILE *fp;
    uint64_t data_size = size_mb * 1024 * 1024;

    /* Prompt for password with confirmation */
    if (!read_password("Enter password: ", password, sizeof(password))) return 1;
    if (!read_password("Confirm password: ", confirm, sizeof(confirm))) return 1;
    if (strcmp(password, confirm) != 0) {
        fprintf(stderr, "Error: Passwords do not match.\n");
        return 1;
    }

    printf("Creating %llu MB encrypted container...\n",
           (unsigned long long)size_mb);

    /* Generate random salt and master key */
    if (!generate_random(salt, SALT_SIZE)) {
        fprintf(stderr, "Error: Failed to generate random salt.\n");
        return 1;
    }
    if (!generate_random(master_key, KEY_SIZE)) {
        fprintf(stderr, "Error: Failed to generate master key.\n");
        return 1;
    }

    /* Derive the header key from the password + salt */
    printf("Deriving key (this may take a moment)...\n");
    if (!derive_key(password, salt, header_key)) {
        fprintf(stderr, "Error: Key derivation failed.\n");
        return 1;
    }

    /* Build the header struct with magic, version, and master key */
    memset(&header, 0, sizeof(header));
    memcpy(header.magic, MAGIC, MAGIC_SIZE);
    header.version = 1;
    memcpy(header.master_key, master_key, KEY_SIZE);
    header.data_size = data_size;

    /* Encrypt the header with XTS using sector 0 */
    memset(header_buf, 0, sizeof(header_buf));
    memcpy(header_buf, &header, sizeof(header));
    if (!xts_encrypt(header_key, 0, header_buf, sizeof(header_buf),
                     encrypted_header)) {
        fprintf(stderr, "Error: Header encryption failed.\n");
        return 1;
    }

    /* Open the container file for writing */
    fp = fopen(filepath, "wb");
    if (!fp) {
        fprintf(stderr, "Error: Cannot create file '%s'.\n", filepath);
        return 1;
    }

    /* Write salt and encrypted header */
    fwrite(salt, 1, SALT_SIZE, fp);
    fwrite(encrypted_header, 1, sizeof(encrypted_header), fp);

    /* Fill the data area with random bytes (sector by sector) */
    {
        unsigned char sector[SECTOR_SIZE];
        uint64_t sectors = data_size / SECTOR_SIZE;
        for (uint64_t i = 0; i < sectors; i++) {
            generate_random(sector, SECTOR_SIZE);
            fwrite(sector, 1, SECTOR_SIZE, fp);
            if ((i % 2048) == 0) {
                printf("\r  Progress: %llu%%",
                       (unsigned long long)(i * 100 / sectors));
                fflush(stdout);
            }
        }
    }
    printf("\r  Progress: 100%%\n");

    fclose(fp);

    /* Wipe all sensitive data from memory */
    memset(password, 0, sizeof(password));
    memset(confirm, 0, sizeof(confirm));
    memset(header_key, 0, KEY_SIZE);
    memset(master_key, 0, KEY_SIZE);
    memset(&header, 0, sizeof(header));

    printf("Container created successfully: %s\n", filepath);
    return 0;
}

int main(int argc, char *argv[])
{
    if (argc != 4 || strcmp(argv[1], "create") != 0) {
        printf("Usage: vault create <file> <size_mb>\n");
        return 1;
    }
    uint64_t size_mb = (uint64_t)atoi(argv[3]);
    if (size_mb == 0) {
        fprintf(stderr, "Error: Size must be at least 1 MB.\n");
        return 1;
    }
    return cmd_create(argv[2], size_mb);
}

What does this code do?

The function follows a strict sequence that mirrors how real disk encryption tools work:

  • Password confirmation prevents typos from locking you out of your own container.
  • generate_random() creates a cryptographically secure salt and master key. The master key is the real secret that protects your data.
  • derive_key() runs PBKDF2 with 200,000 iterations. This makes brute-force attacks expensive.
  • xts_encrypt() at sector 0 encrypts the header (which contains the master key). Only someone with the correct password can recover it.
  • The data area is filled with random bytes so that used and unused space are indistinguishable.
  • memset() wipes passwords and keys from memory as soon as they are no longer needed.

The main() function validates command-line arguments and dispatches to cmd_create().

  • Save vault.c.

You will not see output yet. The next substep builds and runs the tool.

Seeing red squiggles in VS Code?

The C/C++ extension may flag uint64_t or the OpenSSL types because it does not know the include paths. The code will still compile from the terminal. You can safely ignore these editor warnings.

Why does VS Code show errors even though GCC compiles fine?

Test container creation

Time to build the updated vault.c and create your first encrypted container.

  • Build the project by running this command:
gcc -Wall -O2 -std=c11 -o vault vault.c crypto.c -lcrypto
  • Create a 1 MB encrypted container by running:
./vault create test.vault 1

What will happen when you run this?

The tool will prompt you for a password twice (enter and confirm). After you type your password and press Enter both times, it will derive a key (which takes a moment due to the 200,000 PBKDF2 iterations), then write the container with a progress indicator.

  • Enter a password when prompted (and confirm it).
  • Verify the container file size by running:
ls -l test.vault

You should see a file size of 1049088 bytes. That is 512 bytes (header) plus 1,048,576 bytes (1 MB data area).

Why 1,049,088 bytes?

The container stores its 64-byte salt and 448-byte encrypted header before the data area begins. 64 + 448 = 512 bytes of overhead. The data area for a 1 MB container is 1,048,576 bytes. Total: 512 + 1,048,576 = 1,049,088.

Build failed with linker errors?

Make sure you are running the command from the UCRT64 terminal or from a terminal where C:\msys64\ucrt64\bin is on your PATH. The -lcrypto flag tells GCC to link against the OpenSSL library installed in that environment.

If you see "undefined reference to" errors for OpenSSL functions, confirm the OpenSSL package is installed by running pacman -Qs openssl in the UCRT64 terminal.

Help me fix linker errors with OpenSSL on MSYS2

✔️ Awesome, I've got everything!

Great. Double check you have saved vault.c and that test.vault exists at 1,049,088 bytes.

ⓧ I'd like to double check the full code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "crypto.h"

#pragma pack(push, 1)
typedef struct {
    char magic[MAGIC_SIZE];
    uint16_t version;
    unsigned char master_key[KEY_SIZE];
    uint64_t data_size;
    unsigned char reserved[368];
} VaultHeader;
#pragma pack(pop)

static int read_password(const char *prompt, char *buf, size_t bufsize)
{
    printf("%s", prompt);
    if (!fgets(buf, (int)bufsize, stdin)) return 0;
    size_t len = strlen(buf);
    if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0';
    return 1;
}

static int cmd_create(const char *filepath, uint64_t size_mb)
{
    char password[256];
    char confirm[256];
    unsigned char salt[SALT_SIZE];
    unsigned char header_key[KEY_SIZE];
    unsigned char master_key[KEY_SIZE];
    VaultHeader header;
    unsigned char header_buf[HEADER_SIZE - SALT_SIZE];
    unsigned char encrypted_header[HEADER_SIZE - SALT_SIZE];
    FILE *fp;
    uint64_t data_size = size_mb * 1024 * 1024;

    if (!read_password("Enter password: ", password, sizeof(password))) return 1;
    if (!read_password("Confirm password: ", confirm, sizeof(confirm))) return 1;
    if (strcmp(password, confirm) != 0) {
        fprintf(stderr, "Error: Passwords do not match.\n");
        return 1;
    }

    printf("Creating %llu MB encrypted container...\n",
           (unsigned long long)size_mb);

    if (!generate_random(salt, SALT_SIZE)) {
        fprintf(stderr, "Error: Failed to generate random salt.\n");
        return 1;
    }
    if (!generate_random(master_key, KEY_SIZE)) {
        fprintf(stderr, "Error: Failed to generate master key.\n");
        return 1;
    }

    printf("Deriving key (this may take a moment)...\n");
    if (!derive_key(password, salt, header_key)) {
        fprintf(stderr, "Error: Key derivation failed.\n");
        return 1;
    }

    memset(&header, 0, sizeof(header));
    memcpy(header.magic, MAGIC, MAGIC_SIZE);
    header.version = 1;
    memcpy(header.master_key, master_key, KEY_SIZE);
    header.data_size = data_size;

    memset(header_buf, 0, sizeof(header_buf));
    memcpy(header_buf, &header, sizeof(header));
    if (!xts_encrypt(header_key, 0, header_buf, sizeof(header_buf),
                     encrypted_header)) {
        fprintf(stderr, "Error: Header encryption failed.\n");
        return 1;
    }

    fp = fopen(filepath, "wb");
    if (!fp) {
        fprintf(stderr, "Error: Cannot create file '%s'.\n", filepath);
        return 1;
    }

    fwrite(salt, 1, SALT_SIZE, fp);
    fwrite(encrypted_header, 1, sizeof(encrypted_header), fp);

    {
        unsigned char sector[SECTOR_SIZE];
        uint64_t sectors = data_size / SECTOR_SIZE;
        for (uint64_t i = 0; i < sectors; i++) {
            generate_random(sector, SECTOR_SIZE);
            fwrite(sector, 1, SECTOR_SIZE, fp);
            if ((i % 2048) == 0) {
                printf("\r  Progress: %llu%%",
                       (unsigned long long)(i * 100 / sectors));
                fflush(stdout);
            }
        }
    }
    printf("\r  Progress: 100%%\n");

    fclose(fp);

    memset(password, 0, sizeof(password));
    memset(confirm, 0, sizeof(confirm));
    memset(header_key, 0, KEY_SIZE);
    memset(master_key, 0, KEY_SIZE);
    memset(&header, 0, sizeof(header));

    printf("Container created successfully: %s\n", filepath);
    return 0;
}

int main(int argc, char *argv[])
{
    if (argc != 4 || strcmp(argv[1], "create") != 0) {
        printf("Usage: vault create <file> <size_mb>\n");
        return 1;
    }
    uint64_t size_mb = (uint64_t)atoi(argv[3]);
    if (size_mb == 0) {
        fprintf(stderr, "Error: Size must be at least 1 MB.\n");
        return 1;
    }
    return cmd_create(argv[2], size_mb);
}

Your container is sealed. Next, you will build the commands that unlock it, store files inside, and extract them back out.

Implement Unlock, Store, and Extract

Your container is created and sitting on disk as a blob of encrypted bytes. Now you need a way to get data in and out of it.

In this step, you will build three functions that form the core of the vault tool. First, an unlock_header() function that reads a container's header, decrypts it using the password, and verifies the magic bytes. Then cmd_store() and cmd_extract() use the unlocked master key to encrypt files into the container and decrypt them back out.

In this step, get ready to:
  • Implement the header-unlocking function that verifies the password.
  • Build store and extract commands that encrypt and decrypt files sector-by-sector.
  • Run an end-to-end test proving a file survives the store-delete-extract cycle.
Implement unlock_header()

Every command that reads from a container needs to unlock the header first. This function reverses what cmd_create() did: it reads the salt from disk, re-derives the header key from the password, decrypts the header, and checks for the VAULT1 magic bytes to confirm the password is correct.

  • In vault.c, add the following unlock_header() function directly below cmd_create():
static int unlock_header(const char *filepath, const char *password,
                         VaultHeader *header_out, FILE **fp_out)
{
    unsigned char salt[SALT_SIZE];
    unsigned char header_key[KEY_SIZE];
    unsigned char encrypted_header[HEADER_SIZE - SALT_SIZE];
    unsigned char decrypted[HEADER_SIZE - SALT_SIZE];
    FILE *fp;

    /* Open for read+write if possible, fall back to read-only */
    fp = fopen(filepath, "r+b");
    if (!fp) {
        fp = fopen(filepath, "rb");
        if (!fp) {
            fprintf(stderr, "Error: Cannot open file '%s'.\n", filepath);
            return 0;
        }
    }

    /* Read the salt and encrypted header from the first 512 bytes */
    if (fread(salt, 1, SALT_SIZE, fp) != SALT_SIZE ||
        fread(encrypted_header, 1, sizeof(encrypted_header), fp)
            != sizeof(encrypted_header)) {
        fprintf(stderr, "Error: File too small or corrupted.\n");
        fclose(fp);
        return 0;
    }

    /* Re-derive the header key from password + salt */
    printf("Deriving key...\n");
    if (!derive_key(password, salt, header_key)) {
        fprintf(stderr, "Error: Key derivation failed.\n");
        fclose(fp);
        return 0;
    }

    /* Decrypt the header using XTS with sector number 0 */
    if (!xts_decrypt(header_key, 0, encrypted_header,
                     sizeof(encrypted_header), decrypted)) {
        fprintf(stderr, "Error: Header decryption failed.\n");
        fclose(fp);
        return 0;
    }

    /* Check magic bytes to verify the password was correct */
    memcpy(header_out, decrypted, sizeof(VaultHeader));
    if (memcmp(header_out->magic, MAGIC, MAGIC_SIZE) != 0) {
        fprintf(stderr, "Error: Wrong password or corrupted container.\n");
        memset(header_out, 0, sizeof(VaultHeader));
        fclose(fp);
        return 0;
    }

    /* Wipe sensitive key material from memory */
    memset(header_key, 0, KEY_SIZE);
    memset(decrypted, 0, sizeof(decrypted));

    /* Return the file pointer if the caller needs it, otherwise close */
    if (fp_out) {
        *fp_out = fp;
    } else {
        fclose(fp);
    }
    return 1;
}

What does this code do?

This function is the gatekeeper for every container operation. It proves that the user knows the password by checking whether decryption produces the VAULT1 magic string.

  • It first tries r+b mode (read+write) because store needs to write back into the container. If that fails, it falls back to rb (read-only).
  • The fp_out parameter lets callers keep the file open for further I/O. If they pass NULL, the function closes the file after unlocking.
  • After verification, the header key and decrypted buffer are wiped with memset() so they do not linger on the stack.
  • Save vault.c.

Why check magic bytes instead of a MAC?

A dedicated MAC (like HMAC) over the header would be another valid approach. This project uses the same method as VeraCrypt: if decryption produces a known magic string, the password was correct. If the password is wrong, the decrypted bytes are random garbage and the probability of accidentally matching 6 magic bytes is 1 in 281 trillion.

Seeing a compile error about undeclared identifiers?

Make sure unlock_header() appears above cmd_store() in the file. In C, functions must be declared or defined before they are called.

Help me fix a compile error about undeclared functions in vault.c

Implement cmd_store() and cmd_extract()

With unlock_header() in place, you can build the two data commands. Both use the master key from the decrypted header to encrypt or decrypt file data sector-by-sector.

  • Add the cmd_store() function below unlock_header() in vault.c:
static int cmd_store(const char *container_path, const char *input_path)
{
    char password[256];
    VaultHeader header;
    FILE *container_fp;
    FILE *input_fp;
    unsigned char *file_data = NULL;
    unsigned char sector[SECTOR_SIZE];
    unsigned char encrypted[SECTOR_SIZE];
    uint64_t file_size, offset;
    uint16_t name_len;
    const char *filename;

    if (!read_password("Enter password: ", password, sizeof(password))) return 1;

    /* Unlock the header to retrieve the master key */
    if (!unlock_header(container_path, password, &header, &container_fp)) {
        memset(password, 0, sizeof(password));
        return 1;
    }
    memset(password, 0, sizeof(password));

    /* Open the input file and measure its size */
    input_fp = fopen(input_path, "rb");
    if (!input_fp) {
        fprintf(stderr, "Error: Cannot open input file '%s'.\n", input_path);
        fclose(container_fp);
        return 1;
    }
    fseek(input_fp, 0, SEEK_END);
    file_size = (uint64_t)ftell(input_fp);
    fseek(input_fp, 0, SEEK_SET);

    /* Extract just the filename from the path */
    filename = strrchr(input_path, '/');
    if (!filename) filename = strrchr(input_path, '\\');
    filename = filename ? filename + 1 : input_path;
    name_len = (uint16_t)strlen(filename);

    /* Check if the file fits in the container's data area */
    uint64_t needed = sizeof(name_len) + name_len + sizeof(file_size) + file_size;
    if (needed > header.data_size) {
        fprintf(stderr, "Error: File too large for container.\n");
        fclose(input_fp);
        fclose(container_fp);
        return 1;
    }

    /* Build the payload: [name_len][filename][file_size][file_data] */
    uint64_t payload_size = needed;
    file_data = malloc((size_t)payload_size);
    if (!file_data) {
        fprintf(stderr, "Error: Out of memory.\n");
        fclose(input_fp);
        fclose(container_fp);
        return 1;
    }

    offset = 0;
    memcpy(file_data + offset, &name_len, sizeof(name_len));
    offset += sizeof(name_len);
    memcpy(file_data + offset, filename, name_len);
    offset += name_len;
    memcpy(file_data + offset, &file_size, sizeof(file_size));
    offset += sizeof(file_size);
    if (fread(file_data + offset, 1, (size_t)file_size, input_fp) != file_size) {
        fprintf(stderr, "Error: Failed to read input file.\n");
        free(file_data);
        fclose(input_fp);
        fclose(container_fp);
        return 1;
    }
    fclose(input_fp);

    /* Seek past the header to the data area, then encrypt sector-by-sector */
    fseek(container_fp, HEADER_SIZE, SEEK_SET);
    uint64_t sectors_needed = (payload_size + SECTOR_SIZE - 1) / SECTOR_SIZE;

    for (uint64_t i = 0; i < sectors_needed; i++) {
        memset(sector, 0, SECTOR_SIZE);
        uint64_t chunk_offset = i * SECTOR_SIZE;
        uint64_t chunk_size = payload_size - chunk_offset;
        if (chunk_size > SECTOR_SIZE) chunk_size = SECTOR_SIZE;
        memcpy(sector, file_data + chunk_offset, (size_t)chunk_size);

        /* Sector numbers start at 1 (sector 0 is reserved for the header) */
        if (!xts_encrypt(header.master_key, i + 1, sector, SECTOR_SIZE,
                         encrypted)) {
            fprintf(stderr, "Error: Encryption failed at sector %llu.\n",
                    (unsigned long long)i);
            free(file_data);
            fclose(container_fp);
            return 1;
        }
        fwrite(encrypted, 1, SECTOR_SIZE, container_fp);
    }

    free(file_data);
    fclose(container_fp);
    memset(&header, 0, sizeof(header));

    printf("File stored successfully.\n");
    return 0;
}

What does cmd_store() do?

This function prepends metadata (filename length, filename, file size) to the raw file content, then encrypts the combined payload sector-by-sector into the container's data area.

  • Each sector gets a unique AES-256-XTS tweak derived from its sequential sector number. Sector numbers start at 1 because sector 0 is reserved for the header.
  • The metadata header allows cmd_extract() to recover both the original filename and the exact byte count, even though sectors are padded to 512 bytes.
  • After the loop completes, the header struct (which contains the master key) is wiped from memory.
  • Now add the cmd_extract() function directly below cmd_store():
static int cmd_extract(const char *container_path, const char *output_path)
{
    char password[256];
    VaultHeader header;
    FILE *container_fp;
    FILE *output_fp;
    unsigned char sector[SECTOR_SIZE];
    unsigned char decrypted[SECTOR_SIZE];
    uint16_t name_len;
    uint64_t file_size;
    char stored_name[256];

    if (!read_password("Enter password: ", password, sizeof(password))) return 1;

    /* Unlock the header to retrieve the master key */
    if (!unlock_header(container_path, password, &header, &container_fp)) {
        memset(password, 0, sizeof(password));
        return 1;
    }
    memset(password, 0, sizeof(password));

    /* Read and decrypt the first data sector to get the metadata */
    fseek(container_fp, HEADER_SIZE, SEEK_SET);
    if (fread(sector, 1, SECTOR_SIZE, container_fp) != SECTOR_SIZE) {
        fprintf(stderr, "Error: Failed to read data.\n");
        fclose(container_fp);
        return 1;
    }
    if (!xts_decrypt(header.master_key, 1, sector, SECTOR_SIZE, decrypted)) {
        fprintf(stderr, "Error: Decryption failed.\n");
        fclose(container_fp);
        return 1;
    }

    /* Parse the metadata: name_len, filename, file_size */
    size_t pos = 0;
    memcpy(&name_len, decrypted + pos, sizeof(name_len));
    pos += sizeof(name_len);
    if (name_len >= sizeof(stored_name)) {
        fprintf(stderr, "Error: Corrupted data.\n");
        fclose(container_fp);
        return 1;
    }
    memcpy(stored_name, decrypted + pos, name_len);
    stored_name[name_len] = '\0';
    pos += name_len;
    memcpy(&file_size, decrypted + pos, sizeof(file_size));
    pos += sizeof(file_size);

    printf("Extracting: %s (%llu bytes)\n", stored_name,
           (unsigned long long)file_size);

    /* Open the output file for writing */
    output_fp = fopen(output_path, "wb");
    if (!output_fp) {
        fprintf(stderr, "Error: Cannot create output file '%s'.\n", output_path);
        fclose(container_fp);
        return 1;
    }

    /* Write remaining bytes from the first decrypted sector */
    uint64_t bytes_written = 0;
    size_t available = SECTOR_SIZE - pos;
    size_t to_write = (file_size < available) ? (size_t)file_size : available;
    fwrite(decrypted + pos, 1, to_write, output_fp);
    bytes_written += to_write;

    /* Continue reading and decrypting subsequent sectors */
    uint64_t sector_num = 2;
    while (bytes_written < file_size) {
        if (fread(sector, 1, SECTOR_SIZE, container_fp) != SECTOR_SIZE) {
            fprintf(stderr, "Error: Unexpected end of file.\n");
            break;
        }
        if (!xts_decrypt(header.master_key, sector_num, sector, SECTOR_SIZE,
                         decrypted)) {
            fprintf(stderr, "Error: Decryption failed.\n");
            break;
        }
        uint64_t remaining = file_size - bytes_written;
        to_write = (remaining < SECTOR_SIZE) ? (size_t)remaining : SECTOR_SIZE;
        fwrite(decrypted, 1, to_write, output_fp);
        bytes_written += to_write;
        sector_num++;
    }

    fclose(output_fp);
    fclose(container_fp);
    memset(&header, 0, sizeof(header));

    printf("File extracted successfully.\n");
    return 0;
}

What does cmd_extract() do?

This function reverses cmd_store(). It decrypts sector 1 first to read the metadata (filename and size), then continues decrypting subsequent sectors until all file bytes are recovered.

  • The first sector contains both metadata and file content, so the function calculates how many file bytes remain after the metadata and writes just those bytes.
  • For subsequent sectors, it uses the stored file_size to know exactly when to stop writing, avoiding padding bytes at the end of the last sector.
  • Save vault.c.
Run the end-to-end test

Now you need to update main() to dispatch the new store and extract commands, then prove the full cycle works.

  • In vault.c, find the existing main() function at the bottom of the file.
  • Replace the entire main() function with this updated version that handles all three commands:
int main(int argc, char *argv[])
{
    if (argc < 3) {
        printf("Usage: vault create|store|extract <file> [arg]\n");
        return 1;
    }
    const char *command = argv[1];
    if (strcmp(command, "create") == 0 && argc == 4) {
        uint64_t size_mb = (uint64_t)atoi(argv[3]);
        if (size_mb == 0) { fprintf(stderr, "Error: Size must be at least 1 MB.\n"); return 1; }
        return cmd_create(argv[2], size_mb);
    } else if (strcmp(command, "store") == 0 && argc == 4) {
        return cmd_store(argv[2], argv[3]);
    } else if (strcmp(command, "extract") == 0 && argc == 4) {
        return cmd_extract(argv[2], argv[3]);
    } else {
        printf("Usage: vault create|store|extract <file> [arg]\n");
        return 1;
    }
}

What changed in main()?

The original main() only handled create. The new version checks argc < 3 as a minimum argument count, then dispatches to cmd_create(), cmd_store(), or cmd_extract() based on the first argument.

  • Save vault.c.
  • Build the project by running this command:
gcc -Wall -O2 -std=c11 -o vault vault.c crypto.c -lcrypto
  • Create a test file with sensitive content by running:
echo "TOP SECRET DATA" > secret.txt
  • Store it in your existing test container by running:
./vault store test.vault secret.txt
  • Enter the same password you used when creating test.vault.
  • Delete the original file and extract it back out by running these commands:
del secret.txt
./vault extract test.vault recovered.txt
  • Enter your password again when prompted.
  • Verify the content matches by running:
cat recovered.txt

You should see TOP SECRET DATA printed. The file survived the store-delete-extract cycle.

  • Now test with a wrong password to confirm error handling by running:
./vault extract test.vault bad_attempt.txt

Enter a different password than the one you used during creation. You should see the error message: Error: Wrong password or corrupted container.

Seeing linker errors about undefined references?

Make sure -lcrypto appears at the end of your gcc command. The linker resolves symbols left-to-right, so the library flag must come after the source files that reference it.

Help me fix undefined reference errors when compiling vault.c

✔️ Awesome, I've got everything!

Great. Double check you have saved vault.c and that your end-to-end test passed.

ⓧ I'd like to double check the full code

Here is the complete vault.c as it should look at the end of this step:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "crypto.h"

#pragma pack(push, 1)
typedef struct {
    char magic[MAGIC_SIZE];
    uint16_t version;
    unsigned char master_key[KEY_SIZE];
    uint64_t data_size;
    unsigned char reserved[368];
} VaultHeader;
#pragma pack(pop)

static int read_password(const char *prompt, char *buf, size_t bufsize)
{
    printf("%s", prompt);
    if (!fgets(buf, (int)bufsize, stdin)) return 0;
    size_t len = strlen(buf);
    if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0';
    return 1;
}

static int cmd_create(const char *filepath, uint64_t size_mb)
{
    char password[256];
    char confirm[256];
    unsigned char salt[SALT_SIZE];
    unsigned char header_key[KEY_SIZE];
    unsigned char master_key[KEY_SIZE];
    VaultHeader header;
    unsigned char header_buf[HEADER_SIZE - SALT_SIZE];
    unsigned char encrypted_header[HEADER_SIZE - SALT_SIZE];
    FILE *fp;
    uint64_t data_size = size_mb * 1024 * 1024;

    if (!read_password("Enter password: ", password, sizeof(password))) return 1;
    if (!read_password("Confirm password: ", confirm, sizeof(confirm))) return 1;
    if (strcmp(password, confirm) != 0) {
        fprintf(stderr, "Error: Passwords do not match.\n");
        return 1;
    }

    printf("Creating %llu MB encrypted container...\n",
           (unsigned long long)size_mb);

    if (!generate_random(salt, SALT_SIZE)) {
        fprintf(stderr, "Error: Failed to generate random salt.\n");
        return 1;
    }
    if (!generate_random(master_key, KEY_SIZE)) {
        fprintf(stderr, "Error: Failed to generate master key.\n");
        return 1;
    }

    printf("Deriving key (this may take a moment)...\n");
    if (!derive_key(password, salt, header_key)) {
        fprintf(stderr, "Error: Key derivation failed.\n");
        return 1;
    }

    memset(&header, 0, sizeof(header));
    memcpy(header.magic, MAGIC, MAGIC_SIZE);
    header.version = 1;
    memcpy(header.master_key, master_key, KEY_SIZE);
    header.data_size = data_size;

    memset(header_buf, 0, sizeof(header_buf));
    memcpy(header_buf, &header, sizeof(header));
    if (!xts_encrypt(header_key, 0, header_buf, sizeof(header_buf),
                     encrypted_header)) {
        fprintf(stderr, "Error: Header encryption failed.\n");
        return 1;
    }

    fp = fopen(filepath, "wb");
    if (!fp) {
        fprintf(stderr, "Error: Cannot create file '%s'.\n", filepath);
        return 1;
    }

    fwrite(salt, 1, SALT_SIZE, fp);
    fwrite(encrypted_header, 1, sizeof(encrypted_header), fp);

    {
        unsigned char sector[SECTOR_SIZE];
        uint64_t sectors = data_size / SECTOR_SIZE;
        for (uint64_t i = 0; i < sectors; i++) {
            generate_random(sector, SECTOR_SIZE);
            fwrite(sector, 1, SECTOR_SIZE, fp);
            if ((i % 2048) == 0) {
                printf("\r  Progress: %llu%%",
                       (unsigned long long)(i * 100 / sectors));
                fflush(stdout);
            }
        }
    }
    printf("\r  Progress: 100%%\n");

    fclose(fp);

    memset(password, 0, sizeof(password));
    memset(confirm, 0, sizeof(confirm));
    memset(header_key, 0, KEY_SIZE);
    memset(master_key, 0, KEY_SIZE);
    memset(&header, 0, sizeof(header));

    printf("Container created successfully: %s\n", filepath);
    return 0;
}

static int unlock_header(const char *filepath, const char *password,
                         VaultHeader *header_out, FILE **fp_out)
{
    unsigned char salt[SALT_SIZE];
    unsigned char header_key[KEY_SIZE];
    unsigned char encrypted_header[HEADER_SIZE - SALT_SIZE];
    unsigned char decrypted[HEADER_SIZE - SALT_SIZE];
    FILE *fp;

    fp = fopen(filepath, "r+b");
    if (!fp) {
        fp = fopen(filepath, "rb");
        if (!fp) {
            fprintf(stderr, "Error: Cannot open file '%s'.\n", filepath);
            return 0;
        }
    }

    if (fread(salt, 1, SALT_SIZE, fp) != SALT_SIZE ||
        fread(encrypted_header, 1, sizeof(encrypted_header), fp)
            != sizeof(encrypted_header)) {
        fprintf(stderr, "Error: File too small or corrupted.\n");
        fclose(fp);
        return 0;
    }

    printf("Deriving key...\n");
    if (!derive_key(password, salt, header_key)) {
        fprintf(stderr, "Error: Key derivation failed.\n");
        fclose(fp);
        return 0;
    }

    if (!xts_decrypt(header_key, 0, encrypted_header,
                     sizeof(encrypted_header), decrypted)) {
        fprintf(stderr, "Error: Header decryption failed.\n");
        fclose(fp);
        return 0;
    }

    memcpy(header_out, decrypted, sizeof(VaultHeader));
    if (memcmp(header_out->magic, MAGIC, MAGIC_SIZE) != 0) {
        fprintf(stderr, "Error: Wrong password or corrupted container.\n");
        memset(header_out, 0, sizeof(VaultHeader));
        fclose(fp);
        return 0;
    }

    memset(header_key, 0, KEY_SIZE);
    memset(decrypted, 0, sizeof(decrypted));

    if (fp_out) {
        *fp_out = fp;
    } else {
        fclose(fp);
    }
    return 1;
}

static int cmd_store(const char *container_path, const char *input_path)
{
    char password[256];
    VaultHeader header;
    FILE *container_fp;
    FILE *input_fp;
    unsigned char *file_data = NULL;
    unsigned char sector[SECTOR_SIZE];
    unsigned char encrypted[SECTOR_SIZE];
    uint64_t file_size, offset;
    uint16_t name_len;
    const char *filename;

    if (!read_password("Enter password: ", password, sizeof(password))) return 1;

    if (!unlock_header(container_path, password, &header, &container_fp)) {
        memset(password, 0, sizeof(password));
        return 1;
    }
    memset(password, 0, sizeof(password));

    input_fp = fopen(input_path, "rb");
    if (!input_fp) {
        fprintf(stderr, "Error: Cannot open input file '%s'.\n", input_path);
        fclose(container_fp);
        return 1;
    }
    fseek(input_fp, 0, SEEK_END);
    file_size = (uint64_t)ftell(input_fp);
    fseek(input_fp, 0, SEEK_SET);

    filename = strrchr(input_path, '/');
    if (!filename) filename = strrchr(input_path, '\\');
    filename = filename ? filename + 1 : input_path;
    name_len = (uint16_t)strlen(filename);

    uint64_t needed = sizeof(name_len) + name_len + sizeof(file_size) + file_size;
    if (needed > header.data_size) {
        fprintf(stderr, "Error: File too large for container.\n");
        fclose(input_fp);
        fclose(container_fp);
        return 1;
    }

    uint64_t payload_size = needed;
    file_data = malloc((size_t)payload_size);
    if (!file_data) {
        fprintf(stderr, "Error: Out of memory.\n");
        fclose(input_fp);
        fclose(container_fp);
        return 1;
    }

    offset = 0;
    memcpy(file_data + offset, &name_len, sizeof(name_len));
    offset += sizeof(name_len);
    memcpy(file_data + offset, filename, name_len);
    offset += name_len;
    memcpy(file_data + offset, &file_size, sizeof(file_size));
    offset += sizeof(file_size);
    if (fread(file_data + offset, 1, (size_t)file_size, input_fp) != file_size) {
        fprintf(stderr, "Error: Failed to read input file.\n");
        free(file_data);
        fclose(input_fp);
        fclose(container_fp);
        return 1;
    }
    fclose(input_fp);

    fseek(container_fp, HEADER_SIZE, SEEK_SET);
    uint64_t sectors_needed = (payload_size + SECTOR_SIZE - 1) / SECTOR_SIZE;

    for (uint64_t i = 0; i < sectors_needed; i++) {
        memset(sector, 0, SECTOR_SIZE);
        uint64_t chunk_offset = i * SECTOR_SIZE;
        uint64_t chunk_size = payload_size - chunk_offset;
        if (chunk_size > SECTOR_SIZE) chunk_size = SECTOR_SIZE;
        memcpy(sector, file_data + chunk_offset, (size_t)chunk_size);

        if (!xts_encrypt(header.master_key, i + 1, sector, SECTOR_SIZE,
                         encrypted)) {
            fprintf(stderr, "Error: Encryption failed at sector %llu.\n",
                    (unsigned long long)i);
            free(file_data);
            fclose(container_fp);
            return 1;
        }
        fwrite(encrypted, 1, SECTOR_SIZE, container_fp);
    }

    free(file_data);
    fclose(container_fp);
    memset(&header, 0, sizeof(header));

    printf("File stored successfully.\n");
    return 0;
}

static int cmd_extract(const char *container_path, const char *output_path)
{
    char password[256];
    VaultHeader header;
    FILE *container_fp;
    FILE *output_fp;
    unsigned char sector[SECTOR_SIZE];
    unsigned char decrypted[SECTOR_SIZE];
    uint16_t name_len;
    uint64_t file_size;
    char stored_name[256];

    if (!read_password("Enter password: ", password, sizeof(password))) return 1;

    if (!unlock_header(container_path, password, &header, &container_fp)) {
        memset(password, 0, sizeof(password));
        return 1;
    }
    memset(password, 0, sizeof(password));

    fseek(container_fp, HEADER_SIZE, SEEK_SET);
    if (fread(sector, 1, SECTOR_SIZE, container_fp) != SECTOR_SIZE) {
        fprintf(stderr, "Error: Failed to read data.\n");
        fclose(container_fp);
        return 1;
    }
    if (!xts_decrypt(header.master_key, 1, sector, SECTOR_SIZE, decrypted)) {
        fprintf(stderr, "Error: Decryption failed.\n");
        fclose(container_fp);
        return 1;
    }

    size_t pos = 0;
    memcpy(&name_len, decrypted + pos, sizeof(name_len));
    pos += sizeof(name_len);
    if (name_len >= sizeof(stored_name)) {
        fprintf(stderr, "Error: Corrupted data.\n");
        fclose(container_fp);
        return 1;
    }
    memcpy(stored_name, decrypted + pos, name_len);
    stored_name[name_len] = '\0';
    pos += name_len;
    memcpy(&file_size, decrypted + pos, sizeof(file_size));
    pos += sizeof(file_size);

    printf("Extracting: %s (%llu bytes)\n", stored_name,
           (unsigned long long)file_size);

    output_fp = fopen(output_path, "wb");
    if (!output_fp) {
        fprintf(stderr, "Error: Cannot create output file '%s'.\n", output_path);
        fclose(container_fp);
        return 1;
    }

    uint64_t bytes_written = 0;
    size_t available = SECTOR_SIZE - pos;
    size_t to_write = (file_size < available) ? (size_t)file_size : available;
    fwrite(decrypted + pos, 1, to_write, output_fp);
    bytes_written += to_write;

    uint64_t sector_num = 2;
    while (bytes_written < file_size) {
        if (fread(sector, 1, SECTOR_SIZE, container_fp) != SECTOR_SIZE) {
            fprintf(stderr, "Error: Unexpected end of file.\n");
            break;
        }
        if (!xts_decrypt(header.master_key, sector_num, sector, SECTOR_SIZE,
                         decrypted)) {
            fprintf(stderr, "Error: Decryption failed.\n");
            break;
        }
        uint64_t remaining = file_size - bytes_written;
        to_write = (remaining < SECTOR_SIZE) ? (size_t)remaining : SECTOR_SIZE;
        fwrite(decrypted, 1, to_write, output_fp);
        bytes_written += to_write;
        sector_num++;
    }

    fclose(output_fp);
    fclose(container_fp);
    memset(&header, 0, sizeof(header));

    printf("File extracted successfully.\n");
    return 0;
}

int main(int argc, char *argv[])
{
    if (argc < 3) {
        printf("Usage: vault create|store|extract <file> [arg]\n");
        return 1;
    }
    const char *command = argv[1];
    if (strcmp(command, "create") == 0 && argc == 4) {
        uint64_t size_mb = (uint64_t)atoi(argv[3]);
        if (size_mb == 0) { fprintf(stderr, "Error: Size must be at least 1 MB.\n"); return 1; }
        return cmd_create(argv[2], size_mb);
    } else if (strcmp(command, "store") == 0 && argc == 4) {
        return cmd_store(argv[2], argv[3]);
    } else if (strcmp(command, "extract") == 0 && argc == 4) {
        return cmd_extract(argv[2], argv[3]);
    } else {
        printf("Usage: vault create|store|extract <file> [arg]\n");
        return 1;
    }
}

Your container can now protect real files. Next up, you will build the CLI dispatcher and an info command that displays container metadata.

Build the CLI and Add Container Info

Your vault tool can already create containers, store files, and extract them. But right now, the command-line interface is minimal and there's no way to inspect a container's metadata without extracting data from it.

In this step, you will add a proper usage display, an info command that reports encryption parameters, and a Makefile that automates the build process.

In this step, get ready to:
  • Implement the CLI dispatcher with a help display and the info command.
  • Write a Makefile for repeatable, clean builds.
  • Run a full integration test across all four commands.
Implement the CLI dispatcher and cmd_info()

A proper CLI tool should show helpful usage text when invoked incorrectly. It should also let users inspect containers without modifying them.

  • In vault.c, find the read_password() function near the top of the file.
  • Add the following print_usage() function directly above read_password():
static void print_usage(void)
{
    printf("vault - Encrypted file container tool\n\n");
    printf("Usage:\n");
    printf("  vault create <file> <size_mb>  - Create encrypted container\n");
    printf("  vault store <file> <input>     - Store a file in container\n");
    printf("  vault extract <file> <output>  - Extract file from container\n");
    printf("  vault info <file>              - Show container info\n");
}

What does this code do?

This function prints a summary of all available vault commands with their expected arguments. It runs whenever the user types an invalid command or provides no arguments at all.

  • Save vault.c.

Next, you need the info command itself. This unlocks the header to read metadata, then displays the encryption algorithm and key derivation parameters.

  • In vault.c, scroll down to the end of the cmd_extract() function.
  • Add the following cmd_info() function below cmd_extract():
static int cmd_info(const char *filepath)
{
    char password[256];
    VaultHeader header;

    if (!read_password("Enter password: ", password, sizeof(password))) return 1;

    if (!unlock_header(filepath, password, &header, NULL)) {
        memset(password, 0, sizeof(password));
        return 1;
    }
    memset(password, 0, sizeof(password));

    printf("Container: %s\n", filepath);
    printf("  Format: %.*s v%u\n", MAGIC_SIZE, header.magic, header.version);
    printf("  Data size: %llu bytes (%llu MB)\n",
           (unsigned long long)header.data_size,
           (unsigned long long)(header.data_size / (1024 * 1024)));
    printf("  Encryption: AES-256-XTS\n");
    printf("  Key derivation: PBKDF2-HMAC-SHA256 (%d iterations)\n",
           PBKDF2_ITERATIONS);

    memset(&header, 0, sizeof(header));
    return 0;
}

What does this code do?

The cmd_info() function unlocks the volume header (which requires the password), then prints container metadata: format version, data capacity, encryption algorithm, and KDF parameters.

Passing NULL as the last argument to unlock_header() tells it to close the file after reading. The info command only reads, never writes.

  • Save vault.c.

Finally, replace the existing main() function with a proper dispatcher that handles all four commands.

  • In vault.c, scroll to the bottom and find the existing main() function.
  • Find this line:
int main(int argc, char *argv[])
{
    if (argc < 3) {
        printf("Usage: vault create|store|extract <file> [arg]\n");
        return 1;
    }
  • Delete the entire main() function and replace it with this version:
int main(int argc, char *argv[])
{
    if (argc < 3) {
        print_usage();
        return 1;
    }

    const char *command = argv[1];

    if (strcmp(command, "create") == 0 && argc == 4) {
        uint64_t size_mb = (uint64_t)atoi(argv[3]);
        if (size_mb == 0) {
            fprintf(stderr, "Error: Size must be at least 1 MB.\n");
            return 1;
        }
        return cmd_create(argv[2], size_mb);
    } else if (strcmp(command, "store") == 0 && argc == 4) {
        return cmd_store(argv[2], argv[3]);
    } else if (strcmp(command, "extract") == 0 && argc == 4) {
        return cmd_extract(argv[2], argv[3]);
    } else if (strcmp(command, "info") == 0 && argc == 3) {
        return cmd_info(argv[2]);
    } else {
        print_usage();
        return 1;
    }
}

What changed in main()?

  • The inline usage string is replaced with print_usage() calls so the help text stays in one place.
  • A new else if branch dispatches the info command, which only needs 3 arguments (the program name, the command, and the container path).
  • Each branch validates argc so the user gets clear help text instead of a crash when arguments are missing.
  • Save vault.c.

Seeing a compile error about print_usage?

Make sure print_usage() is defined above main() in the file. C requires functions to be declared or defined before they are called.

Help me fix a compile error about print_usage being undeclared.

Add the Makefile for clean builds

Up to now you have been typing out the full gcc command every time you build. A Makefile automates this so you only type make.

  • Open Makefile in VS Code (the empty file you created earlier).
  • Paste the following content:
CC = gcc
CFLAGS = -Wall -Wextra -O2 -std=c11
LDFLAGS = -lcrypto

vault: vault.c crypto.c crypto.h
	$(CC) $(CFLAGS) -o vault vault.c crypto.c $(LDFLAGS)

clean:
	rm -f vault vault.exe

.PHONY: clean

What does this Makefile do?

  • The vault target compiles both vault.c and crypto.c together, linking against libcrypto.
  • -Wall -Wextra enables all common warnings so you catch mistakes early.
  • -O2 enables optimization for faster runtime performance.
  • -std=c11 uses the C11 standard, which supports uint64_t loop variables.
  • The clean target removes compiled binaries so you can start fresh.

Makefiles require real tabs, not spaces

The indented lines under each target ($(CC)... and rm -f...) must start with a real tab character, not spaces. If VS Code converts tabs to spaces, you will see an error when running make.

To check: place your cursor at the start of the indented line. If you can delete the whitespace in one keypress, it is a tab. If it takes multiple presses, VS Code inserted spaces. Switch to tab mode by clicking Spaces: 4 in the bottom status bar and selecting Indent Using Tabs.

  • Save Makefile.
  • Test the build by running this command in your terminal:
make
  • You should see the gcc command printed out, followed by no errors or warnings.
  • Verify the clean target works by running:
make clean
  • The vault and vault.exe files are removed. Rebuild with make again before proceeding.
make

Seeing "missing separator" error?

This means the recipe lines use spaces instead of tabs. Delete the whitespace before $(CC) and rm -f, then re-insert a single tab character at the start of each line.

Help me fix the missing separator error in my Makefile.

Full integration test

Time to exercise every command end-to-end and confirm the full workflow.

  • Create a 2 MB encrypted container by running:
./vault create secrets.vault 2
  • Enter a password when prompted, then confirm it. You should see progress reach 100% and a success message.
  • Inspect the container's metadata by running:
./vault info secrets.vault
  • Enter the same password. You should see output showing AES-256-XTS as the encryption algorithm and PBKDF2-HMAC-SHA256 (200000 iterations) as the key derivation method.
  • Create a test file to store by running:
echo "TOP SECRET DATA" > important_doc.txt
  • Store the file inside the container by running:
./vault store secrets.vault important_doc.txt
  • Extract it back out by running:
./vault extract secrets.vault output.txt
  • Verify the extracted content matches the original by running:
cat output.txt
  • You should see TOP SECRET DATA printed to the terminal.

What should I see?

All four commands (create, store, extract, info) work correctly with the same password. The info command displays the format version, data size in bytes and MB, and the exact encryption parameters your vault uses.

Info command shows wrong password error?

Make sure you are entering the exact same password you used during create. Passwords are case-sensitive. If you forgot it, delete the container and create a new one.

Help me troubleshoot why my vault info command says wrong password.

✔️ Awesome, I've got everything!

Great. Double check you have saved both vault.c and Makefile.

ⓧ I'd like to double check the full code

Here is the complete vault.c at this point:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "crypto.h"

#pragma pack(push, 1)
typedef struct {
    char magic[MAGIC_SIZE];
    uint16_t version;
    unsigned char master_key[KEY_SIZE];
    uint64_t data_size;
    unsigned char reserved[368];
} VaultHeader;
#pragma pack(pop)

static void print_usage(void)
{
    printf("vault - Encrypted file container tool\n\n");
    printf("Usage:\n");
    printf("  vault create <file> <size_mb>  - Create encrypted container\n");
    printf("  vault store <file> <input>     - Store a file in container\n");
    printf("  vault extract <file> <output>  - Extract file from container\n");
    printf("  vault info <file>              - Show container info\n");
}

static int read_password(const char *prompt, char *buf, size_t bufsize)
{
    printf("%s", prompt);
    if (!fgets(buf, (int)bufsize, stdin)) return 0;
    size_t len = strlen(buf);
    if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0';
    return 1;
}

static int cmd_create(const char *filepath, uint64_t size_mb)
{
    char password[256];
    char confirm[256];
    unsigned char salt[SALT_SIZE];
    unsigned char header_key[KEY_SIZE];
    unsigned char master_key[KEY_SIZE];
    VaultHeader header;
    unsigned char header_buf[HEADER_SIZE - SALT_SIZE];
    unsigned char encrypted_header[HEADER_SIZE - SALT_SIZE];
    FILE *fp;
    uint64_t data_size = size_mb * 1024 * 1024;

    if (!read_password("Enter password: ", password, sizeof(password))) return 1;
    if (!read_password("Confirm password: ", confirm, sizeof(confirm))) return 1;
    if (strcmp(password, confirm) != 0) {
        fprintf(stderr, "Error: Passwords do not match.\n");
        return 1;
    }

    printf("Creating %llu MB encrypted container...\n",
           (unsigned long long)size_mb);

    if (!generate_random(salt, SALT_SIZE)) {
        fprintf(stderr, "Error: Failed to generate random salt.\n");
        return 1;
    }
    if (!generate_random(master_key, KEY_SIZE)) {
        fprintf(stderr, "Error: Failed to generate master key.\n");
        return 1;
    }

    printf("Deriving key (this may take a moment)...\n");
    if (!derive_key(password, salt, header_key)) {
        fprintf(stderr, "Error: Key derivation failed.\n");
        return 1;
    }

    memset(&header, 0, sizeof(header));
    memcpy(header.magic, MAGIC, MAGIC_SIZE);
    header.version = 1;
    memcpy(header.master_key, master_key, KEY_SIZE);
    header.data_size = data_size;

    memset(header_buf, 0, sizeof(header_buf));
    memcpy(header_buf, &header, sizeof(header));
    if (!xts_encrypt(header_key, 0, header_buf, sizeof(header_buf),
                     encrypted_header)) {
        fprintf(stderr, "Error: Header encryption failed.\n");
        return 1;
    }

    fp = fopen(filepath, "wb");
    if (!fp) {
        fprintf(stderr, "Error: Cannot create file '%s'.\n", filepath);
        return 1;
    }

    fwrite(salt, 1, SALT_SIZE, fp);
    fwrite(encrypted_header, 1, sizeof(encrypted_header), fp);

    {
        unsigned char sector[SECTOR_SIZE];
        uint64_t sectors = data_size / SECTOR_SIZE;
        for (uint64_t i = 0; i < sectors; i++) {
            generate_random(sector, SECTOR_SIZE);
            fwrite(sector, 1, SECTOR_SIZE, fp);
            if ((i % 2048) == 0) {
                printf("\r  Progress: %llu%%",
                       (unsigned long long)(i * 100 / sectors));
                fflush(stdout);
            }
        }
    }
    printf("\r  Progress: 100%%\n");

    fclose(fp);

    memset(password, 0, sizeof(password));
    memset(confirm, 0, sizeof(confirm));
    memset(header_key, 0, KEY_SIZE);
    memset(master_key, 0, KEY_SIZE);
    memset(&header, 0, sizeof(header));

    printf("Container created successfully: %s\n", filepath);
    return 0;
}

static int unlock_header(const char *filepath, const char *password,
                         VaultHeader *header_out, FILE **fp_out)
{
    unsigned char salt[SALT_SIZE];
    unsigned char header_key[KEY_SIZE];
    unsigned char encrypted_header[HEADER_SIZE - SALT_SIZE];
    unsigned char decrypted[HEADER_SIZE - SALT_SIZE];
    FILE *fp;

    fp = fopen(filepath, "r+b");
    if (!fp) {
        fp = fopen(filepath, "rb");
        if (!fp) {
            fprintf(stderr, "Error: Cannot open file '%s'.\n", filepath);
            return 0;
        }
    }

    if (fread(salt, 1, SALT_SIZE, fp) != SALT_SIZE ||
        fread(encrypted_header, 1, sizeof(encrypted_header), fp)
            != sizeof(encrypted_header)) {
        fprintf(stderr, "Error: File too small or corrupted.\n");
        fclose(fp);
        return 0;
    }

    printf("Deriving key...\n");
    if (!derive_key(password, salt, header_key)) {
        fprintf(stderr, "Error: Key derivation failed.\n");
        fclose(fp);
        return 0;
    }

    if (!xts_decrypt(header_key, 0, encrypted_header,
                     sizeof(encrypted_header), decrypted)) {
        fprintf(stderr, "Error: Header decryption failed.\n");
        fclose(fp);
        return 0;
    }

    memcpy(header_out, decrypted, sizeof(VaultHeader));
    if (memcmp(header_out->magic, MAGIC, MAGIC_SIZE) != 0) {
        fprintf(stderr, "Error: Wrong password or corrupted container.\n");
        memset(header_out, 0, sizeof(VaultHeader));
        fclose(fp);
        return 0;
    }

    memset(header_key, 0, KEY_SIZE);
    memset(decrypted, 0, sizeof(decrypted));

    if (fp_out) {
        *fp_out = fp;
    } else {
        fclose(fp);
    }
    return 1;
}

static int cmd_store(const char *container_path, const char *input_path)
{
    char password[256];
    VaultHeader header;
    FILE *container_fp;
    FILE *input_fp;
    unsigned char *file_data = NULL;
    unsigned char sector[SECTOR_SIZE];
    unsigned char encrypted[SECTOR_SIZE];
    uint64_t file_size, offset;
    uint16_t name_len;
    const char *filename;

    if (!read_password("Enter password: ", password, sizeof(password))) return 1;

    if (!unlock_header(container_path, password, &header, &container_fp)) {
        memset(password, 0, sizeof(password));
        return 1;
    }
    memset(password, 0, sizeof(password));

    input_fp = fopen(input_path, "rb");
    if (!input_fp) {
        fprintf(stderr, "Error: Cannot open input file '%s'.\n", input_path);
        fclose(container_fp);
        return 1;
    }
    fseek(input_fp, 0, SEEK_END);
    file_size = (uint64_t)ftell(input_fp);
    fseek(input_fp, 0, SEEK_SET);

    filename = strrchr(input_path, '/');
    if (!filename) filename = strrchr(input_path, '\\');
    filename = filename ? filename + 1 : input_path;
    name_len = (uint16_t)strlen(filename);

    uint64_t needed = sizeof(name_len) + name_len + sizeof(file_size) + file_size;
    if (needed > header.data_size) {
        fprintf(stderr, "Error: File too large for container.\n");
        fclose(input_fp);
        fclose(container_fp);
        return 1;
    }

    uint64_t payload_size = needed;
    file_data = malloc((size_t)payload_size);
    if (!file_data) {
        fprintf(stderr, "Error: Out of memory.\n");
        fclose(input_fp);
        fclose(container_fp);
        return 1;
    }

    offset = 0;
    memcpy(file_data + offset, &name_len, sizeof(name_len));
    offset += sizeof(name_len);
    memcpy(file_data + offset, filename, name_len);
    offset += name_len;
    memcpy(file_data + offset, &file_size, sizeof(file_size));
    offset += sizeof(file_size);
    if (fread(file_data + offset, 1, (size_t)file_size, input_fp) != file_size) {
        fprintf(stderr, "Error: Failed to read input file.\n");
        free(file_data);
        fclose(input_fp);
        fclose(container_fp);
        return 1;
    }
    fclose(input_fp);

    fseek(container_fp, HEADER_SIZE, SEEK_SET);
    uint64_t sectors_needed = (payload_size + SECTOR_SIZE - 1) / SECTOR_SIZE;

    for (uint64_t i = 0; i < sectors_needed; i++) {
        memset(sector, 0, SECTOR_SIZE);
        uint64_t chunk_offset = i * SECTOR_SIZE;
        uint64_t chunk_size = payload_size - chunk_offset;
        if (chunk_size > SECTOR_SIZE) chunk_size = SECTOR_SIZE;
        memcpy(sector, file_data + chunk_offset, (size_t)chunk_size);

        if (!xts_encrypt(header.master_key, i + 1, sector, SECTOR_SIZE,
                         encrypted)) {
            fprintf(stderr, "Error: Encryption failed at sector %llu.\n",
                    (unsigned long long)i);
            free(file_data);
            fclose(container_fp);
            return 1;
        }
        fwrite(encrypted, 1, SECTOR_SIZE, container_fp);
    }

    free(file_data);
    fclose(container_fp);
    memset(&header, 0, sizeof(header));

    printf("File stored successfully.\n");
    return 0;
}

static int cmd_extract(const char *container_path, const char *output_path)
{
    char password[256];
    VaultHeader header;
    FILE *container_fp;
    FILE *output_fp;
    unsigned char sector[SECTOR_SIZE];
    unsigned char decrypted[SECTOR_SIZE];
    uint16_t name_len;
    uint64_t file_size;
    char stored_name[256];

    if (!read_password("Enter password: ", password, sizeof(password))) return 1;

    if (!unlock_header(container_path, password, &header, &container_fp)) {
        memset(password, 0, sizeof(password));
        return 1;
    }
    memset(password, 0, sizeof(password));

    fseek(container_fp, HEADER_SIZE, SEEK_SET);
    if (fread(sector, 1, SECTOR_SIZE, container_fp) != SECTOR_SIZE) {
        fprintf(stderr, "Error: Failed to read data.\n");
        fclose(container_fp);
        return 1;
    }
    if (!xts_decrypt(header.master_key, 1, sector, SECTOR_SIZE, decrypted)) {
        fprintf(stderr, "Error: Decryption failed.\n");
        fclose(container_fp);
        return 1;
    }

    size_t pos = 0;
    memcpy(&name_len, decrypted + pos, sizeof(name_len));
    pos += sizeof(name_len);
    if (name_len >= sizeof(stored_name)) {
        fprintf(stderr, "Error: Corrupted data.\n");
        fclose(container_fp);
        return 1;
    }
    memcpy(stored_name, decrypted + pos, name_len);
    stored_name[name_len] = '\0';
    pos += name_len;
    memcpy(&file_size, decrypted + pos, sizeof(file_size));
    pos += sizeof(file_size);

    printf("Extracting: %s (%llu bytes)\n", stored_name,
           (unsigned long long)file_size);

    output_fp = fopen(output_path, "wb");
    if (!output_fp) {
        fprintf(stderr, "Error: Cannot create output file '%s'.\n", output_path);
        fclose(container_fp);
        return 1;
    }

    uint64_t bytes_written = 0;
    size_t available = SECTOR_SIZE - pos;
    size_t to_write = (file_size < available) ? (size_t)file_size : available;
    fwrite(decrypted + pos, 1, to_write, output_fp);
    bytes_written += to_write;

    uint64_t sector_num = 2;
    while (bytes_written < file_size) {
        if (fread(sector, 1, SECTOR_SIZE, container_fp) != SECTOR_SIZE) {
            fprintf(stderr, "Error: Unexpected end of file.\n");
            break;
        }
        if (!xts_decrypt(header.master_key, sector_num, sector, SECTOR_SIZE,
                         decrypted)) {
            fprintf(stderr, "Error: Decryption failed.\n");
            break;
        }
        uint64_t remaining = file_size - bytes_written;
        to_write = (remaining < SECTOR_SIZE) ? (size_t)remaining : SECTOR_SIZE;
        fwrite(decrypted, 1, to_write, output_fp);
        bytes_written += to_write;
        sector_num++;
    }

    fclose(output_fp);
    fclose(container_fp);
    memset(&header, 0, sizeof(header));

    printf("File extracted successfully.\n");
    return 0;
}

static int cmd_info(const char *filepath)
{
    char password[256];
    VaultHeader header;

    if (!read_password("Enter password: ", password, sizeof(password))) return 1;

    if (!unlock_header(filepath, password, &header, NULL)) {
        memset(password, 0, sizeof(password));
        return 1;
    }
    memset(password, 0, sizeof(password));

    printf("Container: %s\n", filepath);
    printf("  Format: %.*s v%u\n", MAGIC_SIZE, header.magic, header.version);
    printf("  Data size: %llu bytes (%llu MB)\n",
           (unsigned long long)header.data_size,
           (unsigned long long)(header.data_size / (1024 * 1024)));
    printf("  Encryption: AES-256-XTS\n");
    printf("  Key derivation: PBKDF2-HMAC-SHA256 (%d iterations)\n",
           PBKDF2_ITERATIONS);

    memset(&header, 0, sizeof(header));
    return 0;
}

int main(int argc, char *argv[])
{
    if (argc < 3) {
        print_usage();
        return 1;
    }

    const char *command = argv[1];

    if (strcmp(command, "create") == 0 && argc == 4) {
        uint64_t size_mb = (uint64_t)atoi(argv[3]);
        if (size_mb == 0) {
            fprintf(stderr, "Error: Size must be at least 1 MB.\n");
            return 1;
        }
        return cmd_create(argv[2], size_mb);
    } else if (strcmp(command, "store") == 0 && argc == 4) {
        return cmd_store(argv[2], argv[3]);
    } else if (strcmp(command, "extract") == 0 && argc == 4) {
        return cmd_extract(argv[2], argv[3]);
    } else if (strcmp(command, "info") == 0 && argc == 3) {
        return cmd_info(argv[2]);
    } else {
        print_usage();
        return 1;
    }
}

And here is the complete Makefile:

CC = gcc
CFLAGS = -Wall -Wextra -O2 -std=c11
LDFLAGS = -lcrypto

vault: vault.c crypto.c crypto.h
	$(CC) $(CFLAGS) -o vault vault.c crypto.c $(LDFLAGS)

clean:
	rm -f vault vault.exe

.PHONY: clean

Your vault tool is now feature-complete. You have a clean build system, four working commands, and a polished CLI that helps users discover what the tool can do. Next up is the Secret Mission, where you will add password rotation without re-encrypting the entire data area.

Secret mission

Add a Change-Password Command

What happens when you need to change a container's password without re-encrypting all the data inside? This is where the two-key architecture you built really proves its worth. Add a passwd command that rotates the password while leaving the encrypted data completely untouched.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine, so there are no ongoing costs. Decide whether to keep your resources available, or delete them entirely.

Resources you used:

  • Container files (.vault) on your local filesystem.
  • The vault binary and project source files (crypto.h, crypto.c, vault.c, Makefile).
  • MSYS2 installation at C:\msys64 (includes GCC and OpenSSL).
  • The C:\msys64\ucrt64\bin entry added to your system PATH.

Keep everything running

No action needed. Choose this if you plan to continue building C projects or extending your vault tool.

  • Your vault binary and container files remain on disk at no cost.
  • MSYS2 stays installed and ready for future C development.
  • The PATH entry keeps gcc and openssl accessible from any terminal.

Pause - I'll come back to this later

Nothing is running in the background, so there is nothing to pause. Your files stay on disk until you decide to delete them.

  • Optionally remove the PATH entry for C:\msys64\ucrt64\bin to keep your terminal clean. You can re-add it later.

Delete - I don't want to use this again

Remove all project resources and the MSYS2 toolchain completely.

  • Delete any container files by running this in your project folder:
del *.vault
  • Remove the vault binary and object files by running:
make clean
  • Delete the project folder itself. Navigate to its parent directory and remove it.
  • Uninstall MSYS2 by running the built-in uninstaller:
C:\msys64\uninstall.exe pr --confirm-command
  • Remove the C:\msys64\ucrt64\bin entry from your system PATH. Open **Settings**, select **System**, then **About**, then **Advanced system settings**. Click **Environment Variables**, find Path under System variables, select it, click **Edit**, and remove the MSYS2 entry.

Nice Work!

Nice Work!

You just built a complete encrypted file container tool in C from scratch. Your vault program uses the same cryptographic architecture that powers real-world disk encryption systems like VeraCrypt.

You've learned how to:

  • Implement a two-key cryptographic architecture (password-derived header key + random master key) that separates authentication from data protection.
  • Use production-grade cryptography in C by linking against OpenSSL libcrypto and calling its EVP API for AES-256-XTS sector encryption and PBKDF2-HMAC-SHA256 key derivation.
  • Build a full CLI tool with create, store, extract, and info commands that produces containers indistinguishable from random data without the password.
  • Secret Mission: Added a change-password command that re-encrypts only the volume header while leaving the data area untouched, proving why the two-key architecture exists.

Ready to quiz yourself?