Skip to content

Tutorial Building a Secure CLI Messenger

Eldric Arlo Bennett edited this page Nov 14, 2025 · 1 revision

Tutorial: Building a Secure CLI Messenger

This tutorial will guide you through building a simple yet secure command-line messaging tool using the Oracipher Core library. Instead of merely listing API functions, we will tell a story that connects them into a meaningful, end-to-end workflow. By the end, you will understand how to manage user identities, encrypt messages for a specific recipient, and securely decrypt them.

The Goal: Alice will securely send the message "The eagle flies at midnight." to Bob.

Prerequisites:

  • You have successfully compiled the libhsc_kernel shared library.
  • You have used the test_ca_util and hsc_cli tools to create a Certificate Authority (ca.pem, ca.key) and have issued signed certificates for two users: "Alice" (alice.pem, with Common Name alice@example.com) and "Bob" (bob.pem, with Common Name bob@example.com).
  • You have the private keys for Alice (alice.key) and Bob (bob.key).

Step 1: The Setup - Application and User Initialization

Before any communication can happen, our application must initialize the library, and our users, Alice and Bob, must have their cryptographic identities loaded.

In a real application, key generation (hsc_generate_master_key_pair) and CSR generation (hsc_generate_csr) would be a one-time "registration" step. For this tutorial, we will simulate this by loading their pre-existing private keys from files.

Our C program will start with a main function that handles the global library lifecycle.

messenger.c - Part 1: The Main Function

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

#include "hsc_kernel.h" // The only header you need from the library

// Helper function prototypes
char* read_file_contents(const char* filename);
hsc_master_key_pair* setup_user(const char* private_key_path, const char* username);

// --- Main application entry point ---
int main(void) {
    printf("--- Secure Messenger Tutorial ---\n\n");

    // 1. Initialize the Oracipher Core library (MUST be the first call)
    if (hsc_init() != HSC_OK) {
        fprintf(stderr, "FATAL: Failed to initialize Oracipher Core library.\n");
        return 1;
    }

    // --- User Setup ---
    printf("[SETUP] Loading identities for Alice and Bob...\n");
    hsc_master_key_pair* alice_kp = setup_user("alice.key", "Alice");
    hsc_master_key_pair* bob_kp = setup_user("bob.key", "Bob");

    if (!alice_kp || !bob_kp) {
        fprintf(stderr, "Error: Could not set up user identities. Exiting.\n");
        // Safely free any partially created key pairs before exiting
        hsc_free_master_key_pair(&alice_kp);
        hsc_free_master_key_pair(&bob_kp);
        hsc_cleanup();
        return 1;
    }
    printf("Identities loaded successfully.\n\n");

    // --- Main Logic (will be added in the next steps) ---


    // --- Cleanup ---
    printf("\n[CLEANUP] Securely freeing all resources...\n");
    hsc_free_master_key_pair(&alice_kp);
    hsc_free_master_key_pair(&bob_kp);

    // Clean up the library's global resources (MUST be the last call)
    hsc_cleanup();
    printf("Cleanup complete.\n");

    return 0;
}


// --- Helper Function Implementations ---

hsc_master_key_pair* setup_user(const char* private_key_path, const char* username) {
    // In a real application for a new user, you would call:
    // 1. hsc_generate_master_key_pair()
    // 2. hsc_generate_csr() and submit the result to a CA
    // For this tutorial, we load an existing key, which also places it in secure memory.
    hsc_master_key_pair* kp = hsc_load_master_key_pair_from_private_key(private_key_path);
    if (kp == NULL) {
        fprintf(stderr, "Failed to load private key for %s from: %s\n", username, private_key_path);
        return NULL;
    }
    return kp;
}

char* read_file_contents(const char* filename) {
    FILE* f = fopen(filename, "rb");
    if (!f) return NULL;
    fseek(f, 0, SEEK_END);
    long len = ftell(f);
    fseek(f, 0, SEEK_SET);
    if (len <= 0) {
        fclose(f);
        return NULL;
    }
    char* buf = malloc(len + 1);
    if (!buf) {
        fclose(f);
        return NULL;
    }
    fread(buf, 1, len, f);
    buf[len] = '\0';
    fclose(f);
    return buf;
}

Step 2: Alice Sends an Encrypted Message to Bob

Now, let's implement the logic for Alice. This is the core of the hybrid encryption workflow.

  1. Generate a one-time session key. This key will be used for fast, symmetric encryption.
  2. Encrypt the message using the session key.
  3. Verify Bob's identity using his public certificate and a trusted CA certificate. This is a critical step to prevent man-in-the-middle attacks.
  4. Extract Bob's public key from his now-verified certificate.
  5. Encapsulate (asymmetrically encrypt) the session key for Bob. Only Bob, with his private key, can open this.
  6. Package the results for transmission.

We'll define a simple struct to represent the data packet Alice sends.

messenger.c - Part 2: Sending Logic

// Add this struct definition at the top of the file
typedef struct {
    unsigned char* encrypted_message;
    size_t encrypted_message_len;

    unsigned char* encapsulated_key;
    size_t encapsulated_key_len;
} SecureMessagePacket;


// Add this function prototype at the top
bool alice_sends_message(SecureMessagePacket* packet,
                         const char* plaintext,
                         const hsc_master_key_pair* alice_kp,
                         const char* recipient_cert_path,
                         const char* recipient_username,
                         const char* ca_cert_path);


// Add this function implementation after main()
bool alice_sends_message(SecureMessagePacket* packet,
                         const char* plaintext,
                         const hsc_master_key_pair* alice_kp,
                         const char* recipient_cert_path,
                         const char* recipient_username,
                         const char* ca_cert_path)
{
    printf("--- Alice is sending a message ---\n");
    printf("Plaintext: \"%s\"\n", plaintext);

    // 1. Generate a one-time session key for symmetric encryption
    unsigned char session_key[HSC_SESSION_KEY_BYTES];
    hsc_random_bytes(session_key, sizeof(session_key));

    // 2. Encrypt the message with the session key using AEAD
    size_t plaintext_len = strlen(plaintext);
    size_t ciphertext_buf_size = plaintext_len + HSC_AEAD_OVERHEAD_BYTES;
    packet->encrypted_message = malloc(ciphertext_buf_size);
    unsigned long long actual_ciphertext_len;

    if (hsc_aead_encrypt(packet->encrypted_message, &actual_ciphertext_len,
                         (const unsigned char*)plaintext, plaintext_len, session_key) != HSC_OK) {
        fprintf(stderr, "Alice Error: Failed to encrypt message.\n");
        return false;
    }
    packet->encrypted_message_len = actual_ciphertext_len;
    printf("Step 1 & 2: Message encrypted with a new session key.\n");

    // 3. Verify Recipient's (Bob's) Identity [CRITICAL SECURITY STEP]
    char* recipient_cert_pem = read_file_contents(recipient_cert_path);
    char* ca_cert_pem = read_file_contents(ca_cert_path);
    if (!recipient_cert_pem || !ca_cert_pem) {
        fprintf(stderr, "Alice Error: Could not read certificate files.\n");
        free(recipient_cert_pem); free(ca_cert_pem);
        return false;
    }

    if (hsc_verify_user_certificate(recipient_cert_pem, ca_cert_pem, recipient_username) != HSC_OK) {
        fprintf(stderr, "Alice Error: Recipient certificate verification FAILED. ABORTING.\n");
        free(recipient_cert_pem); free(ca_cert_pem);
        return false;
    }
    printf("Step 3: Recipient '%s' certificate verified successfully.\n", recipient_username);

    // 4. Extract Bob's Public Key from his verified certificate
    unsigned char recipient_pk[HSC_MASTER_PUBLIC_KEY_BYTES];
    if (hsc_extract_public_key_from_cert(recipient_cert_pem, recipient_pk) != HSC_OK) {
        fprintf(stderr, "Alice Error: Failed to extract public key from certificate.\n");
        free(recipient_cert_pem); free(ca_cert_pem);
        return false;
    }
    printf("Step 4: Extracted recipient's public key from certificate.\n");

    // 5. Encapsulate the Session Key for Bob using asymmetric cryptography
    size_t enc_key_buf_size = sizeof(session_key) + HSC_ENCAPSULATED_KEY_OVERHEAD_BYTES;
    packet->encapsulated_key = malloc(enc_key_buf_size);
    size_t actual_enc_key_len;

    if (hsc_encapsulate_session_key(packet->encapsulated_key, &actual_enc_key_len,
                                    session_key, sizeof(session_key),
                                    recipient_pk, alice_kp) != HSC_OK) {
        fprintf(stderr, "Alice Error: Failed to encapsulate session key.\n");
        free(recipient_cert_pem); free(ca_cert_pem);
        return false;
    }
    packet->encapsulated_key_len = actual_enc_key_len;
    printf("Step 5: Session key encapsulated for recipient.\n");

    // 6. Securely wipe the session key from the stack now that it's encapsulated
    sodium_memzero(session_key, sizeof(session_key));
    free(recipient_cert_pem);
    free(ca_cert_pem);
    printf("Packet is ready to be sent.\n");
    return true;
}

Step 3: Bob Receives and Decrypts the Message

Finally, we implement Bob's logic. When he receives the SecureMessagePacket, he performs the reverse process to securely access the original message.

  1. Extract Alice's (the sender's) public key from her certificate.
  2. Decapsulate the session key using his own private key. This is the core asymmetric decryption step.
  3. Use the recovered session key to decrypt the message.
  4. Securely destroy the session key after use.

messenger.c - Part 3: Receiving Logic

// Add this function prototype at the top
char* bob_receives_message(const SecureMessagePacket* packet,
                           const hsc_master_key_pair* bob_kp,
                           const char* sender_cert_path);


// Add this function implementation after the others
char* bob_receives_message(const SecureMessagePacket* packet,
                           const hsc_master_key_pair* bob_kp,
                           const char* sender_cert_path)
{
    printf("\n--- Bob is receiving a message ---\n");

    // 1. Get Sender's (Alice's) Public Key
    // As a best practice, Bob should also verify Alice's certificate here.
    char* sender_cert_pem = read_file_contents(sender_cert_path);
    if (!sender_cert_pem) {
        fprintf(stderr, "Bob Error: Could not read sender's certificate file.\n");
        return NULL;
    }
    unsigned char sender_pk[HSC_MASTER_PUBLIC_KEY_BYTES];
    if (hsc_extract_public_key_from_cert(sender_cert_pem, sender_pk) != HSC_OK) {
        fprintf(stderr, "Bob Error: Could not extract sender's public key.\n");
        free(sender_cert_pem);
        return NULL;
    }
    printf("Step 1: Extracted sender's public key.\n");
    free(sender_cert_pem);

    // 2. Decapsulate the Session Key using his own private key
    // Use hsc_secure_alloc to ensure the recovered key is in protected memory.
    unsigned char* recovered_session_key = hsc_secure_alloc(HSC_SESSION_KEY_BYTES);
    if (!recovered_session_key) {
        fprintf(stderr, "Bob Error: Secure memory allocation failed.\n");
        return NULL;
    }

    if (hsc_decapsulate_session_key(recovered_session_key,
                                    packet->encapsulated_key, packet->encapsulated_key_len,
                                    sender_pk, bob_kp) != HSC_OK) {
        fprintf(stderr, "Bob Error: FAILED to decapsulate session key. Message may be corrupt or not intended for Bob.\n");
        hsc_secure_free(recovered_session_key);
        return NULL;
    }
    printf("Step 2: Session key successfully decapsulated and recovered.\n");

    // 3. Use the recovered key to decrypt the message
    size_t decrypted_buf_size = packet->encrypted_message_len;
    char* decrypted_plaintext = malloc(decrypted_buf_size);
    unsigned long long actual_decrypted_len;

    if (hsc_aead_decrypt((unsigned char*)decrypted_plaintext, &actual_decrypted_len,
                         packet->encrypted_message, packet->encrypted_message_len,
                         recovered_session_key) != HSC_OK) {
        fprintf(stderr, "Bob Error: FAILED to decrypt message. The ciphertext may have been tampered with.\n");
        hsc_secure_free(recovered_session_key);
        free(decrypted_plaintext);
        return NULL;
    }
    decrypted_plaintext[actual_decrypted_len] = '\0'; // Null-terminate for printing
    printf("Step 3: Message decrypted successfully.\n");

    // 4. Secure Cleanup: The session key is no longer needed.
    hsc_secure_free(recovered_session_key);
    printf("Step 4: Recovered session key has been securely destroyed from memory.\n");

    return decrypted_plaintext;
}

Step 4: Putting It All Together

Now, update the main function to call the sender and receiver logic, completing our end-to-end story.

messenger.c - Final main function

// Replace the original main function with this complete version.
int main(void) {
    printf("--- Secure Messenger Tutorial ---\n\n");

    if (hsc_init() != HSC_OK) {
        fprintf(stderr, "FATAL: Failed to initialize Oracipher Core library.\n");
        return 1;
    }

    printf("[SETUP] Loading identities for Alice and Bob...\n");
    hsc_master_key_pair* alice_kp = setup_user("alice.key", "Alice");
    hsc_master_key_pair* bob_kp = setup_user("bob.key", "Bob");

    if (!alice_kp || !bob_kp) {
        fprintf(stderr, "Error: Could not set up user identities.\n");
        hsc_free_master_key_pair(&alice_kp);
        hsc_free_master_key_pair(&bob_kp);
        hsc_cleanup();
        return 1;
    }
    printf("Identities loaded successfully.\n\n");

    // --- Alice sends the message to Bob ---
    SecureMessagePacket packet = {0};
    const char* message_to_send = "The eagle flies at midnight.";
    
    bool sent_ok = alice_sends_message(&packet, message_to_send, alice_kp,
                                       "bob.pem", "bob@example.com", "ca.pem");

    if (sent_ok) {
        // --- Bob receives the message from Alice ---
        char* received_message = bob_receives_message(&packet, bob_kp, "alice.pem");
        if (received_message) {
            printf("\n[SUCCESS] Bob successfully decrypted the message:\n");
            printf("  -> \"%s\"\n", received_message);

            // Final validation to confirm the content is identical
            if (strcmp(message_to_send, received_message) == 0) {
                printf("Message content is correct!\n");
            } else {
                fprintf(stderr, "ERROR: Decrypted message does not match original!\n");
            }
            free(received_message);
        }

        // Free memory allocated for the packet
        free(packet.encrypted_message);
        free(packet.encapsulated_key);
    }

    printf("\n[CLEANUP] Securely freeing all resources...\n");
    hsc_free_master_key_pair(&alice_kp);
    hsc_free_master_key_pair(&bob_kp);
    hsc_cleanup();
    printf("Cleanup complete.\n");

    return sent_ok ? 0 : 1;
}

Compilation and Execution

Save the complete code as messenger.c. You can compile it with the following command (assuming libhsc_kernel.so and the required header/key/cert files are in the current directory):

gcc messenger.c -o messenger -I./include -L. -lhsc_kernel -lsodium -lssl -lcrypto -lcurl -Wl,-rpath,'$ORIGIN'

Run the program:

./messenger

You have now successfully built a secure messaging application that demonstrates the complete, end-to-end workflow of the Oracipher Core library, from identity management to secure decryption.