libsignal_protocol

Hex.pmHex.pmLicense

Elixir wrapper over :libsignal_protocol_nif -- the Erlang NIF in the parent repo. The wrapper is a thin facade: it forwards calls to the NIF and returns whatever the NIF returns ({:ok, term} | {:error, atom}).

The NIF is not built by this package. Either depend on the parent project's build artifacts or run make build in the repo root first. A missing NIF raises UndefinedFunctionError at the call site.

Install

def deps do
[{:libsignal_protocol, "~> 0.2"}]
end

Modules

Quick start

:ok = LibsignalProtocol.init()
# Each party generates an identity key pair (Ed25519)
{:ok, {alice_pub, alice_priv}} = SignalProtocol.generate_identity_key_pair()
{:ok, {bob_pub, bob_priv}} = SignalProtocol.generate_identity_key_pair()
# Pre-keys (Bob publishes these)
{:ok, {opk_id, opk_pub}} = SignalProtocol.generate_pre_key(1)
{:ok, {spk_id, spk_pub, spk_sig}} = SignalProtocol.generate_signed_pre_key(bob_priv, 2)

X3DH against a remote bundle. The bundle binary is remote_identity_pub(32) ++ signed_prekey_pub(32) ++ signature(64) with an optional trailing 32-byte one-time prekey. The signature is Ed25519 over signed_prekey_pub under the remote identity key.

bundle = <<bob_pub::binary, spk_pub::binary, spk_sig::binary, opk_pub::binary>>
{:ok, {shared_secret, alice_eph_pub}} =
SignalProtocol.process_pre_key_bundle(alice_priv, bundle)

Double Ratchet:

# Alice is the initiator -> is_alice = 1
{:ok, alice_dr} =
SignalProtocol.init_double_ratchet(shared_secret, alice_pub, bob_pub, <<>>, 1)
# Alice's first message is wrapped in a PreKeySignalMessage envelope
pre_key_info = {_reg_id = 1, opk_id, spk_id, alice_eph_pub}
{:ok, {wire, alice_dr}} =
SignalProtocol.dr_encrypt_prekey(alice_dr, "hello", pre_key_info)
# Subsequent messages
{:ok, {ct, alice_dr}} = SignalProtocol.dr_encrypt_message(alice_dr, "second")

Bob recovers the same shared secret from the envelope:

{:ok, {_reg, _base, _id, opk_id, spk_id, inner}} = SignalProtocol.pksm_decode(wire)
{:ok, bob_shared} =
SignalProtocol.process_pre_key_bundle_bob(
bob_priv, spk_priv, opk_priv, alice_pub, alice_eph_pub
)
# is_alice = 0 for Bob
{:ok, bob_dr} =
SignalProtocol.init_double_ratchet(bob_shared, bob_pub, alice_pub, bob_priv, 0)
{:ok, {"hello", bob_dr}} = SignalProtocol.dr_decrypt_message(bob_dr, inner)

Full inline docs live in lib/signal_protocol.ex. End-to-end test flows are in the parent repo under test/erl/unit/protocol/.

Pre-key bundles

SignalProtocol.PreKeyBundle handles the higher-level bundle format used between parties (versioned, includes key IDs):

{:ok, bundle} =
SignalProtocol.PreKeyBundle.create(
registration_id, identity_key, {opk_id, opk_pub},
{spk_id, spk_pub, spk_sig}, base_key
)
{:ok, parsed} = SignalProtocol.PreKeyBundle.parse(bundle)
:ok = SignalProtocol.PreKeyBundle.verify_signature(bundle)

Errors

NIF errors are returned verbatim. The wrapper does not translate, rename, or wrap them. If the NIF returns {:error, :invalid_signature}, that is what you get.

License

Apache-2.0.