Skip to content

Secure channel / install mode #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/jeff.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
118 changes: 95 additions & 23 deletions lib/jeff/acu.ex
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
"""
Expand All @@ -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.
"""
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -183,15 +210,43 @@ 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

defp handle_reply(state, %{name: RMAC_I} = reply) 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

Expand All @@ -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(
Expand Down Expand Up @@ -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

Expand All @@ -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
22 changes: 16 additions & 6 deletions lib/jeff/bus.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/jeff/command.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
49 changes: 43 additions & 6 deletions lib/jeff/device.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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

Expand All @@ -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()
Expand Down Expand Up @@ -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
Comment on lines +126 to +136
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where I got hung up on this PR was trying to decide how much install mode Jeff should be responsible for. This is the basic logic implemented in this PR in pseudocode:

if Jeff.add_device was called with a non-nil SCBK:
  Try to establish a secure channel with the given SCBK
  If secure channel is successful:
    Done
# Else if install mode is not allowed:
#   I was planning to implement this case, but I removed it
#   because of the same issue in the on failure case below
#   (not having a way to signal that a device is not working
#   properly)
  Else:
    Try to connect with the install mode SCBK
    On success:
       Issue a KEYSET command to set the SCBK on the device
       Restart the secure channel with the new SCBK
    On failure:
       TODO -- what _should_ happen here? We don't currently
       have a way to indicate that a device isn't working
       as expected


def next_command(%{commands: {[], []}, address: address} = device) do
command = Command.new(address, POLL)
{device, command}
Expand Down
5 changes: 3 additions & 2 deletions lib/jeff/message.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was the most critical change to get things to work -- commands with no data payload need to use SCS 0x15.

It would have been nice to calculate this based on the actual payload length, but that was a bigger refactor than I wanted to do, and there are a fixed number of commands with no payload.

defp do_scs(:command, _, true), do: 0x17
defp do_scs(:reply, _, true), do: 0x18
defp do_scs(:command, _, false), do: nil
Expand Down Expand Up @@ -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
Expand Down
Loading