From 94744bcf6fb15e87366ebc94576835163d55a947 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 16 Aug 2023 09:53:38 -0700 Subject: [PATCH] Search bigger space for packets that belong to devices (#996) 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. --- src/device/router_device_routing.erl | 31 ++++++++- src/device/router_device_worker.erl | 48 +++++++++++++- test/router_device_routing_SUITE.erl | 3 +- test/router_device_worker_SUITE.erl | 99 ++++++++++++++++++++++++++++ 4 files changed, 175 insertions(+), 6 deletions(-) diff --git a/src/device/router_device_routing.erl b/src/device/router_device_routing.erl index 2267354d2..92eee2e15 100644 --- a/src/device/router_device_routing.erl +++ b/src/device/router_device_routing.erl @@ -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). @@ -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( + <>, + 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), diff --git a/src/device/router_device_worker.erl b/src/device/router_device_worker.erl index 62891361f..235d63a42 100644 --- a/src/device/router_device_worker.erl +++ b/src/device/router_device_worker.erl @@ -1530,9 +1530,19 @@ validate_frame( FrameCache, OfferCache ) -> - <> = blockchain_helium_packet_v1:payload(Packet), + Payload = + <> = 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), @@ -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_( @@ -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( + <>, + 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"). diff --git a/test/router_device_routing_SUITE.erl b/test/router_device_routing_SUITE.erl index 6154bf83b..672c8e48f 100644 --- a/test/router_device_routing_SUITE.erl +++ b/test/router_device_routing_SUITE.erl @@ -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() ) diff --git a/test/router_device_worker_SUITE.erl b/test/router_device_worker_SUITE.erl index 94ff03b22..c062db709 100644 --- a/test/router_device_worker_SUITE.erl +++ b/test/router_device_worker_SUITE.erl @@ -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, @@ -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, @@ -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">> => + <>, + <<"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) ->