diff --git a/lib/jeff.ex b/lib/jeff.ex index c358cee..3dfb485 100644 --- a/lib/jeff.ex +++ b/lib/jeff.ex @@ -6,7 +6,7 @@ defmodule Jeff do alias Jeff.{ACU, Command, Device, MFG.Encoder, Reply} @type acu() :: GenServer.server() - @type device_opt() :: ACU.device_opt() + @type device_opt() :: Device.opt() @type osdp_address() :: 0x00..0x7F @type vendor_code() :: 0x000000..0xFFFFFF diff --git a/lib/jeff/acu.ex b/lib/jeff/acu.ex index bd0ca1d..a94e5af 100644 --- a/lib/jeff/acu.ex +++ b/lib/jeff/acu.ex @@ -1,6 +1,25 @@ defmodule Jeff.ACU do @moduledoc """ - GenServer process for an ACU + GenServer process for an ACU. + + ### Messages + + If `Jeff.ACU` is started with the `controlling_process` option, the passed pid + will be sent any unsolicited events/replies received from peripheral devices, as + well as messages regarding device status. + + Unsolicited events/replies consist of any of the following structs: + + * `Jeff.Events.CardRead` + * `Jeff.Events.Keypress` + * `Jeff.Reply` + + Device status messages will be sent as tuples: + + * `{:install_mode_complete, %Jeff.Device{}}` - sent when a device has had its + SCBK set and the secure channel will be re-established with the new SCBK + * `{:secure_channel_failed, %Jeff.Device{}}` - sent when a device is removed from + the `Jeff.ACU` due to secure channel establishment failure """ require Logger @@ -20,8 +39,6 @@ defmodule Jeff.ACU do | {:controlling_process, Process.dest()} | {:transport_opts, Transport.opts()} - @type device_opt() :: {:check_scheme, atom()} - @doc """ Start the ACU process. """ @@ -34,11 +51,16 @@ defmodule Jeff.ACU do @doc """ Register a peripheral device on the ACU communication bus. """ - @spec add_device(acu(), osdp_address(), [device_opt()]) :: Device.t() + @spec add_device(acu(), osdp_address(), [Device.opt()]) :: Device.t() def add_device(acu, address, opts \\ []) do GenServer.call(acu, {:add_device, address, opts}) end + @spec get_device(acu(), osdp_address()) :: Device.t() + def get_device(acu, address) do + GenServer.call(acu, {:get_device, address}) + end + @doc """ Remove a peripheral device from the ACU communication bus. """ @@ -141,6 +163,11 @@ defmodule Jeff.ACU do {:reply, device, state} end + def handle_call({:get_device, address}, _from, state) do + device = Bus.get_device(state, address) + {:reply, device, state} + end + def handle_call({:remove_device, address}, _from, state) do device = Bus.get_device(state, address) state = Bus.remove_device(state, address) @@ -183,8 +210,22 @@ defmodule Jeff.ACU do defp handle_reply(state, %{name: CCRYPT} = reply) do device = Bus.current_device(state) - secure_channel = SecureChannel.initialize(device.secure_channel, reply.data) - device = %{device | secure_channel: secure_channel} + + device = + case SecureChannel.initialize(device.secure_channel, reply.data) do + {:ok, sc} -> + %{device | secure_channel: sc} + + :error -> + if device.install_mode? do + # TODO: + maybe_notify(state, {:secure_channel_failed, device}) + device + else + Device.install_mode(device) + end + end + Bus.put_device(state, device) end @@ -192,6 +233,20 @@ defmodule Jeff.ACU do device = Bus.current_device(state) secure_channel = SecureChannel.establish(device.secure_channel, reply.data) device = %{device | secure_channel: secure_channel} + + Bus.put_device(state, device) + end + + defp handle_reply(%{command: %{name: KEYSET}} = state, %{name: ACK}) do + device = Bus.current_device(state) + secure_channel = SecureChannel.new(scbk: device.scbk) + + if device.install_mode? do + maybe_notify(state, {:install_mode_complete, device}) + end + + device = %{device | install_mode?: false, secure_channel: secure_channel} + Bus.put_device(state, device) end @@ -202,6 +257,21 @@ defmodule Jeff.ACU do Bus.put_device(state, device) end + # NAK while establishing secure channel + defp handle_reply( + %{command: %{name: command_name}} = state, + %{name: NAK, data: %Reply.ErrorCode{code: code}} = _reply + ) + when command_name in [CHLNG, SCRYPT] and code in [0x06, 0x09] do + device = Bus.current_device(state) + + if device.install_mode? do + maybe_notify(state, {:secure_channel_failed, device}) + else + state + end + end + defp handle_reply(state, _reply), do: state defp handle_recv( @@ -232,32 +302,31 @@ defmodule Jeff.ACU do reply = Reply.new(reply_message) - if controlling_process do - if reply.name == MFGREP do - send(controlling_process, reply) - end + # Handle solicited and unsolicited replies + cond do + command.caller -> + GenServer.reply(command.caller, reply) - if reply.name == ISTATR do - send(controlling_process, reply) - end + is_nil(controlling_process) -> + :ok - if reply.name == KEYPAD do + reply.name in [ACK, NAK, CCRYPT, RMAC_I] -> + :ok + + reply.name == KEYPAD -> event = Events.Keypress.from_reply(reply) - send(controlling_process, event) - end + maybe_notify(state, event) - if reply.name == RAW do + reply.name == RAW -> event = Events.CardRead.from_reply(reply) - send(controlling_process, event) - end + maybe_notify(state, event) + + true -> + maybe_notify(state, reply) end state = handle_reply(state, reply) - if command.caller do - GenServer.reply(command.caller, reply) - end - %{state | reply: reply} end @@ -278,4 +347,7 @@ defmodule Jeff.ACU do send(self(), :tick) Bus.tick(bus) end + + defp maybe_notify(%{controlling_process: pid}, message) when is_pid(pid), do: send(pid, message) + defp maybe_notify(_, message), do: message end diff --git a/lib/jeff/bus.ex b/lib/jeff/bus.ex index 94ad6ec..2f216ea 100644 --- a/lib/jeff/bus.ex +++ b/lib/jeff/bus.ex @@ -11,17 +11,25 @@ defmodule Jeff.Bus do conn: nil, controlling_process: nil - @type t :: %__MODULE__{} + @type t :: %__MODULE__{ + registry: map(), + command: Jeff.Command.t() | nil, + reply: Jeff.Reply.t() | nil, + cursor: Jeff.osdp_address() | nil, + poll: list(), + conn: pid() | nil, + controlling_process: pid() | nil + } @spec new(keyword()) :: t() - def new(_opts \\ []) do - %__MODULE__{} + def new(opts \\ []) do + struct(__MODULE__, opts) end - @spec add_device(t(), keyword()) :: t() + @spec add_device(t(), [Device.opt()]) :: t() def add_device(bus, opts \\ []) do device = Device.new(opts) - _bus = register(bus, device.address, device) + register(bus, device.address, device) end @spec remove_device(t(), byte()) :: t() @@ -51,16 +59,18 @@ defmodule Jeff.Bus do register(bus, address, device) end - @spec current_device(%__MODULE__{cursor: byte(), registry: map()}) :: Device.t() + @spec current_device(t()) :: Device.t() def current_device(%{cursor: cursor} = bus) do get_device(bus, cursor) end + @spec register(t(), Jeff.osdp_address(), Device.t()) :: t() defp register(%{registry: registry} = bus, address, device) do registry = Map.put(registry, address, device) %{bus | registry: registry} end + @spec register(t(), Device.t()) :: t() defp register(%{cursor: cursor} = bus, device) do _bus = register(bus, cursor, device) end diff --git a/lib/jeff/command.ex b/lib/jeff/command.ex index 25b47af..c7f46d2 100644 --- a/lib/jeff/command.ex +++ b/lib/jeff/command.ex @@ -37,7 +37,7 @@ defmodule Jeff.Command do code: byte(), data: binary(), name: name(), - caller: reference() + caller: reference() | nil } defstruct [:address, :code, :data, :name, :caller] diff --git a/lib/jeff/device.ex b/lib/jeff/device.ex index c73823e..cf5c3b2 100644 --- a/lib/jeff/device.ex +++ b/lib/jeff/device.ex @@ -3,14 +3,26 @@ defmodule Jeff.Device do Peripheral Device configuration and handling """ + require Logger + + alias Jeff.{Command, SecureChannel} + @type check_scheme :: :checksum | :crc @type sequence_number :: 0..3 + @type opt :: + {:address, Jeff.osdp_address()} + | {:check_scheme, check_scheme()} + | {:scbk, SecureChannel.scbk()} + | {:security?, boolean()} + @type t :: %__MODULE__{ address: Jeff.osdp_address(), check_scheme: check_scheme(), security?: boolean(), secure_channel: term(), + install_mode?: boolean(), + scbk: <<_::128>>, sequence: sequence_number(), commands: :queue.queue(term()), last_valid_reply: non_neg_integer() @@ -20,20 +32,20 @@ defmodule Jeff.Device do check_scheme: :checksum, security?: false, secure_channel: nil, + install_mode?: false, + scbk: nil, sequence: 0, commands: :queue.new(), last_valid_reply: nil - alias Jeff.{Command, SecureChannel} - @offline_threshold_ms 8000 - @spec new(keyword()) :: t() + @spec new([opt()]) :: t() def new(params \\ []) do - secure_channel = SecureChannel.new() + secure_channel = SecureChannel.new(scbk: params[:scbk]) __MODULE__ - |> struct(Keyword.take(params, [:address, :check_scheme, :security?])) + |> struct(Keyword.take(params, ~w(address check_scheme scbk security?)a)) |> Map.put(:secure_channel, secure_channel) end @@ -49,7 +61,20 @@ defmodule Jeff.Device do """ @spec reset(t()) :: t() def reset(device) do - %{device | sequence: 0, last_valid_reply: 0, secure_channel: SecureChannel.new()} + %{ + device + | install_mode?: false, + sequence: 0, + last_valid_reply: 0, + secure_channel: SecureChannel.new(scbk: device.scbk) + } + end + + @spec install_mode(t()) :: t() + def install_mode(%__MODULE__{install_mode?: true} = device), do: device + + def install_mode(device) do + %{device | secure_channel: SecureChannel.new(), install_mode?: true} end @spec receive_valid_reply(t()) :: t() @@ -98,6 +123,18 @@ defmodule Jeff.Device do {device, command} end + def next_command( + %{ + security?: true, + install_mode?: true, + secure_channel: %{established?: true, scbkd?: true}, + address: address + } = device + ) do + command = Command.new(address, KEYSET, key: device.scbk) + {device, command} + end + def next_command(%{commands: {[], []}, address: address} = device) do command = Command.new(address, POLL) {device, command} diff --git a/lib/jeff/message.ex b/lib/jeff/message.ex index c4057db..50800d3 100644 --- a/lib/jeff/message.ex +++ b/lib/jeff/message.ex @@ -90,7 +90,7 @@ defmodule Jeff.Message do end @spec scs(Jeff.osdp_address(), byte(), boolean()) :: - 0x11 | 0x12 | 0x13 | 0x14 | 0x17 | 0x18 | nil + 0x11 | 0x12 | 0x13 | 0x14 | 0x15 | 0x17 | 0x18 | nil def scs(address, code, sc_established?) do do_scs(type(address), code, sc_established?) end @@ -99,6 +99,7 @@ defmodule Jeff.Message do defp do_scs(:reply, 0x76, _), do: 0x12 defp do_scs(:command, 0x77, _), do: 0x13 defp do_scs(:reply, 0x78, _), do: 0x14 + defp do_scs(:command, id, true) when id in [0x60, 0x64, 0x65, 0x66, 0x67], do: 0x15 defp do_scs(:command, _, true), do: 0x17 defp do_scs(:reply, _, true), do: 0x18 defp do_scs(:command, _, false), do: nil @@ -144,7 +145,7 @@ defmodule Jeff.Message do defp maybe_add_mac(%{bytes: bytes, device: device} = message) do {secure_channel, mac} = - if add_mac?(message) do + if device.secure_channel.established? && add_mac?(message) do secure_channel = SecureChannel.calculate_mac(device.secure_channel, bytes, true) {secure_channel, secure_channel.cmac |> :binary.part(0, 4)} else diff --git a/lib/jeff/secure_channel.ex b/lib/jeff/secure_channel.ex index 56e5ae7..68eb9ea 100644 --- a/lib/jeff/secure_channel.ex +++ b/lib/jeff/secure_channel.ex @@ -7,6 +7,7 @@ defmodule Jeff.SecureChannel do :enc, :established?, :initialized?, + :failed?, :scbk, :server_cryptogram, :server_rnd, @@ -19,6 +20,9 @@ defmodule Jeff.SecureChannel do @type t :: %__MODULE__{} + @typedoc "Secure Channel Base Key" + @type scbk :: <<_::128>> + @scbk_default Base.decode16!("303132333435363738393A3B3C3D3E3F") @padding_start 0x80 @@ -27,10 +31,11 @@ defmodule Jeff.SecureChannel do server_rnd: binary(), initialized?: false, established?: false, + failed?: false, scbkd?: boolean() } def new(opts \\ []) do - scbk = Keyword.get(opts, :scbk, @scbk_default) + scbk = opts[:scbk] || @scbk_default server_rnd = Keyword.get(opts, :server_rnd, :rand.bytes(8)) %__MODULE__{ @@ -38,11 +43,12 @@ defmodule Jeff.SecureChannel do server_rnd: server_rnd, initialized?: false, established?: false, + failed?: false, scbkd?: scbk == @scbk_default } end - @spec initialize(t(), Jeff.Reply.EncryptionClient.t()) :: t() + @spec initialize(t(), Jeff.Reply.EncryptionClient.t()) :: {:ok, t()} | :error def initialize( %{scbk: scbk, server_rnd: server_rnd} = sc, %{cryptogram: client_cryptogram, cuid: _cuid, rnd: client_rnd} @@ -50,20 +56,23 @@ defmodule Jeff.SecureChannel do enc = gen_enc(server_rnd, scbk) # verify client cryptogram - ^client_cryptogram = gen_client_cryptogram(server_rnd, client_rnd, enc) - - smac1 = gen_smac1(server_rnd, scbk) - smac2 = gen_smac2(server_rnd, scbk) - server_cryptogram = gen_server_cryptogram(client_rnd, server_rnd, enc) - - %{ - sc - | enc: enc, - server_cryptogram: server_cryptogram, - smac1: smac1, - smac2: smac2, - initialized?: true - } + if client_cryptogram == gen_client_cryptogram(server_rnd, client_rnd, enc) do + smac1 = gen_smac1(server_rnd, scbk) + smac2 = gen_smac2(server_rnd, scbk) + server_cryptogram = gen_server_cryptogram(client_rnd, server_rnd, enc) + + {:ok, + %{ + sc + | enc: enc, + server_cryptogram: server_cryptogram, + smac1: smac1, + smac2: smac2, + initialized?: true + }} + else + :error + end end @spec establish(t(), binary()) :: t() diff --git a/test/device_test.exs b/test/device_test.exs index 47bdceb..a21ca15 100644 --- a/test/device_test.exs +++ b/test/device_test.exs @@ -91,6 +91,26 @@ defmodule DeviceTest do assert next_command.name == SCRYPT end + test "next command is KEYSET if security is established in install mode" do + device = Device.new(scbk: :rand.bytes(16)) |> Device.inc_sequence() + assert device.sequence == 1 + + device = %{device | security?: true, install_mode?: true} + + device = %{ + device + | secure_channel: %{ + device.secure_channel + | initialized?: true, + established?: true, + scbkd?: true + } + } + + {_device, next_command} = Device.next_command(device) + assert next_command.name == KEYSET + end + test "next command is POLL if command queue is empty" do poll_command = Command.new(0x01, POLL) device = Device.new(address: 0x01) |> Device.inc_sequence() diff --git a/test/message_test.exs b/test/message_test.exs index 5d17da1..261675a 100644 --- a/test/message_test.exs +++ b/test/message_test.exs @@ -71,8 +71,15 @@ defmodule MessageTest do # RMAC-I reply assert Message.scs(0x1 + 0x80, 0x78, false) == 0x14 + # Commands with no data payload + assert Message.scs(0x1, 0x60, true) == 0x15 + assert Message.scs(0x1, 0x64, true) == 0x15 + assert Message.scs(0x1, 0x65, true) == 0x15 + assert Message.scs(0x1, 0x66, true) == 0x15 + assert Message.scs(0x1, 0x67, true) == 0x15 + # Any other command - established secure channel - assert Message.scs(0x1, 0x60, true) == 0x17 + assert Message.scs(0x1, 0x61, true) == 0x17 # Any other reply - established secure channel assert Message.scs(0x1 + 0x80, 0x40, true) == 0x18 diff --git a/test/secure_channel_test.exs b/test/secure_channel_test.exs index 3a608de..2f685e5 100644 --- a/test/secure_channel_test.exs +++ b/test/secure_channel_test.exs @@ -48,7 +48,7 @@ defmodule SecureChannelTest do rnd: client_rnd } - sc = + {:ok, sc} = SC.new(scbk: scbk_d_key, server_rnd: server_rnd) |> SC.initialize(encryption_client)