Packages
WhatsApp Web API client for Elixir. Full-featured port of Baileys with end-to-end Signal Protocol encryption, multi-device support, groups, communities, media, newsletters, and native BEAM fault tolerance.
Current section
10 Versions
Jump to
Current section
10 Versions
Compare versions
29
files changed
+913
additions
-477
deletions
| @@ -1,5 +1,18 @@ | |
| 1 1 | # Changelog |
| 2 2 | |
| 3 | + ## [0.1.0-alpha.3] - 2026-03-19 |
| 4 | + |
| 5 | + ### Changed |
| 6 | + |
| 7 | + - Event emission no longer blocks callers on slow subscribers |
| 8 | + - Protocol and connection logging demoted from warning to debug level |
| 9 | + - Improved binary node encoding and decoding performance |
| 10 | + |
| 11 | + ### Fixed |
| 12 | + |
| 13 | + - Programmer errors in Noise protocol functions are no longer silently swallowed |
| 14 | + - Rust NIF error handling hardened to eliminate panic paths |
| 15 | + |
| 3 16 | ## [0.1.0-alpha.2] - 2026-03-19 |
| 4 17 | |
| 5 18 | Initial alpha release. |
| @@ -5,7 +5,7 @@ | |
| 5 5 | <<"https://github.com/jeffhuen/baileys_ex/blob/main/CHANGELOG.md">>}, |
| 6 6 | {<<"GitHub">>,<<"https://github.com/jeffhuen/baileys_ex">>}]}. |
| 7 7 | {<<"name">>,<<"baileys_ex">>}. |
| 8 | - {<<"version">>,<<"0.1.0-alpha.2">>}. |
| 8 | + {<<"version">>,<<"0.1.0-alpha.3">>}. |
| 9 9 | {<<"description">>, |
| 10 10 | <<"WhatsApp Web API client for Elixir. Full-featured port of Baileys with end-to-end Signal Protocol encryption, multi-device support, groups, communities, media, newsletters, and native BEAM fault tolerance.">>}. |
| 11 11 | {<<"elixir">>,<<"~> 1.19">>}. |
| @@ -114,7 +114,8 @@ | |
| 114 114 | <<"lib/baileys_ex/util/lt_hash.ex">>,<<"lib/baileys_ex/wam.ex">>, |
| 115 115 | <<"lib/baileys_ex/wam/binary_info.ex">>, |
| 116 116 | <<"lib/baileys_ex/wam/definitions.ex">>,<<"lib/baileys_ex/wam/encoder.ex">>, |
| 117 | - <<"native/baileys_nif/src/lib.rs">>,<<"native/baileys_nif/src/noise.rs">>, |
| 117 | + <<"native/baileys_nif/src/error.rs">>,<<"native/baileys_nif/src/lib.rs">>, |
| 118 | + <<"native/baileys_nif/src/noise.rs">>, |
| 118 119 | <<"native/baileys_nif/src/xeddsa.rs">>,<<"priv/proto/WAProto.proto">>, |
| 119 120 | <<"priv/wam/definitions.json">>, |
| 120 121 | <<"user_docs/getting-started/first-connection.md">>, |
| @@ -7,10 +7,170 @@ defmodule BaileysEx.Auth.FilePersistence do | |
| 7 7 | |
| 8 8 | alias BaileysEx.Auth.KeyStore |
| 9 9 | alias BaileysEx.Auth.State |
| 10 | + alias BaileysEx.Protocol.Proto.ADVSignedDeviceIdentity |
| 11 | + alias BaileysEx.Protocol.Proto.MessageKey |
| 12 | + alias BaileysEx.Signal.Group.SenderChainKey |
| 13 | + alias BaileysEx.Signal.Group.SenderKeyRecord |
| 14 | + alias BaileysEx.Signal.Group.SenderKeyState |
| 15 | + alias BaileysEx.Signal.Group.SenderMessageKey |
| 16 | + alias BaileysEx.Signal.SessionRecord |
| 10 17 | |
| 11 18 | @buffer_tag "Buffer" |
| 12 19 | @default_dir "baileys_auth_info" |
| 13 20 | |
| 21 | + # Persistence decoding is intentionally bounded to explicit atoms/modules instead of |
| 22 | + # relying on generic atom reconstruction from disk. This keeps auth loading |
| 23 | + # deterministic across fresh and warm VMs and avoids reopening unbounded atom |
| 24 | + # creation from persisted files. |
| 25 | + # |
| 26 | + # Top-level struct fields are derived from the owning structs so they track schema |
| 27 | + # changes automatically. Nested map atoms still need explicit maintenance because |
| 28 | + # they are persisted as atom keys without dedicated structs. When adding persisted |
| 29 | + # nested fields, update the relevant decode context below and extend the fresh-VM |
| 30 | + # regressions in `test/baileys_ex/auth/file_persistence_test.exs`. |
| 31 | + @empty_decode_context %{atoms: %{}, modules: %{}} |
| 32 | + |
| 33 | + @credential_decode_context %{ |
| 34 | + atoms: |
| 35 | + State.__struct__() |
| 36 | + |> Map.keys() |
| 37 | + |> Enum.reject(&(&1 == :__struct__)) |
| 38 | + |> Kernel.++( |
| 39 | + ADVSignedDeviceIdentity.__struct__() |
| 40 | + |> Map.keys() |
| 41 | + |> Enum.reject(&(&1 == :__struct__)) |
| 42 | + ) |
| 43 | + |> Kernel.++( |
| 44 | + MessageKey.__struct__() |
| 45 | + |> Map.keys() |
| 46 | + |> Enum.reject(&(&1 == :__struct__)) |
| 47 | + ) |
| 48 | + |> Kernel.++([ |
| 49 | + :public, |
| 50 | + :private, |
| 51 | + :key_pair, |
| 52 | + :key_id, |
| 53 | + :signature, |
| 54 | + :identifier, |
| 55 | + :identifier_key, |
| 56 | + :name, |
| 57 | + :device_id, |
| 58 | + :lid, |
| 59 | + :key, |
| 60 | + :message_timestamp, |
| 61 | + :unarchive_chats, |
| 62 | + :default_disappearing_mode, |
| 63 | + # processed_history_messages message key atoms |
| 64 | + :id, |
| 65 | + :remote_jid, |
| 66 | + :remote_jid_alt, |
| 67 | + :participant, |
| 68 | + :participant_alt, |
| 69 | + :from_me, |
| 70 | + :addressing_mode, |
| 71 | + :server_id, |
| 72 | + # addressing_mode atom values |
| 73 | + :pn, |
| 74 | + :lid |
| 75 | + ]) |
| 76 | + |> Enum.uniq() |
| 77 | + |> Map.new(&{Atom.to_string(&1), &1}), |
| 78 | + modules: |
| 79 | + [State, ADVSignedDeviceIdentity, MessageKey] |
| 80 | + |> Map.new(&{Atom.to_string(&1), &1}) |
| 81 | + } |
| 82 | + |
| 83 | + @session_decode_context %{ |
| 84 | + atoms: |
| 85 | + SessionRecord.__struct__() |
| 86 | + |> Map.keys() |
| 87 | + |> Enum.reject(&(&1 == :__struct__)) |
| 88 | + |> Kernel.++([ |
| 89 | + :current_ratchet, |
| 90 | + :root_key, |
| 91 | + :ephemeral_key_pair, |
| 92 | + :public, |
| 93 | + :private, |
| 94 | + :last_remote_ephemeral, |
| 95 | + :previous_counter, |
| 96 | + :index_info, |
| 97 | + :remote_identity_key, |
| 98 | + :local_identity_key, |
| 99 | + :base_key, |
| 100 | + :base_key_type, |
| 101 | + :closed, |
| 102 | + :chains, |
| 103 | + :pending_pre_key, |
| 104 | + :registration_id, |
| 105 | + :chain_key, |
| 106 | + :counter, |
| 107 | + :key, |
| 108 | + :chain_type, |
| 109 | + :message_keys, |
| 110 | + :pre_key_id, |
| 111 | + :signed_pre_key_id, |
| 112 | + :sending, |
| 113 | + :receiving |
| 114 | + ]) |
| 115 | + |> Enum.uniq() |
| 116 | + |> Map.new(&{Atom.to_string(&1), &1}), |
| 117 | + modules: |
| 118 | + [SessionRecord] |
| 119 | + |> Map.new(&{Atom.to_string(&1), &1}) |
| 120 | + } |
| 121 | + |
| 122 | + @sender_key_decode_context %{ |
| 123 | + atoms: |
| 124 | + SenderKeyRecord.__struct__() |
| 125 | + |> Map.keys() |
| 126 | + |> Enum.reject(&(&1 == :__struct__)) |
| 127 | + |> Kernel.++( |
| 128 | + SenderKeyState.__struct__() |
| 129 | + |> Map.keys() |
| 130 | + |> Enum.reject(&(&1 == :__struct__)) |
| 131 | + ) |
| 132 | + |> Kernel.++( |
| 133 | + SenderChainKey.__struct__() |
| 134 | + |> Map.keys() |
| 135 | + |> Enum.reject(&(&1 == :__struct__)) |
| 136 | + ) |
| 137 | + |> Kernel.++( |
| 138 | + SenderMessageKey.__struct__() |
| 139 | + |> Map.keys() |
| 140 | + |> Enum.reject(&(&1 == :__struct__)) |
| 141 | + ) |
| 142 | + |> Kernel.++([:public, :private]) |
| 143 | + |> Enum.uniq() |
| 144 | + |> Map.new(&{Atom.to_string(&1), &1}), |
| 145 | + modules: |
| 146 | + [SenderKeyRecord, SenderKeyState, SenderChainKey, SenderMessageKey] |
| 147 | + |> Map.new(&{Atom.to_string(&1), &1}) |
| 148 | + } |
| 149 | + |
| 150 | + @pre_key_decode_context %{ |
| 151 | + atoms: %{Atom.to_string(:public) => :public, Atom.to_string(:private) => :private}, |
| 152 | + modules: %{} |
| 153 | + } |
| 154 | + |
| 155 | + @app_state_sync_key_decode_context %{ |
| 156 | + atoms: %{Atom.to_string(:key_data) => :key_data}, |
| 157 | + modules: %{} |
| 158 | + } |
| 159 | + |
| 160 | + @app_state_sync_version_decode_context %{ |
| 161 | + atoms: |
| 162 | + [:version, :hash, :index_value_map, :value_mac] |
| 163 | + |> Map.new(&{Atom.to_string(&1), &1}), |
| 164 | + modules: %{} |
| 165 | + } |
| 166 | + |
| 167 | + @tc_token_decode_context %{ |
| 168 | + atoms: |
| 169 | + [:token, :timestamp] |
| 170 | + |> Map.new(&{Atom.to_string(&1), &1}), |
| 171 | + modules: %{} |
| 172 | + } |
| 173 | + |
| 14 174 | @type multi_file_auth_state :: %{ |
| 15 175 | required(:state) => State.t(), |
| 16 176 | required(:connect_opts) => keyword(), |
| @@ -49,7 +209,7 @@ defmodule BaileysEx.Auth.FilePersistence do | |
| 49 209 | @spec load_credentials(Path.t()) :: {:ok, State.t()} | {:error, term()} |
| 50 210 | def load_credentials(path) when is_binary(path) do |
| 51 211 | with :ok <- ensure_directory(path) do |
| 52 | - case read_data(path, "creds.json") do |
| 212 | + case read_data(path, "creds.json", &decode_credentials/1) do |
| 53 213 | {:ok, nil} -> |
| 54 214 | {:ok, State.new()} |
| 55 215 | |
| @@ -76,7 +236,8 @@ defmodule BaileysEx.Auth.FilePersistence do | |
| 76 236 | """ |
| 77 237 | @spec save_credentials(Path.t(), State.t()) :: :ok | {:error, term()} |
| 78 238 | def save_credentials(path, %State{} = state) when is_binary(path) do |
| 79 | - with :ok <- ensure_directory(path) do |
| 239 | + with :ok <- validate_additional_data(state.additional_data), |
| 240 | + :ok <- ensure_directory(path) do |
| 80 241 | write_data(path, "creds.json", state) |
| 81 242 | end |
| 82 243 | end |
| @@ -95,7 +256,7 @@ defmodule BaileysEx.Auth.FilePersistence do | |
| 95 256 | with :ok <- ensure_directory(path) do |
| 96 257 | file_name = data_file_name(type, id) |
| 97 258 | |
| 98 | - case read_data(path, file_name) do |
| 259 | + case read_data(path, file_name, &decode_key_data(type, &1)) do |
| 99 260 | {:ok, nil} -> {:error, :not_found} |
| 100 261 | {:ok, value} -> {:ok, value} |
| 101 262 | {:error, _reason} = error -> error |
| @@ -135,6 +296,59 @@ defmodule BaileysEx.Auth.FilePersistence do | |
| 135 296 | end |
| 136 297 | end |
| 137 298 | |
| 299 | + defp validate_additional_data(nil), do: :ok |
| 300 | + defp validate_additional_data(data), do: validate_json_safe(data, [:additional_data]) |
| 301 | + |
| 302 | + defp validate_json_safe(nil, _path), do: :ok |
| 303 | + defp validate_json_safe(value, _path) when is_binary(value), do: :ok |
| 304 | + defp validate_json_safe(value, _path) when is_number(value), do: :ok |
| 305 | + defp validate_json_safe(value, _path) when is_boolean(value), do: :ok |
| 306 | + |
| 307 | + defp validate_json_safe(list, path) when is_list(list) do |
| 308 | + list |
| 309 | + |> Enum.with_index() |
| 310 | + |> Enum.reduce_while(:ok, fn {item, i}, :ok -> |
| 311 | + case validate_json_safe(item, [i | path]) do |
| 312 | + :ok -> {:cont, :ok} |
| 313 | + error -> {:halt, error} |
| 314 | + end |
| 315 | + end) |
| 316 | + end |
| 317 | + |
| 318 | + defp validate_json_safe(map, path) when is_map(map) do |
| 319 | + Enum.reduce_while(map, :ok, fn {key, value}, :ok -> |
| 320 | + with :ok <- validate_json_safe_key(key, path), |
| 321 | + :ok <- validate_json_safe(value, [key | path]) do |
| 322 | + {:cont, :ok} |
| 323 | + else |
| 324 | + error -> {:halt, error} |
| 325 | + end |
| 326 | + end) |
| 327 | + end |
| 328 | + |
| 329 | + defp validate_json_safe(value, path) do |
| 330 | + {:error, |
| 331 | + {:invalid_additional_data, |
| 332 | + "additional_data contains a #{type_name(value)} at path #{inspect(Enum.reverse(path))}; " <> |
| 333 | + "only strings, numbers, booleans, lists, and string-keyed maps are supported"}} |
| 334 | + end |
| 335 | + |
| 336 | + defp validate_json_safe_key(key, _path) when is_binary(key), do: :ok |
| 337 | + |
| 338 | + defp validate_json_safe_key(key, path) do |
| 339 | + {:error, |
| 340 | + {:invalid_additional_data, |
| 341 | + "additional_data contains a #{type_name(key)} map key at path #{inspect(Enum.reverse(path))}; " <> |
| 342 | + "only string keys are supported"}} |
| 343 | + end |
| 344 | + |
| 345 | + defp type_name(value) when is_atom(value), do: "atom (#{inspect(value)})" |
| 346 | + defp type_name(value) when is_tuple(value), do: "tuple" |
| 347 | + defp type_name(value) when is_pid(value), do: "pid" |
| 348 | + defp type_name(value) when is_reference(value), do: "reference" |
| 349 | + defp type_name(value) when is_function(value), do: "function" |
| 350 | + defp type_name(_value), do: "unsupported term" |
| 351 | + |
| 138 352 | defp default_path do |
| 139 353 | Application.get_env(:baileys_ex, __MODULE__, []) |
| 140 354 | |> Keyword.get(:path, Path.join(File.cwd!(), @default_dir)) |
| @@ -158,13 +372,13 @@ defmodule BaileysEx.Auth.FilePersistence do | |
| 158 372 | |> String.replace(":", "-") |
| 159 373 | end |
| 160 374 | |
| 161 | - defp read_data(path, file_name) do |
| 375 | + defp read_data(path, file_name, decoder_fun) do |
| 162 376 | file_path = Path.join(path, sanitize_file_name(file_name)) |
| 163 377 | |
| 164 378 | with_file_lock(file_path, fn -> |
| 165 379 | case File.read(file_path) do |
| 166 380 | {:ok, contents} -> |
| 167 | - {:ok, JSON.decode!(contents) |> decode_term()} |
| 381 | + {:ok, contents |> JSON.decode!() |> decoder_fun.()} |
| 168 382 | |
| 169 383 | {:error, :enoent} -> |
| 170 384 | {:ok, nil} |
| @@ -271,33 +485,65 @@ defmodule BaileysEx.Auth.FilePersistence do | |
| 271 485 | |
| 272 486 | defp encode_term(other), do: other |
| 273 487 | |
| 274 | - defp decode_term(%{"__type__" => "struct", "module" => module_name, "value" => value}) do |
| 275 | - module = String.to_existing_atom(module_name) |
| 276 | - fields = decode_term(value) |
| 488 | + defp decode_credentials(value) do |
| 489 | + case decode_term(value, @credential_decode_context) do |
| 490 | + %State{} = state -> state |
| 491 | + %{} = fields -> struct(State, fields) |
| 492 | + other -> other |
| 493 | + end |
| 494 | + end |
| 495 | + |
| 496 | + defp decode_key_data(:session, value), do: decode_term(value, @session_decode_context) |
| 497 | + defp decode_key_data(:"pre-key", value), do: decode_term(value, @pre_key_decode_context) |
| 498 | + defp decode_key_data(:"sender-key", value), do: decode_term(value, @sender_key_decode_context) |
| 499 | + defp decode_key_data(:"sender-key-memory", value), do: decode_term(value, @empty_decode_context) |
| 500 | + |
| 501 | + defp decode_key_data(:"app-state-sync-key", value), |
| 502 | + do: decode_term(value, @app_state_sync_key_decode_context) |
| 503 | + |
| 504 | + defp decode_key_data(:"app-state-sync-version", value), |
| 505 | + do: decode_term(value, @app_state_sync_version_decode_context) |
| 506 | + |
| 507 | + defp decode_key_data(:"lid-mapping", value), do: decode_term(value, @empty_decode_context) |
| 508 | + defp decode_key_data(:"device-list", value), do: decode_term(value, @empty_decode_context) |
| 509 | + defp decode_key_data(:tctoken, value), do: decode_term(value, @tc_token_decode_context) |
| 510 | + defp decode_key_data(:"identity-key", value), do: decode_term(value, @empty_decode_context) |
| 511 | + defp decode_key_data(_type, value), do: decode_term(value) |
| 512 | + |
| 513 | + defp decode_term(value), do: decode_term(value, @empty_decode_context) |
| 514 | + |
| 515 | + defp decode_term( |
| 516 | + %{"__type__" => "struct", "module" => module_name, "value" => value}, |
| 517 | + decode_context |
| 518 | + ) do |
| 519 | + module = decode_module(module_name, decode_context) |
| 520 | + fields = decode_term(value, decode_context) |
| 277 521 | struct(module, fields) |
| 278 522 | end |
| 279 523 | |
| 280 | - defp decode_term(%{"__type__" => "map", "entries" => entries}) do |
| 524 | + defp decode_term(%{"__type__" => "map", "entries" => entries}, decode_context) do |
| 281 525 | entries |
| 282 | - |> Enum.map(fn [key, value] -> {decode_term(key), decode_term(value)} end) |
| 526 | + |> Enum.map(fn [key, value] -> |
| 527 | + {decode_term(key, decode_context), decode_term(value, decode_context)} |
| 528 | + end) |
| 283 529 | |> Map.new() |
| 284 530 | end |
| 285 531 | |
| 286 | - defp decode_term(%{"__type__" => "tuple", "items" => items}) do |
| 532 | + defp decode_term(%{"__type__" => "tuple", "items" => items}, decode_context) do |
| 287 533 | items |
| 288 | - |> Enum.map(&decode_term/1) |
| 534 | + |> Enum.map(&decode_term(&1, decode_context)) |
| 289 535 | |> List.to_tuple() |
| 290 536 | end |
| 291 537 | |
| 292 | - defp decode_term(%{"__type__" => "atom", "value" => value}) when is_binary(value) do |
| 293 | - String.to_atom(value) |
| 538 | + defp decode_term(%{"__type__" => "atom", "value" => value}, decode_context) |
| 539 | + when is_binary(value) do |
| 540 | + decode_atom(value, decode_context) |
| 294 541 | end |
| 295 542 | |
| 296 | - defp decode_term(%{"type" => @buffer_tag, "data" => data}) when is_list(data) do |
| 297 | - :erlang.list_to_binary(data) |
| 298 | - end |
| 543 | + defp decode_term(%{"type" => @buffer_tag, "data" => data}, _decode_context) when is_list(data), |
| 544 | + do: :erlang.list_to_binary(data) |
| 299 545 | |
| 300 | - defp decode_term(%{} = map) do |
| 546 | + defp decode_term(%{} = map, decode_context) do |
| 301 547 | atom_keys = Map.get(map, "__atom_keys__", []) |
| 302 548 | |
| 303 549 | map |
| @@ -305,18 +551,44 @@ defmodule BaileysEx.Auth.FilePersistence do | |
| 305 551 | |> Enum.map(fn {key, value} -> |
| 306 552 | decoded_key = |
| 307 553 | if key in atom_keys do |
| 308 | - String.to_atom(key) |
| 554 | + decode_atom(key, decode_context) |
| 309 555 | else |
| 310 556 | key |
| 311 557 | end |
| 312 558 | |
| 313 | - {decoded_key, decode_term(value)} |
| 559 | + {decoded_key, decode_term(value, decode_context)} |
| 314 560 | end) |
| 315 561 | |> Map.new() |
| 316 562 | end |
| 317 563 | |
| 318 | - defp decode_term(list) when is_list(list), do: Enum.map(list, &decode_term/1) |
| 319 | - defp decode_term(other), do: other |
| 564 | + defp decode_term(list, decode_context) when is_list(list), |
| 565 | + do: Enum.map(list, &decode_term(&1, decode_context)) |
| 566 | + |
| 567 | + defp decode_term(other, _decode_context), do: other |
| 568 | + |
| 569 | + defp decode_atom(value, %{atoms: atoms}) do |
| 570 | + case Map.fetch(atoms, value) do |
| 571 | + {:ok, atom} -> |
| 572 | + atom |
| 573 | + |
| 574 | + :error -> |
| 575 | + raise ArgumentError, |
| 576 | + "unknown persisted atom #{inspect(value)}; " <> |
| 577 | + "add it to the relevant decode context in #{inspect(__MODULE__)}" |
| 578 | + end |
| 579 | + end |
| 580 | + |
| 581 | + defp decode_module(module_name, %{modules: modules}) do |
| 582 | + case Map.fetch(modules, module_name) do |
| 583 | + {:ok, module} -> |
| 584 | + module |
| 585 | + |
| 586 | + :error -> |
| 587 | + raise ArgumentError, |
| 588 | + "unknown persisted module #{inspect(module_name)}; " <> |
| 589 | + "add it to the relevant decode context in #{inspect(__MODULE__)}" |
| 590 | + end |
| 591 | + end |
| 320 592 | |
| 321 593 | defp normalize_credentials_state(%State{} = state), do: state |
| @@ -17,13 +17,18 @@ defmodule BaileysEx.Auth.Phone do | |
| 17 17 | @pairing_bundle_info "link_code_pairing_key_bundle_encryption_key" |
| 18 18 | @adv_secret_info "adv_secret" |
| 19 19 | |
| 20 | - @spec derive_pairing_code_key(binary(), binary()) :: {:ok, binary()} | {:error, term()} |
| 21 | - def derive_pairing_code_key(pairing_code, salt) |
| 22 | - when is_binary(pairing_code) and is_binary(salt) and byte_size(salt) == 32 do |
| 23 | - Crypto.pbkdf2_sha256(pairing_code, salt, @pairing_iterations, 32) |
| 20 | + @spec derive_pairing_code_key(binary(), binary(), pos_integer()) :: |
| 21 | + {:ok, binary()} | {:error, term()} |
| 22 | + def derive_pairing_code_key(pairing_code, salt, iterations \\ @pairing_iterations) |
| 23 | + |
| 24 | + def derive_pairing_code_key(pairing_code, salt, iterations) |
| 25 | + when is_binary(pairing_code) and is_binary(salt) and byte_size(salt) == 32 and |
| 26 | + is_integer(iterations) and iterations > 0 do |
| 27 | + Crypto.pbkdf2_sha256(pairing_code, salt, iterations, 32) |
| 24 28 | end |
| 25 29 | |
| 26 | - def derive_pairing_code_key(_pairing_code, _salt), do: {:error, :invalid_pairing_key_material} |
| 30 | + def derive_pairing_code_key(_pairing_code, _salt, _iterations), |
| 31 | + do: {:error, :invalid_pairing_key_material} |
| 27 32 | |
| 28 33 | @spec build_pairing_request(binary(), map(), Config.t(), keyword()) :: |
| 29 34 | {:ok, %{pairing_code: binary(), creds_update: map(), node: BinaryNode.t()}} |
| @@ -61,7 +66,7 @@ defmodule BaileysEx.Auth.Phone do | |
| 61 66 | {:ok, signed_identity_key} <- fetch_key_pair(auth_state, :signed_identity_key), |
| 62 67 | {:ok, me} <- fetch_me(auth_state), |
| 63 68 | {:ok, code_pairing_public_key} <- |
| 64 | - decipher_link_public_key(wrapped_primary_ephemeral_public_key, pairing_code), |
| 69 | + decipher_link_public_key(wrapped_primary_ephemeral_public_key, pairing_code, opts), |
| 65 70 | {:ok, companion_shared_key} <- |
| 66 71 | Curve.shared_key(pairing_ephemeral_key.private, code_pairing_public_key), |
| 67 72 | {:ok, finish_payload, adv_secret_key} <- |
| @@ -96,8 +101,9 @@ defmodule BaileysEx.Auth.Phone do | |
| 96 101 | defp generate_pairing_key(pairing_code, companion_ephemeral_public_key, opts) do |
| 97 102 | salt = random_bytes(opts, :pairing_key_salt, 32) |
| 98 103 | iv = random_bytes(opts, :pairing_key_iv, 16) |
| 104 | + iterations = Keyword.get(opts, :pairing_iterations, @pairing_iterations) |
| 99 105 | |
| 100 | - with {:ok, key} <- derive_pairing_code_key(pairing_code, salt), |
| 106 | + with {:ok, key} <- derive_pairing_code_key(pairing_code, salt, iterations), |
| 101 107 | {:ok, encrypted_public_key} <- |
| 102 108 | Crypto.aes_ctr_encrypt(key, iv, companion_ephemeral_public_key) do |
| 103 109 | {:ok, salt <> iv <> encrypted_public_key} |
| @@ -205,14 +211,17 @@ defmodule BaileysEx.Auth.Phone do | |
| 205 211 | |
| 206 212 | defp decipher_link_public_key( |
| 207 213 | <<salt::binary-size(32), iv::binary-size(16), payload::binary-size(32)>>, |
| 208 | - pairing_code |
| 214 | + pairing_code, |
| 215 | + opts |
| 209 216 | ) do |
| 210 | - with {:ok, secret_key} <- derive_pairing_code_key(pairing_code, salt) do |
| 217 | + iterations = Keyword.get(opts, :pairing_iterations, @pairing_iterations) |
| 218 | + |
| 219 | + with {:ok, secret_key} <- derive_pairing_code_key(pairing_code, salt, iterations) do |
| 211 220 | Crypto.aes_ctr_decrypt(secret_key, iv, payload) |
| 212 221 | end |
| 213 222 | end |
| 214 223 | |
| 215 | - defp decipher_link_public_key(_wrapped_public_key, _pairing_code), |
| 224 | + defp decipher_link_public_key(_wrapped_public_key, _pairing_code, _opts), |
| 216 225 | do: {:error, :invalid_wrapped_primary_ephemeral_public_key} |
| 217 226 | |
| 218 227 | defp random_bytes(opts, key, size) do |
| @@ -21,6 +21,14 @@ defmodule BaileysEx.Auth.State do | |
| 21 21 | default_disappearing_mode: map() | nil |
| 22 22 | } |
| 23 23 | |
| 24 | + @type json_safe :: |
| 25 | + nil |
| 26 | + | boolean() |
| 27 | + | number() |
| 28 | + | binary() |
| 29 | + | [json_safe()] |
| 30 | + | %{optional(binary()) => json_safe()} |
| 31 | + |
| 24 32 | @type t :: %__MODULE__{ |
| 25 33 | noise_key: key_pair(), |
| 26 34 | pairing_ephemeral_key: key_pair() | nil, |
| @@ -41,7 +49,7 @@ defmodule BaileysEx.Auth.State do | |
| 41 49 | last_prop_hash: binary() | nil, |
| 42 50 | routing_info: binary() | nil, |
| 43 51 | my_app_state_key_id: binary() | nil, |
| 44 | - additional_data: term() | nil |
| 52 | + additional_data: json_safe() | nil |
| 45 53 | } |
| 46 54 | |
| 47 55 | defstruct [ |
| @@ -130,6 +138,24 @@ defmodule BaileysEx.Auth.State do | |
| 130 138 | |
| 131 139 | def get(_state, _key, default), do: default |
| 132 140 | |
| 141 | + @doc """ |
| 142 | + Returns the current account name from the auth state when present. |
| 143 | + """ |
| 144 | + @spec me_name(t() | map()) :: binary() | nil |
| 145 | + def me_name(state), do: me_field(state, :name) |
| 146 | + |
| 147 | + @doc """ |
| 148 | + Returns the current account JID from the auth state when present. |
| 149 | + """ |
| 150 | + @spec me_id(t() | map()) :: binary() | nil |
| 151 | + def me_id(state), do: me_field(state, :id) |
| 152 | + |
| 153 | + @doc """ |
| 154 | + Returns the current LID from the auth state when present. |
| 155 | + """ |
| 156 | + @spec me_lid(t() | map()) :: binary() | nil |
| 157 | + def me_lid(state), do: me_field(state, :lid) |
| 158 | + |
| 133 159 | @doc """ |
| 134 160 | Returns a `creds` viewing projection mapping suitable for saving standalone. |
| 135 161 | """ |
| @@ -181,6 +207,24 @@ defmodule BaileysEx.Auth.State do | |
| 181 207 | |
| 182 208 | defp nested_creds_get(_state, _key, default), do: default |
| 183 209 | |
| 210 | + defp me_field(%{me: me}, field) when is_map(me), do: me_field(me, field) |
| 211 | + defp me_field(%{"me" => me}, field) when is_map(me), do: me_field(me, field) |
| 212 | + |
| 213 | + defp me_field(map, field) when is_map(map) do |
| 214 | + case Map.fetch(map, field) do |
| 215 | + {:ok, value} when is_binary(value) -> |
| 216 | + value |
| 217 | + |
| 218 | + _ -> |
| 219 | + case Map.get(map, Atom.to_string(field)) do |
| 220 | + value when is_binary(value) -> value |
| 221 | + _ -> nil |
| 222 | + end |
| 223 | + end |
| 224 | + end |
| 225 | + |
| 226 | + defp me_field(_state, _field), do: nil |
| 227 | + |
| 184 228 | defp merge_maps(left, right) when is_map(left) and is_map(right) do |
| 185 229 | Map.merge(left, right, fn _key, left_value, right_value -> |
| 186 230 | if is_map(left_value) and is_map(right_value) do |
Loading more files…