Packages

Signal Protocol cryptographic primitives NIF implementation

Current section

Files

Jump to
Raw

c_src/dr.c

#include <erl_nif.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <limits.h>
#include <time.h>
#include <stdbool.h>
#include <sodium.h>
#include <openssl/evp.h>
#include <openssl/crypto.h>
#include <openssl/params.h>
#include "dr.h"
#include "dr_chain.h"
#include "dr_crypto.h"
#include "dr_proto.h"
#include "pksm.h"
// Signal Protocol wire version. Encoded as (v << 4) | v in the leading byte.
#define DR_SIGNAL_VERSION 3
#define DR_VERSION_BYTE ((DR_SIGNAL_VERSION << 4) | DR_SIGNAL_VERSION) // 0x33
// Double Ratchet init (per Signal DR spec section 3.3).
// Args: SharedSecret(64), LocalIdentityPub(32), RemoteIdentityPub(32),
// SelfIdentityPriv(32 or 64), IsAlice(int).
// LocalIdentityPub and RemoteIdentityPub are Ed25519 pubs; they are converted
// to X25519 and stored in state for use as the Signal-spec MAC binding (every
// message MAC is HMAC(macKey, local_id || remote_id || version || proto)).
// Alice: SelfIdentityPriv may be empty (she uses a fresh ephemeral for DH).
// She does an initial send ratchet against the remote pub.
// Bob: SelfIdentityPriv must be his Ed25519 64B secret. He stores it as
// his initial DH ratchet pair; send_chain_key is derived on first receive.
ERL_NIF_TERM dr_init(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
if (argc != 5) {
return enif_make_badarg(env);
}
ErlNifBinary shared_secret, local_identity_pub, remote_identity_pub, self_priv;
int is_alice;
if (!enif_inspect_binary(env, argv[0], &shared_secret) ||
!enif_inspect_binary(env, argv[1], &local_identity_pub) ||
!enif_inspect_binary(env, argv[2], &remote_identity_pub) ||
!enif_inspect_binary(env, argv[3], &self_priv) ||
!enif_get_int(env, argv[4], &is_alice)) {
return enif_make_badarg(env);
}
if (shared_secret.size != 96) {
return enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, "invalid_shared_secret_size"));
}
if (local_identity_pub.size != crypto_sign_PUBLICKEYBYTES ||
remote_identity_pub.size != crypto_sign_PUBLICKEYBYTES) {
return enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, "invalid_identity_pub_size"));
}
const char *err = NULL;
double_ratchet_state_t state;
unsigned char dh_out[crypto_scalarmult_BYTES];
unsigned char kdf_out[96];
memset(&state, 0, sizeof(state));
memset(dh_out, 0, sizeof(dh_out));
memset(kdf_out, 0, sizeof(kdf_out));
memcpy(state.root_key, shared_secret.data, 32);
// DR-HE bootstrap: shared_secret[64..96) seeds the next-header-key for
// both directions. After the first DH ratchet step on each side, the
// active header_key for that direction is rotated in from this seed,
// so Alice's HKs at time t == Bob's HKr at time t. header_key_send /
// header_key_recv stay zero until that first rotation.
// shared_secret[32..64) belongs to the 64B X3DH SK and is reserved
// for future Signal-spec wire elements; not consumed by DR.
memcpy(state.next_header_key_send, shared_secret.data + 64, 32);
memcpy(state.next_header_key_recv, shared_secret.data + 64, 32);
// Convert + store both identity pubs as X25519 form (used as MAC binding).
if (crypto_sign_ed25519_pk_to_curve25519(state.local_identity_pub,
local_identity_pub.data) != 0 ||
crypto_sign_ed25519_pk_to_curve25519(state.remote_identity_pub,
remote_identity_pub.data) != 0) {
err = "identity_pub_conversion_failed"; goto cleanup;
}
if (is_alice) {
// Alice uses the remote identity X25519 form as her initial dh_recv_public.
memcpy(state.dh_recv_public, state.remote_identity_pub,
crypto_box_PUBLICKEYBYTES);
if (crypto_box_keypair(state.dh_send_public, state.dh_send_private) != 0) {
err = "key_generation_failed"; goto cleanup;
}
state.dh_recv_initialized = true;
// Initial send ratchet: KDF_RK_HE(root, DH(alice_eph_priv, bob_pub)).
// L=96 output: root(32) || send_chain(32) || next_header_key_send(32).
// The previously-X3DH-seeded NHKs rotates into HKs at this step.
if (crypto_scalarmult(dh_out, state.dh_send_private,
state.dh_recv_public) != 0) {
err = "dh_failed"; goto cleanup;
}
if (hkdf_sha256(kdf_out, 96, state.root_key, 32, dh_out, 32,
(const unsigned char *)"DR-RK", 5) != 0) {
err = "kdf_failed"; goto cleanup;
}
memcpy(state.header_key_send, state.next_header_key_send, 32);
memcpy(state.root_key, kdf_out, 32);
memcpy(state.send_chain_key, kdf_out + 32, 32);
memcpy(state.next_header_key_send, kdf_out + 64, 32);
} else {
// Bob's self_identity_priv is his Ed25519 secret key. Convert to
// X25519 priv for the initial send ratchet.
if (self_priv.size != crypto_sign_SECRETKEYBYTES) {
err = "invalid_self_priv_size"; goto cleanup;
}
if (crypto_sign_ed25519_sk_to_curve25519(state.dh_send_private,
self_priv.data) != 0) {
err = "identity_priv_conversion_failed"; goto cleanup;
}
crypto_scalarmult_base(state.dh_send_public, state.dh_send_private);
// dh_recv_public stays zero; dh_recv_initialized stays false.
// send_chain_key / recv_chain_key stay zero -- set on first receive.
}
state.initialized = true;
cleanup:
sodium_memzero(dh_out, sizeof(dh_out));
sodium_memzero(kdf_out, sizeof(kdf_out));
if (err) {
sodium_memzero(&state, sizeof(state));
return enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, err));
}
ERL_NIF_TERM dr_session_term;
unsigned char *dr_session_data =
enif_make_new_binary(env, DR_STATE_SIZE, &dr_session_term);
memcpy(dr_session_data, &state, DR_STATE_SIZE);
sodium_memzero(&state, sizeof(state));
return enif_make_tuple2(env, enif_make_atom(env, "ok"), dr_session_term);
}
// Cipher + MAC + envelope. On success returns NULL, *out_wire is an
// enif_alloc'd buffer of *out_wire_len bytes (caller frees with enif_free),
// and *state is advanced in place. On failure returns a static error-name
// string and zeroizes any sensitive locals; state is left unmodified.
static const char *dr_encrypt_core(double_ratchet_state_t *state,
const unsigned char *pt, size_t pt_len,
unsigned char **out_wire, size_t *out_wire_len)
{
const char *err = NULL;
unsigned char message_key[DR_MESSAGE_KEY_SIZE];
unsigned char next_chain[DR_CHAIN_KEY_SIZE];
unsigned char cipher_key[32], mac_key[32], iv[16];
unsigned char hcipher[32];
unsigned char inner_header[64];
unsigned char mac[DR_MAC_LEN];
unsigned char *cbc_buf = NULL;
unsigned char *enc_header = NULL;
unsigned char *envelope = NULL;
unsigned char *wire = NULL;
size_t cbc_len = 0, header_ct_len = 0;
size_t inner_header_len = 0, enc_header_len = 0, envelope_len = 0;
size_t wire_len = 0;
if (!state->initialized) { err = "session_not_initialized"; goto cleanup; }
// Bob cannot send before receiving Alice's first message -- his
// send_chain_key is only derived during the receive ratchet.
if (!state->dh_recv_initialized) { err = "must_receive_first"; goto cleanup; }
// Derive per-message keys: cipher_key(32) || mac_key(32) || iv(16)
// from messageKey via HKDF (info="WhisperMessageKeys", Signal spec).
derive_message_key(message_key, state->send_chain_key);
advance_chain_key(next_chain, state->send_chain_key);
if (dr_derive_message_keys(cipher_key, mac_key, iv, message_key) != 0) {
err = "kdf_failed"; goto cleanup;
}
// AES-256-CBC + PKCS#7 encrypt. Output is always plaintext_len + 1..16.
cbc_buf = enif_alloc(pt_len + 16);
if (!cbc_buf) { err = "memory_allocation_failed"; goto cleanup; }
if (dr_aes_cbc_encrypt(cbc_buf, &cbc_len, pt, pt_len, cipher_key, iv) != 0) {
err = "encryption_failed"; goto cleanup;
}
// Inner header plaintext: protobuf fields 1-3 only (ratchet_key, counter,
// previous_counter). Max 46 bytes.
inner_header_len = dr_serialize_header(inner_header, state->dh_send_public, 32,
state->send_message_number,
state->prev_send_length);
// Encrypt the inner header under the current send header_key with a
// fresh random 16B IV. The wire form of enc_header is `iv || ciphertext`
// so the receiver can run AES-CBC-decrypt with each candidate header_key
// against the same IV during trial-decrypt. Reusing a static IV across
// messages in a chain would expose identical leading blocks (the inner
// header's first 16B are an invariant ratchet_key prefix).
if (dr_derive_header_cipher_key(hcipher, state->header_key_send) != 0) {
err = "kdf_failed"; goto cleanup;
}
enc_header = enif_alloc(16 + inner_header_len + 16);
if (!enc_header) { err = "memory_allocation_failed"; goto cleanup; }
randombytes_buf(enc_header, 16); // random IV at the head
if (dr_aes_cbc_encrypt(enc_header + 16, &header_ct_len,
inner_header, inner_header_len,
hcipher, enc_header) != 0) {
err = "encryption_failed"; goto cleanup;
}
enc_header_len = 16 + header_ct_len;
// Outer envelope protobuf: {enc_header = 1, ciphertext = 2}.
// Max overhead: 2 tags + 2 varints = 22 bytes.
envelope = enif_alloc(22 + enc_header_len + cbc_len);
if (!envelope) { err = "memory_allocation_failed"; goto cleanup; }
envelope_len = dr_serialize_envelope(envelope, enc_header, enc_header_len,
cbc_buf, cbc_len);
// MAC over local_id_pub || remote_id_pub || version || envelope.
if (dr_compute_mac(mac, mac_key,
state->local_identity_pub, state->remote_identity_pub,
DR_VERSION_BYTE, envelope, envelope_len) != 0) {
err = "mac_failed"; goto cleanup;
}
// Wire envelope: version(1) || envelope || mac(8).
wire_len = 1 + envelope_len + DR_MAC_LEN;
wire = enif_alloc(wire_len);
if (!wire) { err = "memory_allocation_failed"; goto cleanup; }
wire[0] = DR_VERSION_BYTE;
memcpy(wire + 1, envelope, envelope_len);
memcpy(wire + 1 + envelope_len, mac, DR_MAC_LEN);
// Commit state and transfer wire to caller.
memcpy(state->send_chain_key, next_chain, DR_CHAIN_KEY_SIZE);
state->send_message_number++;
*out_wire = wire;
*out_wire_len = wire_len;
wire = NULL; // ownership transferred; cleanup must not free
cleanup:
sodium_memzero(message_key, sizeof(message_key));
sodium_memzero(next_chain, sizeof(next_chain));
sodium_memzero(cipher_key, sizeof(cipher_key));
sodium_memzero(mac_key, sizeof(mac_key));
sodium_memzero(iv, sizeof(iv));
sodium_memzero(hcipher, sizeof(hcipher));
sodium_memzero(inner_header, sizeof(inner_header));
if (cbc_buf) enif_free(cbc_buf);
if (enc_header) enif_free(enc_header);
if (envelope) enif_free(envelope);
if (wire) enif_free(wire);
return err;
}
// Double Ratchet: Encrypt message.
ERL_NIF_TERM dr_encrypt(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
if (argc != 2) {
return enif_make_badarg(env);
}
ErlNifBinary dr_session, plaintext;
if (!enif_inspect_binary(env, argv[0], &dr_session) ||
!enif_inspect_binary(env, argv[1], &plaintext)) {
return enif_make_badarg(env);
}
if (dr_session.size != DR_STATE_SIZE) {
return enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, "invalid_session_size"));
}
double_ratchet_state_t state;
memcpy(&state, dr_session.data, DR_STATE_SIZE);
unsigned char *wire = NULL;
size_t wire_len = 0;
const char *err = dr_encrypt_core(&state, plaintext.data, plaintext.size,
&wire, &wire_len);
if (err) {
sodium_memzero(&state, sizeof(state));
return enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, err));
}
ERL_NIF_TERM encrypted_term;
unsigned char *encrypted_data = enif_make_new_binary(env, wire_len, &encrypted_term);
memcpy(encrypted_data, wire, wire_len);
enif_free(wire);
ERL_NIF_TERM updated_session_term;
unsigned char *updated_session_data =
enif_make_new_binary(env, DR_STATE_SIZE, &updated_session_term);
memcpy(updated_session_data, &state, DR_STATE_SIZE);
sodium_memzero(&state, sizeof(state));
return enif_make_tuple2(env, enif_make_atom(env, "ok"),
enif_make_tuple2(env, encrypted_term, updated_session_term));
}
// Double Ratchet: Encrypt Alice's first message and wrap it in a
// PreKeySignalMessage so Bob can derive the X3DH SK before decrypting.
// argv[0] = DR session binary
// argv[1] = plaintext binary
// argv[2] = {RegistrationId :: uint, OneTimePreKeyIdOrUndefined,
// SignedPreKeyId :: uint, BaseKey :: 32B binary}
// where BaseKey is Alice's X3DH ephemeral pub returned by
// process_pre_key_bundle/2.
// Returns {ok, {PksmWire, NewSession}} | {error, Atom}.
ERL_NIF_TERM dr_encrypt_prekey(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
if (argc != 3) {
return enif_make_badarg(env);
}
ErlNifBinary dr_session, plaintext;
if (!enif_inspect_binary(env, argv[0], &dr_session) ||
!enif_inspect_binary(env, argv[1], &plaintext)) {
return enif_make_badarg(env);
}
const ERL_NIF_TERM *info_tup;
int info_arity = 0;
if (!enif_get_tuple(env, argv[2], &info_arity, &info_tup) || info_arity != 4) {
return enif_make_badarg(env);
}
unsigned int registration_id = 0;
unsigned int signed_pre_key_id = 0;
if (!enif_get_uint(env, info_tup[0], &registration_id) ||
!enif_get_uint(env, info_tup[2], &signed_pre_key_id)) {
return enif_make_badarg(env);
}
unsigned int pre_key_id = 0;
int has_pre_key_id = 0;
if (enif_is_atom(env, info_tup[1])) {
char atom_buf[16];
if (enif_get_atom(env, info_tup[1], atom_buf, sizeof(atom_buf),
ERL_NIF_LATIN1) <= 0 ||
strcmp(atom_buf, "undefined") != 0) {
return enif_make_badarg(env);
}
} else if (enif_get_uint(env, info_tup[1], &pre_key_id)) {
has_pre_key_id = 1;
} else {
return enif_make_badarg(env);
}
ErlNifBinary base_key;
if (!enif_inspect_binary(env, info_tup[3], &base_key) ||
base_key.size != crypto_box_PUBLICKEYBYTES) {
return enif_make_badarg(env);
}
if (dr_session.size != DR_STATE_SIZE) {
return enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, "invalid_session_size"));
}
const char *err = NULL;
double_ratchet_state_t state;
unsigned char *inner_wire = NULL;
size_t inner_wire_len = 0;
unsigned char *pksm_buf = NULL;
int pksm_len = -1;
ERL_NIF_TERM wire_term = 0;
memcpy(&state, dr_session.data, DR_STATE_SIZE);
err = dr_encrypt_core(&state, plaintext.data, plaintext.size,
&inner_wire, &inner_wire_len);
if (err) goto cleanup;
// PKSM body: registration_id, base_key(32), identity_key(32), optional
// pre_key_id, signed_pre_key_id, inner_message. Worst-case overhead is
// 6 tags + 6 varints + the two 32B keys = ~80B + inner.
pksm_buf = enif_alloc(80 + inner_wire_len);
if (!pksm_buf) { err = "memory_allocation_failed"; goto cleanup; }
pksm_len = pksm_encode(pksm_buf, 80 + inner_wire_len,
registration_id,
base_key.data, base_key.size,
state.local_identity_pub, crypto_box_PUBLICKEYBYTES,
pre_key_id, has_pre_key_id,
signed_pre_key_id,
inner_wire, inner_wire_len);
if (pksm_len < 0) { err = "pksm_encode_failed"; goto cleanup; }
{
size_t wire_len = 1 + (size_t)pksm_len;
unsigned char *wire_data = enif_make_new_binary(env, wire_len, &wire_term);
wire_data[0] = DR_VERSION_BYTE;
memcpy(wire_data + 1, pksm_buf, (size_t)pksm_len);
}
cleanup:
if (inner_wire) enif_free(inner_wire);
if (pksm_buf) enif_free(pksm_buf);
if (err) {
sodium_memzero(&state, sizeof(state));
return enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, err));
}
ERL_NIF_TERM updated_session_term;
unsigned char *updated_session_data =
enif_make_new_binary(env, DR_STATE_SIZE, &updated_session_term);
memcpy(updated_session_data, &state, DR_STATE_SIZE);
sodium_memzero(&state, sizeof(state));
return enif_make_tuple2(env, enif_make_atom(env, "ok"),
enif_make_tuple2(env, wire_term, updated_session_term));
}
// Double Ratchet: Decrypt message.
//
// DR-HE flow: parse outer envelope -> trial-decrypt enc_header under each
// candidate header_key (current HKr, then NHKr, then each MKSKIPPED slot's
// header_key). The first candidate whose AES-CBC unpads to a valid inner
// header protobuf identifies the path. MAC over the outer envelope is then
// verified against the message key for that path before the body is touched.
//
// State mutations happen on a stack copy and are only committed to the
// returned session binary on full success, so any path that errors out
// silently discards the partial state changes.
ERL_NIF_TERM dr_decrypt(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
if (argc != 2) {
return enif_make_badarg(env);
}
ErlNifBinary dr_session, ciphertext;
if (!enif_inspect_binary(env, argv[0], &dr_session) ||
!enif_inspect_binary(env, argv[1], &ciphertext)) {
return enif_make_badarg(env);
}
if (dr_session.size != DR_STATE_SIZE) {
return enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, "invalid_session_size"));
}
const char *err = NULL;
double_ratchet_state_t state;
unsigned char header_plain[64];
unsigned char message_key[DR_MESSAGE_KEY_SIZE];
unsigned char cipher_key[32], mac_key[32], iv[16];
unsigned char expected_mac[DR_MAC_LEN];
unsigned char *pt_buf = NULL;
size_t plaintext_len = 0;
size_t body_ct_len = 0;
ERL_NIF_TERM decrypted_term = 0;
memcpy(&state, dr_session.data, DR_STATE_SIZE);
if (!state.initialized) { err = "session_not_initialized"; goto cleanup; }
// Outer envelope: version(1) || protobuf || mac(8).
if (ciphertext.size < 1 + DR_MAC_LEN) { err = "message_too_short"; goto cleanup; }
if (ciphertext.data[0] != DR_VERSION_BYTE) { err = "unsupported_version"; goto cleanup; }
size_t envelope_len = ciphertext.size - 1 - DR_MAC_LEN;
const unsigned char *envelope = ciphertext.data + 1;
const unsigned char *received_mac = ciphertext.data + 1 + envelope_len;
const unsigned char *enc_header = NULL;
size_t enc_header_len = 0;
const unsigned char *body_ct = NULL;
if (dr_parse_envelope(envelope, envelope_len,
&enc_header, &enc_header_len,
&body_ct, &body_ct_len) != 0) {
err = "malformed_message"; goto cleanup;
}
if (body_ct_len == 0 || (body_ct_len % 16) != 0) {
err = "message_too_short"; goto cleanup;
}
// Trial-decrypt enc_header under each candidate header_key.
enum { PATH_CURRENT, PATH_RATCHET, PATH_SKIPPED } path = PATH_CURRENT;
int matched_skipped_idx = -1;
size_t header_plain_len = 0;
dr_message_t inner;
int found = 0;
if (hk_is_nonzero(state.header_key_recv) &&
dr_try_decrypt_header(header_plain, &header_plain_len, &inner,
enc_header, enc_header_len,
state.header_key_recv) == 0) {
path = PATH_CURRENT;
found = 1;
} else if (hk_is_nonzero(state.next_header_key_recv) &&
dr_try_decrypt_header(header_plain, &header_plain_len, &inner,
enc_header, enc_header_len,
state.next_header_key_recv) == 0) {
path = PATH_RATCHET;
found = 1;
} else {
for (int i = 0; i < MAX_SKIPPED_KEYS; i++) {
if (!state.mkskipped[i].occupied) continue;
if (dr_try_decrypt_header(header_plain, &header_plain_len, &inner,
enc_header, enc_header_len,
state.mkskipped[i].header_key) == 0) {
// Locate the slot matching this (header_key, counter) tuple.
int idx = mkskipped_find(&state, state.mkskipped[i].header_key,
inner.counter);
if (idx < 0) {
// Header decrypted under a chain's HK, but no cached
// message_key for that counter -- treat as malformed
// since we have nowhere to take this from.
err = "bad_mac"; goto cleanup;
}
matched_skipped_idx = idx;
path = PATH_SKIPPED;
found = 1;
break;
}
}
}
if (!found) { err = "bad_mac"; goto cleanup; }
// Apply path-specific state advance and derive the message key.
if (path == PATH_SKIPPED) {
mkskipped_pop(&state, matched_skipped_idx, message_key);
} else if (path == PATH_CURRENT) {
// Late delivery within the current chain: counter < recv_message_number
// means the message's key was already moved into MKSKIPPED when an
// earlier same-chain message advanced the receive chain past it.
// PATH_CURRENT trial-decrypt matched because the chain's HKr also
// keys those cached entries.
if (inner.counter < state.recv_message_number) {
int idx = mkskipped_find(&state, state.header_key_recv, inner.counter);
if (idx < 0) { err = "bad_mac"; goto cleanup; }
mkskipped_pop(&state, idx, message_key);
} else {
if (skip_message_keys(&state, inner.counter) != 0) {
err = "too_many_skipped"; goto cleanup;
}
derive_message_key(message_key, state.recv_chain_key);
advance_chain_key(state.recv_chain_key, state.recv_chain_key);
state.recv_message_number++;
}
} else { // PATH_RATCHET
if (skip_message_keys(&state, inner.previous_counter) != 0) {
err = "too_many_skipped"; goto cleanup;
}
if (dh_ratchet_recv(&state, inner.ratchet_key) != 0) {
err = "dh_ratchet_failed"; goto cleanup;
}
if (skip_message_keys(&state, inner.counter) != 0) {
err = "too_many_skipped"; goto cleanup;
}
derive_message_key(message_key, state.recv_chain_key);
advance_chain_key(state.recv_chain_key, state.recv_chain_key);
state.recv_message_number++;
}
// Derive per-message keys + verify MAC over the outer envelope.
// MAC binding (Signal-spec swap on receive): peer was sender, so their
// identity goes first.
if (dr_derive_message_keys(cipher_key, mac_key, iv, message_key) != 0) {
err = "kdf_failed"; goto cleanup;
}
if (dr_compute_mac(expected_mac, mac_key,
state.remote_identity_pub, state.local_identity_pub,
DR_VERSION_BYTE, envelope, envelope_len) != 0) {
err = "mac_failed"; goto cleanup;
}
if (CRYPTO_memcmp(expected_mac, received_mac, DR_MAC_LEN) != 0) {
err = "bad_mac"; goto cleanup;
}
// MAC OK -- decrypt body. PKCS#7 padding is validated by EVP_DecryptFinal;
// the padding-oracle channel is closed since we already verified the MAC.
pt_buf = enif_alloc(body_ct_len);
if (!pt_buf) { err = "memory_allocation_failed"; goto cleanup; }
if (dr_aes_cbc_decrypt(pt_buf, &plaintext_len,
body_ct, body_ct_len,
cipher_key, iv) != 0) {
err = "decryption_failed"; goto cleanup;
}
{
unsigned char *decrypted_data =
enif_make_new_binary(env, plaintext_len, &decrypted_term);
memcpy(decrypted_data, pt_buf, plaintext_len);
}
sodium_memzero(pt_buf, body_ct_len); // plaintext held briefly in pt_buf
cleanup:
sodium_memzero(header_plain, sizeof(header_plain));
sodium_memzero(message_key, sizeof(message_key));
sodium_memzero(cipher_key, sizeof(cipher_key));
sodium_memzero(mac_key, sizeof(mac_key));
sodium_memzero(iv, sizeof(iv));
if (pt_buf) enif_free(pt_buf);
if (err) {
sodium_memzero(&state, sizeof(state));
return enif_make_tuple2(env, enif_make_atom(env, "error"),
enif_make_atom(env, err));
}
ERL_NIF_TERM updated_session_term;
unsigned char *updated_session_data =
enif_make_new_binary(env, DR_STATE_SIZE, &updated_session_term);
memcpy(updated_session_data, &state, DR_STATE_SIZE);
sodium_memzero(&state, sizeof(state));
return enif_make_tuple2(env, enif_make_atom(env, "ok"),
enif_make_tuple2(env, decrypted_term, updated_session_term));
}