Skip to content

Commit

Permalink
Search bigger space for packets that belong to devices (#996)
Browse files Browse the repository at this point in the history
When looking for a device that a packet belongs to, verify the mic with
the frame count in the packet rather than the expected frame count from
the device.
  • Loading branch information
michaeldjeffrey authored Aug 16, 2023
1 parent 92bf4dd commit 94744bc
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 6 deletions.
31 changes: 29 additions & 2 deletions src/device/router_device_routing.erl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@
force_evict_packet_hash/1
]).

-export([
b0_from_payload/2,
payload_mic/1,
payload_fcnt_low/1
]).

%% biggest unsigned number in 23 bits
-define(BITS_23, 8388607).
-define(MODULO_16_BITS, 16#10000).
Expand Down Expand Up @@ -1262,10 +1268,31 @@ find_right_key(B0, MIC, Payload, Device, [{undefined, _} | Keys]) ->
find_right_key(B0, MIC, Payload, Device, Keys);
find_right_key(B0, MIC, Payload, Device, [{NwkSKey, _} | Keys]) ->
case key_matches_mic(NwkSKey, B0, MIC) of
true -> {Device, NwkSKey};
false -> find_right_key(B0, MIC, Payload, Device, Keys)
true ->
{Device, NwkSKey};
false ->
case key_matches_any_fcnt(NwkSKey, MIC, Payload) of
false -> find_right_key(B0, MIC, Payload, Device, Keys);
true -> {Device, NwkSKey}
end
end.

-spec key_matches_any_fcnt(binary(), binary(), binary()) -> boolean().
key_matches_any_fcnt(NwkSKey, ExpectedMIC, Payload) ->
FCntLow = payload_fcnt_low(Payload),
lists:any(
fun(HighBits) ->
FCnt = binary:decode_unsigned(
<<FCntLow:16/integer-unsigned-little, HighBits:16/integer-unsigned-little>>,
little
),
B0 = b0_from_payload(Payload, FCnt),
ComputedMIC = crypto:macN(cmac, aes_128_cbc, NwkSKey, B0, 4),
ComputedMIC =:= ExpectedMIC
end,
lists:seq(2#000, 2#111)
).

-spec key_matches_mic(binary(), binary(), binary()) -> boolean().
key_matches_mic(Key, B0, ExpectedMIC) ->
ComputedMIC = crypto:macN(cmac, aes_128_cbc, Key, B0, 4),
Expand Down
48 changes: 45 additions & 3 deletions src/device/router_device_worker.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1530,9 +1530,19 @@ validate_frame(
FrameCache,
OfferCache
) ->
<<MType:3, _MHDRRFU:3, _Major:2, _DevAddr:4/binary, _ADR:1, _ADRACKReq:1, _ACK:1, _RFU:1,
_FOptsLen:4, _FCnt:16, _FOpts:_FOptsLen/binary,
_PayloadAndMIC/binary>> = blockchain_helium_packet_v1:payload(Packet),
Payload =
<<MType:3, _MHDRRFU:3, _Major:2, _DevAddr:4/binary, _ADR:1, _ADRACKReq:1, _ACK:1, _RFU:1,
_FOptsLen:4, _FCnt:16, _FOpts:_FOptsLen/binary,
_PayloadAndMIC/binary>> = blockchain_helium_packet_v1:payload(Packet),

VerifiedFCnt = verified_fcnt_from_payload(
router_device_routing:payload_fcnt_low(Payload),
router_device:nwk_s_key(Device0),
router_device_routing:payload_mic(Payload),
Payload
),
DeviceFCnt = router_device:fcnt(Device0),

case MType of
MType when MType == ?CONFIRMED_UP orelse MType == ?UNCONFIRMED_UP ->
FrameAck = router_utils:mtype_to_ack(MType),
Expand Down Expand Up @@ -1591,6 +1601,12 @@ validate_frame(
OfferCache,
true
);
undefined when VerifiedFCnt < DeviceFCnt ->
lager:info(
"we got a replay packet [verified: ~p] [device: ~p]",
[VerifiedFCnt, DeviceFCnt]
),
{error, late_packet};
undefined ->
lager:debug("we got a fresh packet [fcnt: ~p]", [PacketFCnt]),
validate_frame_(
Expand Down Expand Up @@ -2400,6 +2416,32 @@ maybe_will_downlink(Device, #frame{mtype = MType, adrackreq = ADRAckReqBit}) ->
ADR = ADRAllowed andalso ADRAckReqBit == 1,
DeviceQueue =/= [] orelse ACK == 1 orelse ADR orelse ChannelCorrection == false.

-spec verified_fcnt_from_payload(non_neg_integer(), binary(), binary(), binary()) ->
non_neg_integer().
verified_fcnt_from_payload(FCntLow, NwkSKey, ExpectedMIC, Payload) ->
find_first(
fun(HighBits) ->
FCnt = binary:decode_unsigned(
<<FCntLow:16/integer-unsigned-little, HighBits:16/integer-unsigned-little>>,
little
),
B0 = router_device_routing:b0_from_payload(Payload, FCnt),
ComputedMIC = crypto:macN(cmac, aes_128_cbc, NwkSKey, B0, 4),
{ComputedMIC =:= ExpectedMIC, FCnt}
end,
lists:seq(2#000, 2#111)
).

-spec find_first(
FN :: fun((HighBit :: non_neg_integer()) -> {Verified :: boolean(), FCnt :: non_neg_integer()}),
HighBits :: list(non_neg_integer())
) -> non_neg_integer().
find_first(Fn, [FCntHigh | Rest]) ->
case Fn(FCntHigh) of
{true, Found} -> Found;
_ -> find_first(Fn, Rest)
end.

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

Expand Down
3 changes: 2 additions & 1 deletion test/router_device_routing_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -846,8 +846,9 @@ handle_packet_wrong_fcnt_test(Config) ->
devaddr => router_device:devaddr(Device0)
}),

%% NOTE: packets with previous fcnts are now correctly identified to the device that can decode them.
?assertEqual(
{error, unknown_device},
ok,
router_device_routing:handle_packet(
SCPacket1, erlang:system_time(millisecond), self()
)
Expand Down
99 changes: 99 additions & 0 deletions test/router_device_worker_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
replay_joins_test/1,
ddos_joins_test/1,
replay_uplink_test/1,
replay_uplink_far_in_the_past_test/1,
device_worker_stop_children_test/1,
device_worker_late_packet_double_charge_test/1,
offer_cache_test/1,
Expand Down Expand Up @@ -73,6 +74,7 @@ all_tests() ->
replay_joins_test,
ddos_joins_test,
replay_uplink_test,
replay_uplink_far_in_the_past_test,
device_worker_late_packet_double_charge_test,
offer_cache_test,
load_offer_cache_test,
Expand Down Expand Up @@ -1323,6 +1325,103 @@ replay_uplink_test(Config) ->
}
}),

ok.

replay_uplink_far_in_the_past_test(Config) ->
#{
pubkey_bin := PubKeyBin,
stream := Stream,
hotspot_name := HotspotName
} = test_utils:join_device(Config),

%% Check that device is in cache now
{ok, DB, CF} = router_db:get_devices(),
WorkerID = router_devices_sup:id(?CONSOLE_DEVICE_ID),
{ok, Device0} = router_device:get_by_id(DB, CF, WorkerID),

Stream !
{send,
test_utils:frame_packet(
?UNCONFIRMED_UP,
PubKeyBin,
router_device:nwk_s_key(Device0),
router_device:app_s_key(Device0),
10_000
)},

test_utils:wait_channel_data(#{
<<"type">> => <<"uplink">>,
<<"replay">> => false,
<<"uuid">> => fun erlang:is_binary/1,
<<"id">> => ?CONSOLE_DEVICE_ID,
<<"downlink_url">> =>
<<?CONSOLE_URL/binary, "/api/v1/down/", ?CONSOLE_HTTP_CHANNEL_ID/binary, "/",
?CONSOLE_HTTP_CHANNEL_DOWNLINK_TOKEN/binary, "/", ?CONSOLE_DEVICE_ID/binary>>,
<<"name">> => ?CONSOLE_DEVICE_NAME,
<<"dev_eui">> => lorawan_utils:binary_to_hex(?DEVEUI),
<<"app_eui">> => lorawan_utils:binary_to_hex(?APPEUI),
<<"metadata">> => #{
<<"labels">> => ?CONSOLE_LABELS,
<<"organization_id">> => ?CONSOLE_ORG_ID,
<<"multi_buy">> => fun erlang:is_integer/1,
<<"adr_allowed">> => false,
<<"cf_list_enabled">> => false,
<<"rx_delay_state">> => fun erlang:is_binary/1,
<<"rx_delay">> => 0,
<<"preferred_hotspots">> => fun erlang:is_list/1
},
<<"fcnt">> => 10_000,
<<"reported_at">> => fun erlang:is_integer/1,
<<"payload">> => <<>>,
<<"payload_size">> => 0,
<<"raw_packet">> => fun erlang:is_binary/1,
<<"port">> => 1,
<<"devaddr">> => '_',
<<"hotspots">> => [
#{
<<"id">> => erlang:list_to_binary(libp2p_crypto:bin_to_b58(PubKeyBin)),
<<"name">> => erlang:list_to_binary(HotspotName),
<<"reported_at">> => fun erlang:is_integer/1,
<<"hold_time">> => fun erlang:is_integer/1,
<<"status">> => <<"success">>,
<<"rssi">> => 0.0,
<<"snr">> => 0.0,
<<"spreading">> => <<"SF8BW125">>,
<<"frequency">> => fun erlang:is_float/1,
<<"channel">> => fun erlang:is_number/1,
<<"lat">> => fun erlang:is_float/1,
<<"long">> => fun erlang:is_float/1
}
],
<<"dc">> => #{
<<"balance">> => fun erlang:is_integer/1,
<<"nonce">> => fun erlang:is_integer/1
}
}),

timer:sleep(2000 + 100),

Stream !
{send,
test_utils:frame_packet(
?UNCONFIRMED_UP,
PubKeyBin,
router_device:nwk_s_key(Device0),
router_device:app_s_key(Device0),
5000
)},

test_utils:wait_for_console_event_sub(<<"uplink_dropped_late">>, #{
<<"category">> => <<"uplink_dropped">>,
<<"data">> => fun erlang:is_map/1,
<<"description">> => <<"Late packet">>,
<<"device_id">> => <<"yolo_id">>,
<<"id">> => fun erlang:is_binary/1,
<<"reported_at">> => fun erlang:is_integer/1,
<<"sub_category">> => <<"uplink_dropped_late">>
}),


ok.

offer_cache_test(Config) ->
Expand Down

0 comments on commit 94744bc

Please sign in to comment.