| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- %%--------------------------------------------------------------------
- %% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
- %%
- %% Licensed under the Apache License, Version 2.0 (the "License");
- %% you may not use this file except in compliance with the License.
- %% You may obtain a copy of the License at
- %%
- %% http://www.apache.org/licenses/LICENSE-2.0
- %%
- %% Unless required by applicable law or agreed to in writing, software
- %% distributed under the License is distributed on an "AS IS" BASIS,
- %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- %% See the License for the specific language governing permissions and
- %% limitations under the License.
- %%--------------------------------------------------------------------
- -module(emqx_utils_redact).
- -export([redact/1, redact/2, redact_headers/1, is_redacted/2, is_redacted/3]).
- -export([deobfuscate/2]).
- -define(REDACT_VAL, "******").
- -define(IS_KEY_HEADERS(K), K == headers; K == <<"headers">>; K == "headers").
- %% NOTE: keep alphabetical order
- is_sensitive_key(aws_secret_access_key) -> true;
- is_sensitive_key("aws_secret_access_key") -> true;
- is_sensitive_key(<<"aws_secret_access_key">>) -> true;
- is_sensitive_key(password) -> true;
- is_sensitive_key("password") -> true;
- is_sensitive_key(<<"password">>) -> true;
- is_sensitive_key(secret) -> true;
- is_sensitive_key("secret") -> true;
- is_sensitive_key(<<"secret">>) -> true;
- is_sensitive_key(secret_access_key) -> true;
- is_sensitive_key("secret_access_key") -> true;
- is_sensitive_key(<<"secret_access_key">>) -> true;
- is_sensitive_key(secret_key) -> true;
- is_sensitive_key("secret_key") -> true;
- is_sensitive_key(<<"secret_key">>) -> true;
- is_sensitive_key(security_token) -> true;
- is_sensitive_key("security_token") -> true;
- is_sensitive_key(<<"security_token">>) -> true;
- is_sensitive_key(sp_private_key) -> true;
- is_sensitive_key(<<"sp_private_key">>) -> true;
- is_sensitive_key(token) -> true;
- is_sensitive_key("token") -> true;
- is_sensitive_key(<<"token">>) -> true;
- is_sensitive_key(jwt) -> true;
- is_sensitive_key("jwt") -> true;
- is_sensitive_key(<<"jwt">>) -> true;
- is_sensitive_key(bind_password) -> true;
- is_sensitive_key("bind_password") -> true;
- is_sensitive_key(<<"bind_password">>) -> true;
- is_sensitive_key(_) -> false.
- redact(Term) ->
- do_redact(Term, fun is_sensitive_key/1).
- redact(Term, Checker) ->
- do_redact(Term, fun(V) ->
- is_sensitive_key(V) orelse Checker(V)
- end).
- redact_headers(Term) ->
- do_redact_headers(Term).
- do_redact([], _Checker) ->
- [];
- do_redact([X | Xs], Checker) ->
- %% Note: we could be dealing with an improper list
- [do_redact(X, Checker) | do_redact(Xs, Checker)];
- do_redact(M, Checker) when is_map(M) ->
- maps:map(
- fun(K, V) ->
- do_redact(K, V, Checker)
- end,
- M
- );
- do_redact({Headers, Value}, _Checker) when ?IS_KEY_HEADERS(Headers) ->
- {Headers, do_redact_headers(Value)};
- do_redact({Key, Value}, Checker) ->
- case Checker(Key) of
- true ->
- {Key, redact_v(Value)};
- false ->
- {do_redact(Key, Checker), do_redact(Value, Checker)}
- end;
- do_redact(T, Checker) when is_tuple(T) ->
- Elements = erlang:tuple_to_list(T),
- Redact = do_redact(Elements, Checker),
- erlang:list_to_tuple(Redact);
- do_redact(Any, _Checker) ->
- Any.
- do_redact(Headers, V, _Checker) when ?IS_KEY_HEADERS(Headers) ->
- do_redact_headers(V);
- do_redact(K, V, Checker) ->
- case Checker(K) of
- true ->
- redact_v(V);
- false ->
- do_redact(V, Checker)
- end.
- do_redact_headers(List) when is_list(List) ->
- lists:map(
- fun
- ({K, V} = Pair) ->
- case check_is_sensitive_header(K) of
- true ->
- {K, redact_v(V)};
- _ ->
- Pair
- end;
- (Any) ->
- Any
- end,
- List
- );
- do_redact_headers(Map) when is_map(Map) ->
- maps:map(
- fun(K, V) ->
- case check_is_sensitive_header(K) of
- true ->
- redact_v(V);
- _ ->
- V
- end
- end,
- Map
- );
- do_redact_headers(Value) ->
- Value.
- check_is_sensitive_header(Key) ->
- Key1 = string:trim(emqx_utils_conv:str(Key)),
- is_sensitive_header(string:lowercase(Key1)).
- is_sensitive_header("authorization") ->
- true;
- is_sensitive_header("proxy-authorization") ->
- true;
- is_sensitive_header(_Any) ->
- false.
- redact_v(V) when is_binary(V) -> <<?REDACT_VAL>>;
- %% The HOCON schema system may generate sensitive values with this format
- redact_v([{str, Bin}]) when is_binary(Bin) ->
- [{str, <<?REDACT_VAL>>}];
- redact_v(_V) ->
- ?REDACT_VAL.
- deobfuscate(NewConf, OldConf) ->
- maps:fold(
- fun(K, V, Acc) ->
- case maps:find(K, OldConf) of
- error ->
- case is_redacted(K, V) of
- %% don't put redacted value into new config
- true -> Acc;
- false -> Acc#{K => V}
- end;
- {ok, OldV} when is_map(V), is_map(OldV) ->
- Acc#{K => deobfuscate(V, OldV)};
- {ok, OldV} ->
- case is_redacted(K, V) of
- true ->
- Acc#{K => OldV};
- _ ->
- Acc#{K => V}
- end
- end
- end,
- #{},
- NewConf
- ).
- is_redacted(K, V) ->
- do_is_redacted(K, V, fun is_sensitive_key/1).
- is_redacted(K, V, Fun) ->
- do_is_redacted(K, V, fun(E) ->
- is_sensitive_key(E) orelse Fun(E)
- end).
- do_is_redacted(K, ?REDACT_VAL, Fun) ->
- Fun(K);
- do_is_redacted(K, <<?REDACT_VAL>>, Fun) ->
- Fun(K);
- do_is_redacted(K, WrappedFun, Fun) when is_function(WrappedFun, 0) ->
- %% wrapped by `emqx_secret' or other module
- do_is_redacted(K, WrappedFun(), Fun);
- do_is_redacted(_K, _V, _Fun) ->
- false.
- -ifdef(TEST).
- -include_lib("eunit/include/eunit.hrl").
- redact_test_() ->
- Case = fun(Type, KeyT) ->
- Key =
- case Type of
- atom -> KeyT;
- string -> erlang:atom_to_list(KeyT);
- binary -> erlang:atom_to_binary(KeyT)
- end,
- ?assert(is_sensitive_key(Key)),
- %% direct
- ?assertEqual({Key, ?REDACT_VAL}, redact({Key, foo})),
- ?assertEqual(#{Key => ?REDACT_VAL}, redact(#{Key => foo})),
- ?assertEqual({Key, Key, Key}, redact({Key, Key, Key})),
- ?assertEqual({[{Key, ?REDACT_VAL}], bar}, redact({[{Key, foo}], bar})),
- %% 1 level nested
- ?assertEqual([{Key, ?REDACT_VAL}], redact([{Key, foo}])),
- ?assertEqual([#{Key => ?REDACT_VAL}], redact([#{Key => foo}])),
- %% 2 level nested
- ?assertEqual(#{opts => [{Key, ?REDACT_VAL}]}, redact(#{opts => [{Key, foo}]})),
- ?assertEqual(#{opts => #{Key => ?REDACT_VAL}}, redact(#{opts => #{Key => foo}})),
- ?assertEqual({opts, [{Key, ?REDACT_VAL}]}, redact({opts, [{Key, foo}]})),
- %% 3 level nested
- ?assertEqual([#{opts => [{Key, ?REDACT_VAL}]}], redact([#{opts => [{Key, foo}]}])),
- ?assertEqual([{opts, [{Key, ?REDACT_VAL}]}], redact([{opts, [{Key, foo}]}])),
- ?assertEqual([{opts, [#{Key => ?REDACT_VAL}]}], redact([{opts, [#{Key => foo}]}]))
- end,
- Types = [atom, string, binary],
- Keys = [
- aws_secret_access_key,
- password,
- secret,
- secret_key,
- secret_access_key,
- security_token,
- token,
- bind_password
- ],
- [{case_name(Type, Key), fun() -> Case(Type, Key) end} || Key <- Keys, Type <- Types].
- redact2_test_() ->
- Case = fun(Key, Checker) ->
- ?assertEqual({Key, ?REDACT_VAL}, redact({Key, foo}, Checker)),
- ?assertEqual(#{Key => ?REDACT_VAL}, redact(#{Key => foo}, Checker)),
- ?assertEqual({Key, Key, Key}, redact({Key, Key, Key}, Checker)),
- ?assertEqual({[{Key, ?REDACT_VAL}], bar}, redact({[{Key, foo}], bar}, Checker))
- end,
- Checker = fun(E) -> E =:= passcode end,
- Keys = [secret, passcode],
- [{case_name(atom, Key), fun() -> Case(Key, Checker) end} || Key <- Keys].
- redact_improper_list_test_() ->
- %% improper lists: check that we don't crash
- %% may arise when we redact process states with pending `gen' requests
- [
- ?_assertEqual([alias | foo], redact([alias | foo])),
- ?_assertEqual([1, 2 | foo], redact([1, 2 | foo]))
- ].
- deobfuscate_test() ->
- NewConf0 = #{foo => <<"bar0">>, password => <<"123456">>},
- ?assertEqual(NewConf0, deobfuscate(NewConf0, #{foo => <<"bar">>, password => <<"654321">>})),
- NewConf1 = #{foo => <<"bar1">>, password => <<?REDACT_VAL>>},
- ?assertEqual(
- #{foo => <<"bar1">>, password => <<"654321">>},
- deobfuscate(NewConf1, #{foo => <<"bar">>, password => <<"654321">>})
- ),
- %% Don't have password before and ignore to put redact_val into new config
- NewConf2 = #{foo => <<"bar2">>, password => ?REDACT_VAL},
- ?assertEqual(#{foo => <<"bar2">>}, deobfuscate(NewConf2, #{foo => <<"bar">>})),
- %% Don't have password before and should allow put non-redact-val into new config
- NewConf3 = #{foo => <<"bar3">>, password => <<"123456">>},
- ?assertEqual(NewConf3, deobfuscate(NewConf3, #{foo => <<"bar">>})),
- ok.
- redact_header_test_() ->
- Types = [string, binary, atom],
- Keys = [
- "auThorization",
- "Authorization",
- "authorizaTion",
- "proxy-authorizaTion",
- "proXy-authoriZaTion"
- ],
- Case = fun(Type, Key0) ->
- Converter =
- case Type of
- binary ->
- fun erlang:list_to_binary/1;
- atom ->
- fun erlang:list_to_atom/1;
- _ ->
- fun(Any) -> Any end
- end,
- Name = Converter("headers"),
- Key = Converter(Key0),
- Value = Converter("value"),
- Value1 = redact_v(Value),
- ?assertMatch(
- {Name, [{Key, Value1}]},
- redact({Name, [{Key, Value}]})
- ),
- ?assertMatch(
- #{Name := #{Key := Value1}},
- redact(#{Name => #{Key => Value}})
- )
- end,
- [{case_name(Type, Key), fun() -> Case(Type, Key) end} || Key <- Keys, Type <- Types].
- case_name(Type, Key) ->
- lists:concat([Type, "-", Key]).
- -endif.
|