---
source_url: https://www.pubnub.com/docs/sdks/c/api-reference/encryption
title: Encryption API for C SDK
updated_at: 2026-09-24T12:25:41.000Z
sdk_name: PubNub C SDK
sdk_version: 1.0.0
---

# Encryption API for C SDK

PubNub C SDK, use the latest version: 1.0.0

## Documentation index

To discover more PubNub resources:

1. Fetch [PubNub's llms.txt](https://www.pubnub.com/llms-full.txt) for a list of available pages in Markdown format.
2. Identify relevant URLs from that index.
3. Fetch the target pages.

Do not assume a path exists, always check the index first.

The crypto module encrypts payloads transparently for [publish and subscribe](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md), message persistence, and file transfer, and it can also be called directly for manual encryption. Two distinct layers make up this feature, and keeping them apart is the key to reading this page correctly:

1. **The module API you call.** Declared in `#include <pubnub/features/crypto.h>`, gated by `PUBNUB_ENABLE_CRYPTO`. A `pubnub_crypto_module_t` wraps a default cryptor plus up to `PUBNUB_CFG_CRYPTO_MAX_FALLBACK_CRYPTORS` fallback cryptors (default `4`, `2` under the `embedded` profile). You create a module with one of the factory functions below and attach it to `pubnub_config_t.crypto_module`. The SDK then encrypts and decrypts message payloads and file content for you. You can also call the module's encrypt/decrypt functions directly for data you handle yourself.
2. **The provider vtable, which you implement only for a custom algorithm.** Declared in `#include <pubnub/providers/crypto.h>`. This is `pubnub_crypto_provider_t`, the function-pointer table that both built-in algorithms populate. You only touch this layer if you are writing your own cryptor.

The SDK ships two built-in algorithms: **AES-CBC with a random IV**, identified on the wire as `"ACRH"`, and a **legacy** cryptor kept for interoperating with data encrypted by older PubNub SDKs. Fallback cryptor selection, covered in [Migrate from legacy to AES-CBC](#migrate-from-legacy-to-aes-cbc), lets you switch your default cryptor from legacy to ACRH without losing the ability to read data you already encrypted.

:::warning danger
crypto_module = NULL
disables encryption silently
`pubnub_config_t.crypto_module` defaults to `NULL`, and there is no compiled-in fallback module. A `NULL` `crypto_module` means every publish, subscribe, history, and file payload travels in plaintext, with no error and no warning. If your application expects encryption, verify `crypto_module` is set. Nothing else will tell you it is missing.
:::

## Backend selection and build profiles

Whether encryption is available at all is a build-time choice, controlled by `PUBNUB_PROVIDER_CRYPTO` (`openssl`, `mbedtls`, `none`, or `custom`):

| Profile | `PUBNUB_ENABLE_CRYPTO` | `PUBNUB_PROVIDER_CRYPTO` |
| --- | --- | --- |
| `full` | `ON` | `openssl` |
| `minimal` | `OFF` | `none` |
| `embedded` | `OFF` | `none` |

Under `minimal` and `embedded`, `PUBNUB_PROVIDER_CRYPTO=none` compiles no crypto backend at all. With `PUBNUB_ENABLE_CRYPTO` also `OFF`, the entire body of `features/crypto.h` is preprocessed out. None of the functions on this page exist in the compiled library or the installed header for those profiles. If you want encryption on an embedded target, start from (or switch to) the `full` profile, or override both flags explicitly on top of your chosen profile: `-DPUBNUB_ENABLE_CRYPTO=ON -DPUBNUB_PROVIDER_CRYPTO=openssl` (or `mbedtls`). An explicit `-D` always wins over the profile default. See [Environment Setup](https://www.pubnub.com/docs/sdks/c/environment-setup.md#feature-flags) for the full flag matrix and [Providers](https://www.pubnub.com/docs/sdks/c/environment-setup.md#provider-backends) for backend selection mechanics.

## Create an AES-CBC (ACRH) crypto module

`pubnub_crypto_module_aes_cbc()` creates a module whose default cryptor encrypts with AES-256-CBC and a random IV, identified on the wire as `"ACRH"`. This is the recommended cryptor for all new encryption.

### Method(s)

```c
pubnub_crypto_module_t* pubnub_crypto_module_aes_cbc(const char* cipher_key,
                                                      int use_random_iv,
                                                      pubnub_allocator_provider_t* alloc);
```

| Parameter | Description |
| --- | --- |
| `cipher_key` *Type: `const char*` | NUL-terminated cipher key string. Borrowed only for the duration of the call. The module derives its own key material and does not retain the original pointer. |
| `use_random_iv` *Type: `int` | Controls the IV mode of the **legacy fallback cryptor** this factory wires in automatically, not of ACRH itself (ACRH always uses a random IV). `1` = expect a random IV prepended to legacy ciphertext when decrypting. `0` = use the static IV `"0123456789012345"`. |
| `alloc`Type: `pubnub_allocator_provider_t*` | Allocator for the module's internal state. `NULL` uses the compiled-in default allocator. |

**C-family contract**

* **Header** — `#include <pubnub/features/crypto.h>`
* **Types** — `pubnub_crypto_module_t`, `pubnub_allocator_provider_t`
* **Feature flag** — `PUBNUB_ENABLE_CRYPTO`
* **Ownership / lifetime** — returns a heap-allocated module the caller owns. The module also owns an internally created legacy cryptor (used as its decrypt fallback) and destroys both when you call `pubnub_crypto_module_destroy()`.
* **Blocking** — never blocks. This is a local, synchronous computation with no network involvement.

### Sample code

Adapted from `examples/crypto/cooperative.c`.

```c
#include <pubnub/client.h>
#include <pubnub/features/crypto.h>
#include <pubnub/features/publish.h>
#include <pubnub/future.h>
#include <pubnub/response.h>

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

int main(void)
{
    /* 1. Create a crypto module (AES-256-CBC, random IV). NULL alloc
     * uses the compiled-in default. */
    pubnub_crypto_module_t* crypto = pubnub_crypto_module_aes_cbc("my-cipher-key", 1, NULL);
    if (NULL == crypto) {
        return EXIT_FAILURE;
    }

    /* 2. Attach it to the config before creating the context. */
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.publish_key     = "demo";
    cfg.subscribe_key   = "demo";
    cfg.user_id         = "example-crypto-cooperative";
    cfg.crypto_module    = crypto;

    pubnub_context_t* ctx = pubnub_create(&cfg);
    if (NULL == ctx) {
        pubnub_crypto_module_destroy(crypto);
        return EXIT_FAILURE;
    }

    /* 3. Publish — the payload is encrypted transparently. No explicit
     * encrypt call is needed for publish/subscribe/history/files. */
    pubnub_publish_opts_t opts = PUBNUB_PUBLISH_OPTS_INIT;
    opts.channel = "crypto_demo";
    opts.message = "{\"text\":\"Hello encrypted world!\"}";

    pubnub_future_t fut = pubnub_publish(ctx, &opts);

    /* 4. Cooperative poll until ready. */
    while (!pubnub_future_is_ready(fut)) {
        pubnub_process(ctx);
    }

    if (PUBNUB_OK == pubnub_future_status(fut)) {
        pubnub_timetoken_t tt = pubnub_publish_result_timetoken(fut);
        printf("published (encrypted) at %.*s\n", (int)tt.len, tt.ptr);
    }
    else {
        pubnub_string_view_t err = pubnub_response_error_message(fut);
        printf("publish failed: %.*s\n", (int)err.len, err.ptr);
    }

    /* 5. Cleanup order: release the future, destroy the context, then
     * destroy the module — matching the borrowed crypto_module contract. */
    pubnub_future_release(fut);
    pubnub_destroy(ctx);
    pubnub_crypto_module_destroy(crypto);
    return EXIT_SUCCESS;
}
```

The SDK also ships `examples/crypto/async.c`, the identical scenario delivered through a `pubnub_async()` completion callback instead of cooperative polling. See [Environment Setup](https://www.pubnub.com/docs/sdks/c/environment-setup.md#calling-patterns) for the three ways to consume a `pubnub_future_t`.

### Returns

A `pubnub_crypto_module_t*` you own, or `NULL` on allocation or key-derivation failure.

## Create a legacy crypto module

`pubnub_crypto_module_legacy()` creates a module whose default cryptor uses the legacy algorithm, the same encryption older PubNub SDKs use.

:::warning Weaker key derivation: use only for compatibility
The legacy cryptor derives its AES key from only 32 ASCII hex characters of a SHA-256 digest of `cipher_key`, half the digest's entropy, encoded inefficiently as text, instead of the full 32-byte raw digest ACRH uses. Do not choose the legacy cryptor for new encryption. Use it only to decrypt data that an older client already encrypted with it. For anything new, create an AES-CBC module instead.
:::

### Method(s)

```c
pubnub_crypto_module_t* pubnub_crypto_module_legacy(const char* cipher_key,
                                                     int use_random_iv,
                                                     pubnub_allocator_provider_t* alloc);
```

| Parameter | Description |
| --- | --- |
| `cipher_key` *Type: `const char*` | NUL-terminated cipher key string. Borrowed only for the duration of the call. |
| `use_random_iv` *Type: `int` | `1` = generate a random 16-byte IV per encrypt and prepend it to the ciphertext; `0` = use the static IV `"0123456789012345"`. This governs the module's own (legacy) encryption, not just its fallback, unlike the `use_random_iv` parameter on `pubnub_crypto_module_aes_cbc()`. |
| `alloc`Type: `pubnub_allocator_provider_t*` | Allocator for the module's internal state. `NULL` uses the compiled-in default. |

**C-family contract**

* **Header** — `#include <pubnub/features/crypto.h>`
* **Types** — `pubnub_crypto_module_t`, `pubnub_allocator_provider_t`
* **Feature flag** — `PUBNUB_ENABLE_CRYPTO`
* **Ownership / lifetime** — returns a heap-allocated module. The module owns an internally created ACRH cryptor as its decrypt fallback and destroys both cryptors on `pubnub_crypto_module_destroy()`.
* **Blocking** — never blocks. Synchronous local computation.

### Sample code

```c
#include <pubnub/client.h>
#include <pubnub/features/crypto.h>

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

int main(void)
{
    pubnub_crypto_module_t* legacy = pubnub_crypto_module_legacy("my-cipher-key", 1, NULL);
    if (NULL == legacy) {
        return EXIT_FAILURE;
    }

    const uint8_t plaintext[] = "message encrypted by an older SDK version";
    const size_t  plaintext_len = sizeof(plaintext) - 1;

    size_t enc_size = pubnub_crypto_module_encrypt_size(legacy, plaintext_len);
    uint8_t* enc_buf = (uint8_t*)malloc(enc_size);
    if (NULL == enc_buf) {
        pubnub_crypto_module_destroy(legacy);
        return EXIT_FAILURE;
    }

    size_t       enc_len = 0;
    pubnub_res_t res = pubnub_crypto_module_encrypt_buf(
        legacy, plaintext, plaintext_len, enc_buf, enc_size, &enc_len);
    if (PUBNUB_OK != res) {
        free(enc_buf);
        pubnub_crypto_module_destroy(legacy);
        return EXIT_FAILURE;
    }

    uint8_t dec_buf[256];
    size_t  dec_len = 0;
    res = pubnub_crypto_module_decrypt_buf(
        legacy, enc_buf, enc_len, dec_buf, sizeof(dec_buf), &dec_len);
    if (PUBNUB_OK == res) {
        printf("decrypted %zu bytes\n", dec_len);
    }

    free(enc_buf);
    pubnub_crypto_module_destroy(legacy);
    return EXIT_SUCCESS;
}
```

### Returns

A `pubnub_crypto_module_t*` you own, or `NULL` on allocation or key-derivation failure.

## Create a crypto module with custom cryptors

`pubnub_crypto_module_create()` builds a module from cryptors you supply explicitly, instead of the automatic ACRH/legacy pairing the two named factories create. Use it to control exactly which algorithms participate, or to register a [custom cryptor](#write-a-custom-cryptor-the-provider-vtable).

### Method(s)

```c
pubnub_crypto_module_t* pubnub_crypto_module_create(pubnub_crypto_provider_t* default_cryptor,
                                                     pubnub_crypto_provider_t** others,
                                                     size_t others_count,
                                                     pubnub_allocator_provider_t* alloc);
```

| Parameter | Description |
| --- | --- |
| `default_cryptor` *Type: `pubnub_crypto_provider_t*` | The cryptor that handles **all encryption**. Borrowed. The module does not take ownership. |
| `others`Type: `pubnub_crypto_provider_t**` | Array of fallback cryptors, consulted only on decrypt. Borrowed. May be `NULL` when `others_count` is `0`. |
| `others_count`Type: `size_t` | Number of entries in `others`. Must not exceed `PUBNUB_CFG_CRYPTO_MAX_FALLBACK_CRYPTORS` (default `4`; `2` under `embedded`); exceeding it makes the factory return `NULL`. |
| `alloc`Type: `pubnub_allocator_provider_t*` | Allocator for the module struct itself. `NULL` uses the compiled-in default. |

Standalone factories build the cryptors to pass in:

```c
pubnub_crypto_provider_t* pubnub_cryptor_aes_cbc_create(const char* cipher_key,
                                                         pubnub_allocator_provider_t* alloc);
pubnub_crypto_provider_t* pubnub_cryptor_legacy_create(const char* cipher_key,
                                                        int use_random_iv,
                                                        pubnub_allocator_provider_t* alloc);
void pubnub_cryptor_destroy(pubnub_crypto_provider_t* cryptor);
```

`pubnub_cryptor_destroy()` securely zeros key material before freeing, and is `NULL`-safe.

**C-family contract**

* **Header** — `#include <pubnub/features/crypto.h>`
* **Types** — `pubnub_crypto_module_t`, `pubnub_crypto_provider_t`, `pubnub_allocator_provider_t`
* **Feature flag** — `PUBNUB_ENABLE_CRYPTO`
* **Ownership / lifetime** — unlike `pubnub_crypto_module_aes_cbc()`/`pubnub_crypto_module_legacy()`, a module built with `pubnub_crypto_module_create()` does **not** own any cryptor you pass in. Destroying the module frees only the module struct. You must destroy each cryptor yourself with `pubnub_cryptor_destroy()`, and you must do so **after** destroying the module, not before.
* **Blocking** — never blocks.

### Sample code

```c
#include <pubnub/client.h>
#include <pubnub/features/crypto.h>
#include <pubnub/providers/crypto.h>

#include <stdlib.h>

int main(void)
{
    /* Build the same ACRH-default/legacy-fallback pairing that
     * pubnub_crypto_module_aes_cbc() creates automatically, but with
     * explicit control over both cryptors. */
    pubnub_crypto_provider_t* acrh = pubnub_cryptor_aes_cbc_create("my-cipher-key", NULL);
    if (NULL == acrh) {
        return EXIT_FAILURE;
    }

    pubnub_crypto_provider_t* legacy = pubnub_cryptor_legacy_create("my-cipher-key", 1, NULL);
    if (NULL == legacy) {
        pubnub_cryptor_destroy(acrh);
        return EXIT_FAILURE;
    }

    pubnub_crypto_provider_t* fallbacks[] = { legacy };
    pubnub_crypto_module_t*   module =
        pubnub_crypto_module_create(acrh, fallbacks, 1, NULL);
    if (NULL == module) {
        pubnub_cryptor_destroy(legacy);
        pubnub_cryptor_destroy(acrh);
        return EXIT_FAILURE;
    }

    /* ... use module for encrypt/decrypt or attach it to
     * pubnub_config_t.crypto_module ... */

    /* Destroy the module first — it does not own acrh or legacy —
     * then destroy each cryptor. */
    pubnub_crypto_module_destroy(module);
    pubnub_cryptor_destroy(legacy);
    pubnub_cryptor_destroy(acrh);
    return EXIT_SUCCESS;
}
```

### Returns

A `pubnub_crypto_module_t*` you own, or `NULL` if allocation fails or `others_count` exceeds the compile-time fallback limit.

## Destroy a crypto module

```c
void pubnub_crypto_module_destroy(pubnub_crypto_module_t* module);
```

`NULL`-safe. What it destroys depends on how the module was created:

| Created with | What `pubnub_crypto_module_destroy()` does |
| --- | --- |
| `pubnub_crypto_module_aes_cbc()` or `pubnub_crypto_module_legacy()` | Destroys the module **and** both of its internally created cryptors, securely zeroing key material. |
| `pubnub_crypto_module_create()` | Frees only the module struct. Cryptors you passed in are untouched. Destroy each one yourself with `pubnub_cryptor_destroy()`, after this call returns. |

**Ownership / lifetime**: a module attached to `pubnub_config_t.crypto_module` is borrowed by the context. Destroy the context with `pubnub_destroy()`/`pubnub_deinit()` before destroying the module, never the other way round. **Blocking**: never blocks.

## Attach a crypto module to a context

`pubnub_config_t.crypto_module` is what turns a module into transparent, automatic encryption for feature calls. This field is documented in full in [Configuration](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md#providers); this section covers only its encryption-specific behavior.

| Field | Type | Required | Default | Ownership |
| --- | --- | --- | --- | --- |
| `crypto_module` | `pubnub_crypto_module_t*` | Optional | `NULL`: payload encryption disabled, deliberately, with no compiled-in fallback | Borrowed. The caller manages the module's lifetime and must keep it alive for as long as any context references it. |

When `crypto_module` is non-`NULL`, these operations encrypt and decrypt automatically, with no explicit encrypt/decrypt call in your application code:

| Feature | What gets encrypted |
| --- | --- |
| Publish | The JSON message payload, base64-encoded into the outgoing publish body. |
| Subscribe | The JSON message payload of each incoming message, decoded from the base64 string PubNub delivers. |
| History (`pubnub_fetch_messages()`) | Same base64 JSON-message path as subscribe. |
| Files | Two distinct paths: the raw file **content** is encrypted and decrypted as **binary**, with no base64 or JSON wrapping, around the upload/download transfer; the JSON file-message that describes the file (published after upload) uses the same base64 JSON path as publish. |

Signal payloads are not on this list. A `pubnub_signal()` call does not encrypt its payload through the crypto module.

:::warning Decryption failure is silent, not an error
On subscribe, history, and file downloads, a decryption failure does not fail the operation or surface a distinct `pubnub_res_t`. Subscribe and history return the still-encrypted payload unchanged, exactly as received. File downloads set the result's `decrypted` flag to `0` rather than failing. If your application does not check for this, it can end up treating ciphertext as if it were plaintext. Check the payload or the `decrypted` flag explicitly rather than assuming a successful transaction means successfully decrypted content.
:::

:::note Access Manager and crypto are independent
`PUBNUB_ENABLE_PAM` requires `PUBNUB_ENABLE_CRYPTO` at build time, because PAM request signing depends on the crypto provider's `hmac_sha256` vtable member. This dependency is on the provider layer, not on `crypto_module`. PAM signing works whether or not you configure a `crypto_module` for payload encryption, and configuring one does not by itself enable PAM. On the `minimal` and `embedded` profiles, both `PUBNUB_ENABLE_CRYPTO` and `PUBNUB_ENABLE_PAM` are `OFF`, so Access Manager needs the same explicit override described in [Backend selection and build profiles](#backend-selection-and-build-profiles).
:::

## Encrypt and decrypt into a fixed-size buffer

Use these functions to encrypt or decrypt data yourself, into a buffer you already own. No allocation happens inside the SDK.

### Method(s)

```c
size_t pubnub_crypto_module_encrypt_size(pubnub_crypto_module_t* module, size_t input_len);
size_t pubnub_crypto_module_encrypted_base64_size(pubnub_crypto_module_t* module, size_t input_len);

pubnub_res_t pubnub_crypto_module_encrypt_buf(pubnub_crypto_module_t* module,
                                               const uint8_t* input, size_t input_len,
                                               uint8_t* output, size_t output_cap,
                                               size_t* output_len);

pubnub_res_t pubnub_crypto_module_decrypt_buf(pubnub_crypto_module_t* module,
                                               const uint8_t* input, size_t input_len,
                                               uint8_t* output, size_t output_cap,
                                               size_t* output_len);
```

| Parameter | Description |
| --- | --- |
| `module`Type: `pubnub_crypto_module_t*` | Required, borrowed. |
| `input`Type: `const uint8_t*` | Required, borrowed. Plaintext for encrypt; PNED-headed or raw legacy ciphertext for decrypt. |
| `input_len`Type: `size_t` | Length of `input` in bytes. |
| `output`Type: `uint8_t*` | Required. Caller-owned, pre-allocated output buffer. |
| `output_cap`Type: `size_t` | Capacity of `output` in bytes. |
| `output_len`Type: `size_t*` | Required. Receives the actual number of bytes written on success. |

`pubnub_crypto_module_encrypt_size()` returns the worst-case output size (PNED header + metadata + ciphertext) for a plaintext of `input_len` bytes, or `0` on error (for example, a `NULL` module). `pubnub_crypto_module_encrypted_base64_size()` returns the equivalent worst-case size for the base64-encoded form, including the NUL terminator. Call one of these before `_encrypt_buf`/`_encrypt_to_base64` to size your buffer.

**C-family contract**

* **Header** — `#include <pubnub/features/crypto.h>`
* **Types** — `pubnub_crypto_module_t`
* **Feature flag** — `PUBNUB_ENABLE_CRYPTO`
* **Ownership / lifetime** — no allocation. `output` is entirely caller-owned, before and after the call.
* **Buffers** — see the warning below. This is the single most consequential detail on this page.
* **Blocking** — never blocks. Synchronous local computation.

:::warning Both encrypt and decrypt enforce their buffer size
`pubnub_crypto_module_encrypt_buf()` checks `output_cap` against `pubnub_crypto_module_encrypt_size()` and returns `PUBNUB_ERR_BUFFER_TOO_SMALL` if your buffer is too small. `pubnub_crypto_module_decrypt_buf()` requires `output_cap >= input_len` (plaintext is never longer than the ciphertext that carried it) and returns `PUBNUB_ERR_BUFFER_TOO_SMALL` if that condition is not met. Always size your decrypt output buffer to at least `input_len` bytes; the function checks this for you before writing anything.
:::

### Sample code

```c
#include <pubnub/client.h>
#include <pubnub/features/crypto.h>

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

int main(void)
{
    pubnub_crypto_module_t* crypto = pubnub_crypto_module_aes_cbc("my-cipher-key", 1, NULL);
    if (NULL == crypto) {
        return EXIT_FAILURE;
    }

    const uint8_t plaintext[] = "encrypt me";
    const size_t  plaintext_len = sizeof(plaintext) - 1;

    size_t enc_cap = pubnub_crypto_module_encrypt_size(crypto, plaintext_len);
    if (0 == enc_cap) {
        pubnub_crypto_module_destroy(crypto);
        return EXIT_FAILURE;
    }

    uint8_t* enc_buf = (uint8_t*)malloc(enc_cap);
    if (NULL == enc_buf) {
        pubnub_crypto_module_destroy(crypto);
        return EXIT_FAILURE;
    }

    size_t       enc_len = 0;
    pubnub_res_t res = pubnub_crypto_module_encrypt_buf(
        crypto, plaintext, plaintext_len, enc_buf, enc_cap, &enc_len);
    if (PUBNUB_OK != res) {
        printf("encrypt failed: %d\n", (int)res);
        free(enc_buf);
        pubnub_crypto_module_destroy(crypto);
        return EXIT_FAILURE;
    }

    /* The decrypted plaintext is never longer than the ciphertext that
     * carried it, so sizing the decrypt buffer to enc_len bytes is
     * always sufficient here. decrypt_buf also checks this for you and
     * returns PUBNUB_ERR_BUFFER_TOO_SMALL if the buffer is undersized. */
    uint8_t dec_buf_storage[256];
    uint8_t* dec_buf = (enc_len <= sizeof(dec_buf_storage)) ? dec_buf_storage : NULL;
    if (NULL == dec_buf) {
        free(enc_buf);
        pubnub_crypto_module_destroy(crypto);
        return EXIT_FAILURE;
    }

    size_t dec_len = 0;
    res = pubnub_crypto_module_decrypt_buf(
        crypto, enc_buf, enc_len, dec_buf, sizeof(dec_buf_storage), &dec_len);
    if (PUBNUB_OK == res) {
        printf("decrypted %zu bytes: %.*s\n", dec_len, (int)dec_len, dec_buf);
    }

    free(enc_buf);
    pubnub_crypto_module_destroy(crypto);
    return EXIT_SUCCESS;
}
```

### Returns

`PUBNUB_OK` on success. `pubnub_crypto_module_encrypt_buf()` returns `PUBNUB_ERR_BUFFER_TOO_SMALL` if `output_cap` is insufficient, or `PUBNUB_ERR_CRYPTO` on an encrypt failure. `pubnub_crypto_module_decrypt_buf()` returns `PUBNUB_ERR_BUFFER_TOO_SMALL` if `output_cap < input_len`, or `PUBNUB_ERR_CRYPTO` on failure (no matching cryptor for the input's identifier, a decryption error, or a truncated header). See [Error handling](#error-handling).

## Encrypt and decrypt into an SDK-allocated buffer

These variants allocate the output buffer for you, sized exactly to the result.

### Method(s)

```c
pubnub_res_t pubnub_crypto_module_encrypt(pubnub_crypto_module_t* module,
                                           const uint8_t* input, size_t input_len,
                                           uint8_t** output, size_t* output_len);

pubnub_res_t pubnub_crypto_module_decrypt(pubnub_crypto_module_t* module,
                                           const uint8_t* input, size_t input_len,
                                           uint8_t** output, size_t* output_len);

void pubnub_crypto_module_free(pubnub_crypto_module_t* module, void* ptr);
```

| Parameter | Description |
| --- | --- |
| `module`Type: `pubnub_crypto_module_t*` | Required, borrowed. |
| `input`Type: `const uint8_t*` | Required, borrowed. |
| `input_len`Type: `size_t` | Length of `input` in bytes. |
| `output`Type: `uint8_t**` | Required. Receives an allocator-owned buffer pointer. |
| `output_len`Type: `size_t*` | Required. Receives the output length in bytes. |

**C-family contract**

* **Header** — `#include <pubnub/features/crypto.h>`
* **Ownership / lifetime** — the SDK allocates `*output` using the **module's own stored allocator** (the one passed to the factory that created it, or the compiled-in default if `NULL` was passed). Free it with `pubnub_crypto_module_free(module, ptr)`, **not** `free()` directly, and pass the **same module that produced the pointer**, not any other module or allocator. `pubnub_crypto_module_free()` is `NULL`-safe for `ptr`.
* **Buffers** — no capacity to size yourself. The SDK sizes the allocation exactly to the encrypted or decrypted result.
* **Blocking** — never blocks.

### Sample code

```c
#include <pubnub/client.h>
#include <pubnub/features/crypto.h>

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

int main(void)
{
    pubnub_crypto_module_t* crypto = pubnub_crypto_module_aes_cbc("my-cipher-key", 1, NULL);
    if (NULL == crypto) {
        return EXIT_FAILURE;
    }

    const uint8_t plaintext[] = "encrypt me, allocated";
    const size_t  plaintext_len = sizeof(plaintext) - 1;

    uint8_t*     enc_buf = NULL;
    size_t       enc_len = 0;
    pubnub_res_t res = pubnub_crypto_module_encrypt(
        crypto, plaintext, plaintext_len, &enc_buf, &enc_len);
    if (PUBNUB_OK != res) {
        pubnub_crypto_module_destroy(crypto);
        return EXIT_FAILURE;
    }

    uint8_t* dec_buf = NULL;
    size_t   dec_len = 0;
    res = pubnub_crypto_module_decrypt(crypto, enc_buf, enc_len, &dec_buf, &dec_len);
    if (PUBNUB_OK == res) {
        printf("decrypted %zu bytes: %.*s\n", dec_len, (int)dec_len, dec_buf);
    }

    /* Free each allocated buffer through the module that produced it. */
    pubnub_crypto_module_free(crypto, dec_buf);
    pubnub_crypto_module_free(crypto, enc_buf);
    pubnub_crypto_module_destroy(crypto);
    return EXIT_SUCCESS;
}
```

### Returns

`PUBNUB_OK` on success with `*output`/`*output_len` populated, `PUBNUB_ERR_OUT_OF_MEMORY` or `PUBNUB_ERR_CRYPTO` on encrypt failure, `PUBNUB_ERR_CRYPTO` on decrypt failure.

## Encrypt and decrypt as base64 text

A convenience pair that combines encryption with base64 encoding in one call, the same mechanism [publish, subscribe, and history](#attach-a-crypto-module-to-a-context) use internally to move encrypted payloads through JSON.

### Method(s)

```c
pubnub_res_t pubnub_crypto_module_encrypt_to_base64(pubnub_crypto_module_t* module,
                                                     const uint8_t* input, size_t input_len,
                                                     char** out_base64, size_t* out_len);

pubnub_res_t pubnub_crypto_module_decrypt_from_base64(pubnub_crypto_module_t* module,
                                                       const char* base64, size_t base64_len,
                                                       uint8_t** output, size_t* output_len);
```

| Parameter | Description |
| --- | --- |
| `module`Type: `pubnub_crypto_module_t*` | Required, borrowed. |
| `input` / `base64`Type: `const uint8_t*` / `const char*` | Required, borrowed. Plaintext to encrypt, or base64 ciphertext to decrypt. |
| `input_len` / `base64_len`Type: `size_t` | Length of the corresponding input in bytes. |
| `out_base64` / `output`Type: `char**` / `uint8_t**` | Required. Receives an allocator-owned buffer. |
| `out_len` / `output_len`Type: `size_t*` | Required. Receives the output length in bytes. |

**C-family contract**

* **Header** — `#include <pubnub/features/crypto.h>`
* **Ownership / lifetime** — same rule as the allocating binary variants: free the output with `pubnub_crypto_module_free(module, ptr)`, using the module that produced it.
* **Blocking** — never blocks.

### Sample code

```c
#include <pubnub/client.h>
#include <pubnub/features/crypto.h>

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

int main(void)
{
    pubnub_crypto_module_t* crypto = pubnub_crypto_module_aes_cbc("my-cipher-key", 1, NULL);
    if (NULL == crypto) {
        return EXIT_FAILURE;
    }

    const uint8_t plaintext[] = "{\"text\":\"hello\"}";
    const size_t  plaintext_len = sizeof(plaintext) - 1;

    char*        b64 = NULL;
    size_t       b64_len = 0;
    pubnub_res_t res = pubnub_crypto_module_encrypt_to_base64(
        crypto, plaintext, plaintext_len, &b64, &b64_len);
    if (PUBNUB_OK != res) {
        pubnub_crypto_module_destroy(crypto);
        return EXIT_FAILURE;
    }

    uint8_t* dec_buf = NULL;
    size_t   dec_len = 0;
    res = pubnub_crypto_module_decrypt_from_base64(
        crypto, b64, strlen(b64), &dec_buf, &dec_len);
    if (PUBNUB_OK == res) {
        printf("decrypted %zu bytes: %.*s\n", dec_len, (int)dec_len, dec_buf);
    }

    pubnub_crypto_module_free(crypto, dec_buf);
    pubnub_crypto_module_free(crypto, b64);
    pubnub_crypto_module_destroy(crypto);
    return EXIT_SUCCESS;
}
```

### Returns

`PUBNUB_OK` on success with the output pointer and length populated; `PUBNUB_ERR_CRYPTO` on encrypt or decrypt failure.

## Migrate from legacy to AES-CBC

This is how a legacy-to-AES-CBC migration stays transparent: **encrypt always uses only the module's default cryptor, but decrypt can use the default cryptor or any of its fallbacks.**

Decrypt resolves which cryptor to use like this:

1. If the input carries a PNED header (the short binary marker ACRH writes ahead of its ciphertext), the module reads the header's 4-byte identifier and looks for a cryptor with a matching `identifier`, the default cryptor first, then each fallback in order. If nothing matches, decryption fails with `PUBNUB_ERR_CRYPTO`.
2. If the input has **no** PNED header, it is treated as legacy-format ciphertext, and the module looks for a cryptor whose identifier is the all-zero legacy identifier (default first, then fallbacks). If none match, decryption fails with `PUBNUB_ERR_CRYPTO`.
3. Encryption never consults this fallback list. It is decrypt-only: every encrypt call uses whatever cryptor `pubnub_crypto_module_default_cryptor()` reports for that module.

Both named factories exploit this automatically: `pubnub_crypto_module_aes_cbc()` wires in an internally created legacy cryptor as its fallback, and `pubnub_crypto_module_legacy()` wires in an internally created ACRH cryptor as its fallback. The practical result is that **switching your application's default cryptor from legacy to ACRH does not require re-encrypting anything already stored**: create an ACRH module going forward, and it can still decrypt every message your old, legacy-default deployment encrypted, because the legacy identifier is already in its fallback list.

```c
#include <pubnub/client.h>
#include <pubnub/features/crypto.h>

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

int main(void)
{
    /* Data encrypted earlier, when the application's default cryptor
     * was legacy. */
    pubnub_crypto_module_t* old_deployment = pubnub_crypto_module_legacy("my-cipher-key", 1, NULL);
    if (NULL == old_deployment) {
        return EXIT_FAILURE;
    }

    const uint8_t old_message[] = "message stored under the legacy default";
    const size_t  old_message_len = sizeof(old_message) - 1;

    uint8_t*     stored_ciphertext = NULL;
    size_t       stored_len = 0;
    pubnub_res_t res = pubnub_crypto_module_encrypt(
        old_deployment, old_message, old_message_len, &stored_ciphertext, &stored_len);
    if (PUBNUB_OK != res) {
        pubnub_crypto_module_destroy(old_deployment);
        return EXIT_FAILURE;
    }
    pubnub_crypto_module_free(old_deployment, stored_ciphertext);
    /* In a real migration, `stored_ciphertext` is data already sitting
     * in Message Persistence or a file — not re-derived like this. This
     * sample only shows both cryptors touching the same bytes. */
    stored_ciphertext = NULL;
    res = pubnub_crypto_module_encrypt(
        old_deployment, old_message, old_message_len, &stored_ciphertext, &stored_len);
    if (PUBNUB_OK != res) {
        pubnub_crypto_module_destroy(old_deployment);
        return EXIT_FAILURE;
    }

    /* After migrating, the application's default cryptor is now ACRH.
     * The same cipher_key still decrypts the old legacy-encrypted data,
     * because pubnub_crypto_module_aes_cbc() wires legacy in as a
     * fallback automatically. */
    pubnub_crypto_module_t* new_deployment = pubnub_crypto_module_aes_cbc("my-cipher-key", 1, NULL);
    if (NULL == new_deployment) {
        pubnub_crypto_module_free(old_deployment, stored_ciphertext);
        pubnub_crypto_module_destroy(old_deployment);
        return EXIT_FAILURE;
    }

    uint8_t* recovered = NULL;
    size_t   recovered_len = 0;
    res = pubnub_crypto_module_decrypt(
        new_deployment, stored_ciphertext, stored_len, &recovered, &recovered_len);
    if (PUBNUB_OK == res) {
        printf("recovered %zu bytes with the new ACRH default: %.*s\n",
               recovered_len, (int)recovered_len, recovered);
        pubnub_crypto_module_free(new_deployment, recovered);
    }
    else {
        printf("decrypt failed: %d\n", (int)res);
    }

    pubnub_crypto_module_free(old_deployment, stored_ciphertext);
    pubnub_crypto_module_destroy(new_deployment);
    pubnub_crypto_module_destroy(old_deployment);
    return EXIT_SUCCESS;
}
```

## Write a custom cryptor (the provider vtable)

Implement `pubnub_crypto_provider_t` to plug in an algorithm the SDK does not ship, for example a hardware security module, or a third algorithm you need for interoperability. Pass your provider to [pubnub_crypto_module_create()](#create-a-crypto-module-with-custom-cryptors) as `default_cryptor` or as an entry in `others`.

The sample below is a structural template for wiring up the vtable. Implement `encrypt`, `decrypt`, and `hmac_sha256` with a real cryptographic library before using anything like it in production.

### Method(s)

```c
typedef struct pubnub_encrypted_data {
    uint8_t* data;
    size_t   data_len;
    uint8_t* metadata;
    size_t   metadata_len;
} pubnub_encrypted_data_t;

typedef struct pubnub_crypto_provider {
    uint8_t identifier[4];

    size_t (*encrypt_size)(struct pubnub_crypto_provider* self, size_t plaintext_len);

    pubnub_res_t (*encrypt)(struct pubnub_crypto_provider* self,
                             const uint8_t* input, size_t input_len,
                             pubnub_encrypted_data_t* output);

    pubnub_res_t (*decrypt)(struct pubnub_crypto_provider* self,
                             const pubnub_encrypted_data_t* input,
                             uint8_t* output, size_t* output_len);

    pubnub_res_t (*hmac_sha256)(struct pubnub_crypto_provider* self,
                                 const uint8_t* key, size_t key_len,
                                 const uint8_t* data, size_t data_len,
                                 uint8_t* output, size_t* output_len);

    int (*init)(struct pubnub_crypto_provider* self, const pubnub_provider_deps_t* deps);
    void (*deinit)(struct pubnub_crypto_provider* self);
} pubnub_crypto_provider_t;
```

| Field | Description |
| --- | --- |
| `identifier[4]` | 4-byte algorithm identifier the module matches on decrypt (see [Migrate from legacy to AES-CBC](#migrate-from-legacy-to-aes-cbc)). Use all zeros only if you specifically mean "legacy/default algorithm." |
| `encrypt_size` | Returns the exact output size your `encrypt` needs for a given plaintext length, so the caller (the module, or a caller of the buffer variants) can size `output->data` correctly before calling `encrypt`. `pubnub_encrypted_data_t` carries no capacity field of its own, so `encrypt_size()` is the only contract governing how large a buffer the caller prepares. |
| `encrypt` | Writes ciphertext into `output->data` and sets `output->data_len`; writes algorithm-specific metadata (for example, an IV) into `output->metadata` and sets `output->metadata_len`, or leaves `metadata` untouched (`NULL`/`0`) if your algorithm needs none. |
| `decrypt` | `output_len` is in/out: on entry it carries the caller's buffer capacity, on exit the actual plaintext length. Unlike the SDK's own OpenSSL-backed ACRH and legacy cryptors, a custom `decrypt` **does** receive its buffer's capacity through `output_len` and should check it before writing, since nothing else in the call path performs that check. |
| `hmac_sha256` | Required only if this provider will sign Access Manager requests; output buffer is at least 32 bytes. |
| `init` / `deinit` | Optional (`NULL` = no-op). Called once per context, during context initialization and teardown respectively, from normal (non-ISR) context only. |

**C-family contract**

* **Header** — `#include <pubnub/providers/crypto.h>`
* **Types** — `pubnub_crypto_provider_t`, `pubnub_encrypted_data_t`
* **Thread / callback context** — every callback runs from normal (non-ISR) context only.

### Sample code

```c
#include <pubnub/error.h>
#include <pubnub/providers/crypto.h>

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

/* Toy XOR "cryptor" — illustrates vtable wiring only. This is NOT secure
 * and must never be used to protect real data; a real implementation
 * needs a vetted cryptographic library for encrypt/decrypt/hmac_sha256. */

static size_t demo_encrypt_size(struct pubnub_crypto_provider* self, size_t plaintext_len)
{
    (void)self;
    return plaintext_len;
}

static pubnub_res_t demo_encrypt(struct pubnub_crypto_provider* self,
                                  const uint8_t* input, size_t input_len,
                                  pubnub_encrypted_data_t* output)
{
    (void)self;
    for (size_t i = 0; i < input_len; ++i) {
        output->data[i] = input[i] ^ 0x5A;
    }
    output->data_len     = input_len;
    output->metadata_len = 0;
    return PUBNUB_OK;
}

static pubnub_res_t demo_decrypt(struct pubnub_crypto_provider* self,
                                  const pubnub_encrypted_data_t* input,
                                  uint8_t* output, size_t* output_len)
{
    (void)self;
    /* Unlike the SDK's built-in OpenSSL backends, this callback checks
     * the caller's buffer capacity before writing. */
    if (*output_len < input->data_len) {
        return PUBNUB_ERR_BUFFER_TOO_SMALL;
    }
    for (size_t i = 0; i < input->data_len; ++i) {
        output[i] = input->data[i] ^ 0x5A;
    }
    *output_len = input->data_len;
    return PUBNUB_OK;
}

static pubnub_res_t demo_hmac_sha256(struct pubnub_crypto_provider* self,
                                      const uint8_t* key, size_t key_len,
                                      const uint8_t* data, size_t data_len,
                                      uint8_t* output, size_t* output_len)
{
    (void)self;
    (void)key;
    (void)key_len;
    (void)data;
    (void)data_len;
    (void)output;
    (void)output_len;
    /* Not implemented in this demo — a provider used for Access Manager
     * signing must compute a real HMAC-SHA256 here. */
    return PUBNUB_ERR_NOT_SUPPORTED;
}

static pubnub_crypto_provider_t demo_provider = {
    .identifier   = { 'D', 'E', 'M', 'O' },
    .encrypt_size = demo_encrypt_size,
    .encrypt      = demo_encrypt,
    .decrypt      = demo_decrypt,
    .hmac_sha256  = demo_hmac_sha256,
    .init         = NULL,
    .deinit       = NULL,
};
```

`demo_provider` is a plain static struct with no dynamic state, so it has nothing for `pubnub_cryptor_destroy()` to free or zero. That function exists for cryptors obtained from `pubnub_cryptor_aes_cbc_create()`/`pubnub_cryptor_legacy_create()`, which do own allocated state. Pass `&demo_provider` to [pubnub_crypto_module_create()](#create-a-crypto-module-with-custom-cryptors) as `default_cryptor` or as an entry in `others`. You own its lifetime for as long as any module references it, and there is no destroy call to make on the provider itself.

### Returns

Each callback returns `pubnub_res_t`; `PUBNUB_OK` on success. The module surfaces `PUBNUB_ERR_CRYPTO` to its own caller when a callback fails, except for a `PUBNUB_ERR_BUFFER_TOO_SMALL` your `decrypt` returns explicitly, which propagates unchanged.

## Error handling

Crypto operations return the same `pubnub_res_t` type every other feature uses; see [The pubnub_res_t result catalog](https://www.pubnub.com/docs/sdks/c/status-events.md#the-pubnub_res_t-result-catalog) for the full list. The two values specific to this page:

* **PUBNUB_ERR_CRYPTO**: no cryptor in the module matched the input's identifier, a decrypt or encrypt operation failed inside the selected cryptor, or a PNED header was truncated or malformed.
* **PUBNUB_ERR_BUFFER_TOO_SMALL**: returned by the encrypt path when your output buffer is smaller than `pubnub_crypto_module_encrypt_size()` reports, and by the decrypt path when `output_cap < input_len`; see the [buffer-sizing warning](#encrypt-and-decrypt-into-a-fixed-size-buffer) above.

## Terms in this document

* **Cryptor** - An implementation of a specific cryptographic algorithm used for data encryption/decryption that adheres to a standard interface.

Last updated at: 2026-09-24T12:25:41.000Z
