SignalProtocol (libsignal_protocol v0.3.0)

View Source

Thin Elixir facade over the :libsignal_protocol_nif Erlang NIF.

All functions return {:ok, term} | {:error, term} as the NIF does. A missing NIF raises UndefinedFunctionError at the call site -- callers must ensure the NIF is built and on the load path before use.

Summary

Functions

Encrypt Alice's first message and wrap it in a PreKeySignalMessage envelope so Bob can recover the X3DH shared secret before decrypting.

Generates an X25519 pre-key. Returns {key_id, public, private}; keep the private half to run process_pre_key_bundle_bob/5 against the published pre-key.

Generates an X25519 signed pre-key. Returns {key_id, public, private, signature} -- the signature is Ed25519 over the public key under identity_key.

Optional probe that the NIF is loaded. libsodium is initialised when the NIF library loads, so no per-VM setup call is required; this raises UndefinedFunctionError if the NIF is missing and returns :ok otherwise.

Decode a PreKeySignalMessage wire envelope produced by dr_encrypt_prekey/3.

Performs X3DH key agreement against a remote pre-key bundle.

Bob's side of X3DH. Recovers the same 96-byte shared secret Alice derived via process_pre_key_bundle/2 (32B DR root key || two 32B per-direction DR-HE header-key seeds).

Functions

dr_decrypt_message(dr_session, ciphertext)

@spec dr_decrypt_message(binary(), binary()) ::
  {:ok, {binary(), binary()}} | {:error, term()}

dr_encrypt_message(dr_session, message)

@spec dr_encrypt_message(binary(), binary()) ::
  {:ok, {binary(), binary()}} | {:error, term()}

dr_encrypt_prekey(dr_session, message, pre_key_info)

@spec dr_encrypt_prekey(
  binary(),
  binary(),
  {non_neg_integer(), non_neg_integer() | nil, non_neg_integer(), binary()}
) :: {:ok, {binary(), binary()}} | {:error, term()}

Encrypt Alice's first message and wrap it in a PreKeySignalMessage envelope so Bob can recover the X3DH shared secret before decrypting.

pre_key_info is a 4-tuple {registration_id, one_time_pre_key_id_or_nil, signed_pre_key_id, alice_x3dh_ephemeral_pub}. The ephemeral pub is the 32-byte X25519 key returned by process_pre_key_bundle/2.

Returns {:ok, {pksm_wire_bytes, new_session}} on success.

generate_identity_key_pair()

@spec generate_identity_key_pair() :: {:ok, {binary(), binary()}} | {:error, term()}

generate_pre_key(key_id)

@spec generate_pre_key(non_neg_integer()) ::
  {:ok, {non_neg_integer(), binary(), binary()}} | {:error, atom()}

Generates an X25519 pre-key. Returns {key_id, public, private}; keep the private half to run process_pre_key_bundle_bob/5 against the published pre-key.

generate_signed_pre_key(identity_key, key_id)

@spec generate_signed_pre_key(binary(), non_neg_integer()) ::
  {:ok, {non_neg_integer(), binary(), binary(), binary()}} | {:error, atom()}

Generates an X25519 signed pre-key. Returns {key_id, public, private, signature} -- the signature is Ed25519 over the public key under identity_key.

init()

@spec init() :: :ok

Optional probe that the NIF is loaded. libsodium is initialised when the NIF library loads, so no per-VM setup call is required; this raises UndefinedFunctionError if the NIF is missing and returns :ok otherwise.

init_double_ratchet(shared_secret, local_identity_pub, remote_identity_pub, self_identity_priv, is_alice)

@spec init_double_ratchet(binary(), binary(), binary(), binary(), 0 | 1) ::
  {:ok, binary()} | {:error, term()}

pksm_decode(wire)

@spec pksm_decode(binary()) ::
  {:ok,
   {non_neg_integer(), binary(), binary(), non_neg_integer() | nil,
    non_neg_integer(), binary()}}
  | {:error, term()}

Decode a PreKeySignalMessage wire envelope produced by dr_encrypt_prekey/3.

Returns {:ok, {registration_id, base_key, identity_key, one_time_pre_key_id_or_nil, signed_pre_key_id, inner_dr_message}} on success, or {:error, :malformed_message} on malformed input.

Bob's typical flow:

  1. pksm_decode/1 to extract fields and the inner DR message.
  2. Look up his SPK + OPK private keys by id.
  3. process_pre_key_bundle_bob/5 to derive the same SK Alice has.
  4. init_double_ratchet/5 (as Bob, is_alice = 0).
  5. dr_decrypt_message/2 on the inner DR message.

process_pre_key_bundle(local_identity_priv, bundle)

@spec process_pre_key_bundle(binary(), binary()) ::
  {:ok, {binary(), binary()}} | {:error, term()}

Performs X3DH key agreement against a remote pre-key bundle.

The bundle is a binary in the format expected by the C NIF: remote_identity_pub(32) ++ signed_prekey_pub(32) ++ signature(64) with an optional trailing one_time_prekey(32). The signature is Ed25519 (crypto_sign_detached) over signed_prekey_pub under the remote identity key.

Returns {:ok, {shared_secret(96), ephemeral_pub(32)}} on success. The 96-byte shared secret is root(32) || seed_a(32) || seed_b(32) and is fed directly into init_double_ratchet/5.

process_pre_key_bundle_bob(identity_priv, signed_pre_key_priv, one_time_pre_key_priv, remote_identity_pub, remote_ephemeral_pub)

@spec process_pre_key_bundle_bob(binary(), binary(), binary(), binary(), binary()) ::
  {:ok, binary()} | {:error, term()}

Bob's side of X3DH. Recovers the same 96-byte shared secret Alice derived via process_pre_key_bundle/2 (32B DR root key || two 32B per-direction DR-HE header-key seeds).

Inputs:

  • identity_priv - Bob's 64-byte Ed25519 identity private key.
  • signed_pre_key_priv - 32-byte X25519 private key matching the SPK Alice consumed from Bob's bundle.
  • one_time_pre_key_priv - 32-byte X25519 private key matching the OPK Alice consumed, or <<>> if no OPK was used.
  • remote_identity_pub - Alice's 32-byte Ed25519 identity public key, extracted from the PreKeySignalMessage.
  • remote_ephemeral_pub - Alice's 32-byte X25519 ephemeral public key, also extracted from the PreKeySignalMessage (the base_key field).