fix heartbeat

This commit is contained in:
anlicheng 2026-05-09 18:27:18 +08:00
parent e46dc8d2bb
commit 7e49d2df0c
4 changed files with 24 additions and 8 deletions

View File

@ -41,4 +41,4 @@ Payload = <<Version:8, UuidLen:16, UUID:UuidLen/binary, Timestamp:64/unsigned-bi
Mac = crypto:mac(hmac, sha256, HeartbeatSecret, Payload) Mac = crypto:mac(hmac, sha256, HeartbeatSecret, Payload)
``` ```
服务端在 `efka_client_store:verify_heartbeat/4` 中校验时间窗和 HMAC。当前服务端使用 `efka_client_store` 中保存的 `token_hash` 作为 `HeartbeatSecret`,避免在 iot 侧保存明文 auth token。 服务端在 `efka_client_store:verify_heartbeat/4` 中校验时间窗和 HMAC。`HeartbeatSecret` 使用 `SHA256(Token)`,其中 `Token` 和 TLS 鉴权使用同一个 auth token。`iot` 侧只保存派生后的 `heartbeat_secret`,不保存明文 token。

View File

@ -12,5 +12,6 @@
uuid :: binary(), uuid :: binary(),
token_hash :: binary(), token_hash :: binary(),
salt :: binary(), salt :: binary(),
heartbeat_secret :: binary(),
timestamp = 0 :: integer() timestamp = 0 :: integer()
}). }).

View File

@ -45,8 +45,8 @@ verify_heartbeat(UUID, Timestamp, Payload, Mac)
case mnesia:dirty_read(efka_client, UUID) of case mnesia:dirty_read(efka_client, UUID) of
[] -> [] ->
false; false;
[#efka_client{token_hash = TokenHash}] -> [#efka_client{heartbeat_secret = HeartbeatSecret}] ->
ExpectedMac = crypto:mac(hmac, sha256, TokenHash, Payload), ExpectedMac = crypto:mac(hmac, sha256, HeartbeatSecret, Payload),
ExpectedMac =:= Mac ExpectedMac =:= Mac
end; end;
false -> false ->
@ -61,7 +61,14 @@ register(UUID, Token) when is_binary(UUID), UUID =/= <<>>, is_binary(Token), byt
%% 1. 16 %% 1. 16
Salt = crypto:strong_rand_bytes(16), Salt = crypto:strong_rand_bytes(16),
TokenHash = hash_token(Token, Salt), TokenHash = hash_token(Token, Salt),
ok = mnesia:write(#efka_client{uuid = UUID, token_hash = TokenHash, salt = Salt, timestamp = iot_util:timestamp()}), HeartbeatSecret = heartbeat_secret(Token),
ok = mnesia:write(#efka_client{
uuid = UUID,
token_hash = TokenHash,
salt = Salt,
heartbeat_secret = HeartbeatSecret,
timestamp = iot_util:timestamp()
}),
ok; ok;
[_] -> [_] ->
{error, already_exists} {error, already_exists}
@ -137,6 +144,10 @@ list() ->
hash_token(Token, Salt) when is_binary(Token), is_binary(Salt) -> hash_token(Token, Salt) when is_binary(Token), is_binary(Salt) ->
crypto:pbkdf2_hmac(sha256, Token, Salt, ?TOKEN_HASH_ITERATIONS, ?TOKEN_HASH_BYTES). crypto:pbkdf2_hmac(sha256, Token, Salt, ?TOKEN_HASH_ITERATIONS, ?TOKEN_HASH_BYTES).
-spec heartbeat_secret(binary()) -> binary().
heartbeat_secret(Token) when is_binary(Token) ->
crypto:hash(sha256, Token).
-spec valid_heartbeat_timestamp(integer()) -> boolean(). -spec valid_heartbeat_timestamp(integer()) -> boolean().
valid_heartbeat_timestamp(Timestamp) -> valid_heartbeat_timestamp(Timestamp) ->
Now = iot_util:current_time(), Now = iot_util:current_time(),

View File

@ -82,6 +82,9 @@ handle_cast(_Request, State = #state{}) ->
{stop, Reason :: term(), NewState :: #state{}}). {stop, Reason :: term(), NewState :: #state{}}).
handle_info({udp, Socket, Ip, Port, Packet}, State = #state{socket = Socket}) -> handle_info({udp, Socket, Ip, Port, Packet}, State = #state{socket = Socket}) ->
handle_heartbeat_packet(Packet, {Ip, Port}), handle_heartbeat_packet(Packet, {Ip, Port}),
{noreply, State};
handle_info(Info, State = #state{}) ->
logger:warning("[udp_server] ignore unknown info: ~p", [Info]),
{noreply, State}. {noreply, State}.
%% @private %% @private
@ -91,7 +94,8 @@ handle_info({udp, Socket, Ip, Port, Packet}, State = #state{socket = Socket}) ->
%% with Reason. The return value is ignored. %% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), -spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()). State :: #state{}) -> term()).
terminate(_Reason, _State = #state{}) -> terminate(_Reason, _State = #state{socket = Socket}) ->
gen_udp:close(Socket),
ok. ok.
%% @private %% @private