emqx_utils_redact.erl 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
  3. %%
  4. %% Licensed under the Apache License, Version 2.0 (the "License");
  5. %% you may not use this file except in compliance with the License.
  6. %% You may obtain a copy of the License at
  7. %%
  8. %% http://www.apache.org/licenses/LICENSE-2.0
  9. %%
  10. %% Unless required by applicable law or agreed to in writing, software
  11. %% distributed under the License is distributed on an "AS IS" BASIS,
  12. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. %% See the License for the specific language governing permissions and
  14. %% limitations under the License.
  15. %%--------------------------------------------------------------------
  16. -module(emqx_utils_redact).
  17. -export([redact/1, redact/2, redact_headers/1, is_redacted/2, is_redacted/3]).
  18. -export([deobfuscate/2]).
  19. -define(REDACT_VAL, "******").
  20. -define(IS_KEY_HEADERS(K), K == headers; K == <<"headers">>; K == "headers").
  21. %% NOTE: keep alphabetical order
  22. is_sensitive_key(aws_secret_access_key) -> true;
  23. is_sensitive_key("aws_secret_access_key") -> true;
  24. is_sensitive_key(<<"aws_secret_access_key">>) -> true;
  25. is_sensitive_key(password) -> true;
  26. is_sensitive_key("password") -> true;
  27. is_sensitive_key(<<"password">>) -> true;
  28. is_sensitive_key(secret) -> true;
  29. is_sensitive_key("secret") -> true;
  30. is_sensitive_key(<<"secret">>) -> true;
  31. is_sensitive_key(secret_access_key) -> true;
  32. is_sensitive_key("secret_access_key") -> true;
  33. is_sensitive_key(<<"secret_access_key">>) -> true;
  34. is_sensitive_key(secret_key) -> true;
  35. is_sensitive_key("secret_key") -> true;
  36. is_sensitive_key(<<"secret_key">>) -> true;
  37. is_sensitive_key(security_token) -> true;
  38. is_sensitive_key("security_token") -> true;
  39. is_sensitive_key(<<"security_token">>) -> true;
  40. is_sensitive_key(sp_private_key) -> true;
  41. is_sensitive_key(<<"sp_private_key">>) -> true;
  42. is_sensitive_key(token) -> true;
  43. is_sensitive_key("token") -> true;
  44. is_sensitive_key(<<"token">>) -> true;
  45. is_sensitive_key(jwt) -> true;
  46. is_sensitive_key("jwt") -> true;
  47. is_sensitive_key(<<"jwt">>) -> true;
  48. is_sensitive_key(bind_password) -> true;
  49. is_sensitive_key("bind_password") -> true;
  50. is_sensitive_key(<<"bind_password">>) -> true;
  51. is_sensitive_key(_) -> false.
  52. redact(Term) ->
  53. do_redact(Term, fun is_sensitive_key/1).
  54. redact(Term, Checker) ->
  55. do_redact(Term, fun(V) ->
  56. is_sensitive_key(V) orelse Checker(V)
  57. end).
  58. redact_headers(Term) ->
  59. do_redact_headers(Term).
  60. do_redact([], _Checker) ->
  61. [];
  62. do_redact([X | Xs], Checker) ->
  63. %% Note: we could be dealing with an improper list
  64. [do_redact(X, Checker) | do_redact(Xs, Checker)];
  65. do_redact(M, Checker) when is_map(M) ->
  66. maps:map(
  67. fun(K, V) ->
  68. do_redact(K, V, Checker)
  69. end,
  70. M
  71. );
  72. do_redact({Headers, Value}, _Checker) when ?IS_KEY_HEADERS(Headers) ->
  73. {Headers, do_redact_headers(Value)};
  74. do_redact({Key, Value}, Checker) ->
  75. case Checker(Key) of
  76. true ->
  77. {Key, redact_v(Value)};
  78. false ->
  79. {do_redact(Key, Checker), do_redact(Value, Checker)}
  80. end;
  81. do_redact(T, Checker) when is_tuple(T) ->
  82. Elements = erlang:tuple_to_list(T),
  83. Redact = do_redact(Elements, Checker),
  84. erlang:list_to_tuple(Redact);
  85. do_redact(Any, _Checker) ->
  86. Any.
  87. do_redact(Headers, V, _Checker) when ?IS_KEY_HEADERS(Headers) ->
  88. do_redact_headers(V);
  89. do_redact(K, V, Checker) ->
  90. case Checker(K) of
  91. true ->
  92. redact_v(V);
  93. false ->
  94. do_redact(V, Checker)
  95. end.
  96. do_redact_headers(List) when is_list(List) ->
  97. lists:map(
  98. fun
  99. ({K, V} = Pair) ->
  100. case check_is_sensitive_header(K) of
  101. true ->
  102. {K, redact_v(V)};
  103. _ ->
  104. Pair
  105. end;
  106. (Any) ->
  107. Any
  108. end,
  109. List
  110. );
  111. do_redact_headers(Map) when is_map(Map) ->
  112. maps:map(
  113. fun(K, V) ->
  114. case check_is_sensitive_header(K) of
  115. true ->
  116. redact_v(V);
  117. _ ->
  118. V
  119. end
  120. end,
  121. Map
  122. );
  123. do_redact_headers(Value) ->
  124. Value.
  125. check_is_sensitive_header(Key) ->
  126. Key1 = string:trim(emqx_utils_conv:str(Key)),
  127. is_sensitive_header(string:lowercase(Key1)).
  128. is_sensitive_header("authorization") ->
  129. true;
  130. is_sensitive_header("proxy-authorization") ->
  131. true;
  132. is_sensitive_header(_Any) ->
  133. false.
  134. redact_v(V) when is_binary(V) -> <<?REDACT_VAL>>;
  135. %% The HOCON schema system may generate sensitive values with this format
  136. redact_v([{str, Bin}]) when is_binary(Bin) ->
  137. [{str, <<?REDACT_VAL>>}];
  138. redact_v(_V) ->
  139. ?REDACT_VAL.
  140. deobfuscate(NewConf, OldConf) ->
  141. maps:fold(
  142. fun(K, V, Acc) ->
  143. case maps:find(K, OldConf) of
  144. error ->
  145. case is_redacted(K, V) of
  146. %% don't put redacted value into new config
  147. true -> Acc;
  148. false -> Acc#{K => V}
  149. end;
  150. {ok, OldV} when is_map(V), is_map(OldV) ->
  151. Acc#{K => deobfuscate(V, OldV)};
  152. {ok, OldV} ->
  153. case is_redacted(K, V) of
  154. true ->
  155. Acc#{K => OldV};
  156. _ ->
  157. Acc#{K => V}
  158. end
  159. end
  160. end,
  161. #{},
  162. NewConf
  163. ).
  164. is_redacted(K, V) ->
  165. do_is_redacted(K, V, fun is_sensitive_key/1).
  166. is_redacted(K, V, Fun) ->
  167. do_is_redacted(K, V, fun(E) ->
  168. is_sensitive_key(E) orelse Fun(E)
  169. end).
  170. do_is_redacted(K, ?REDACT_VAL, Fun) ->
  171. Fun(K);
  172. do_is_redacted(K, <<?REDACT_VAL>>, Fun) ->
  173. Fun(K);
  174. do_is_redacted(K, WrappedFun, Fun) when is_function(WrappedFun, 0) ->
  175. %% wrapped by `emqx_secret' or other module
  176. do_is_redacted(K, WrappedFun(), Fun);
  177. do_is_redacted(_K, _V, _Fun) ->
  178. false.
  179. -ifdef(TEST).
  180. -include_lib("eunit/include/eunit.hrl").
  181. redact_test_() ->
  182. Case = fun(Type, KeyT) ->
  183. Key =
  184. case Type of
  185. atom -> KeyT;
  186. string -> erlang:atom_to_list(KeyT);
  187. binary -> erlang:atom_to_binary(KeyT)
  188. end,
  189. ?assert(is_sensitive_key(Key)),
  190. %% direct
  191. ?assertEqual({Key, ?REDACT_VAL}, redact({Key, foo})),
  192. ?assertEqual(#{Key => ?REDACT_VAL}, redact(#{Key => foo})),
  193. ?assertEqual({Key, Key, Key}, redact({Key, Key, Key})),
  194. ?assertEqual({[{Key, ?REDACT_VAL}], bar}, redact({[{Key, foo}], bar})),
  195. %% 1 level nested
  196. ?assertEqual([{Key, ?REDACT_VAL}], redact([{Key, foo}])),
  197. ?assertEqual([#{Key => ?REDACT_VAL}], redact([#{Key => foo}])),
  198. %% 2 level nested
  199. ?assertEqual(#{opts => [{Key, ?REDACT_VAL}]}, redact(#{opts => [{Key, foo}]})),
  200. ?assertEqual(#{opts => #{Key => ?REDACT_VAL}}, redact(#{opts => #{Key => foo}})),
  201. ?assertEqual({opts, [{Key, ?REDACT_VAL}]}, redact({opts, [{Key, foo}]})),
  202. %% 3 level nested
  203. ?assertEqual([#{opts => [{Key, ?REDACT_VAL}]}], redact([#{opts => [{Key, foo}]}])),
  204. ?assertEqual([{opts, [{Key, ?REDACT_VAL}]}], redact([{opts, [{Key, foo}]}])),
  205. ?assertEqual([{opts, [#{Key => ?REDACT_VAL}]}], redact([{opts, [#{Key => foo}]}]))
  206. end,
  207. Types = [atom, string, binary],
  208. Keys = [
  209. aws_secret_access_key,
  210. password,
  211. secret,
  212. secret_key,
  213. secret_access_key,
  214. security_token,
  215. token,
  216. bind_password
  217. ],
  218. [{case_name(Type, Key), fun() -> Case(Type, Key) end} || Key <- Keys, Type <- Types].
  219. redact2_test_() ->
  220. Case = fun(Key, Checker) ->
  221. ?assertEqual({Key, ?REDACT_VAL}, redact({Key, foo}, Checker)),
  222. ?assertEqual(#{Key => ?REDACT_VAL}, redact(#{Key => foo}, Checker)),
  223. ?assertEqual({Key, Key, Key}, redact({Key, Key, Key}, Checker)),
  224. ?assertEqual({[{Key, ?REDACT_VAL}], bar}, redact({[{Key, foo}], bar}, Checker))
  225. end,
  226. Checker = fun(E) -> E =:= passcode end,
  227. Keys = [secret, passcode],
  228. [{case_name(atom, Key), fun() -> Case(Key, Checker) end} || Key <- Keys].
  229. redact_improper_list_test_() ->
  230. %% improper lists: check that we don't crash
  231. %% may arise when we redact process states with pending `gen' requests
  232. [
  233. ?_assertEqual([alias | foo], redact([alias | foo])),
  234. ?_assertEqual([1, 2 | foo], redact([1, 2 | foo]))
  235. ].
  236. deobfuscate_test() ->
  237. NewConf0 = #{foo => <<"bar0">>, password => <<"123456">>},
  238. ?assertEqual(NewConf0, deobfuscate(NewConf0, #{foo => <<"bar">>, password => <<"654321">>})),
  239. NewConf1 = #{foo => <<"bar1">>, password => <<?REDACT_VAL>>},
  240. ?assertEqual(
  241. #{foo => <<"bar1">>, password => <<"654321">>},
  242. deobfuscate(NewConf1, #{foo => <<"bar">>, password => <<"654321">>})
  243. ),
  244. %% Don't have password before and ignore to put redact_val into new config
  245. NewConf2 = #{foo => <<"bar2">>, password => ?REDACT_VAL},
  246. ?assertEqual(#{foo => <<"bar2">>}, deobfuscate(NewConf2, #{foo => <<"bar">>})),
  247. %% Don't have password before and should allow put non-redact-val into new config
  248. NewConf3 = #{foo => <<"bar3">>, password => <<"123456">>},
  249. ?assertEqual(NewConf3, deobfuscate(NewConf3, #{foo => <<"bar">>})),
  250. ok.
  251. redact_header_test_() ->
  252. Types = [string, binary, atom],
  253. Keys = [
  254. "auThorization",
  255. "Authorization",
  256. "authorizaTion",
  257. "proxy-authorizaTion",
  258. "proXy-authoriZaTion"
  259. ],
  260. Case = fun(Type, Key0) ->
  261. Converter =
  262. case Type of
  263. binary ->
  264. fun erlang:list_to_binary/1;
  265. atom ->
  266. fun erlang:list_to_atom/1;
  267. _ ->
  268. fun(Any) -> Any end
  269. end,
  270. Name = Converter("headers"),
  271. Key = Converter(Key0),
  272. Value = Converter("value"),
  273. Value1 = redact_v(Value),
  274. ?assertMatch(
  275. {Name, [{Key, Value1}]},
  276. redact({Name, [{Key, Value}]})
  277. ),
  278. ?assertMatch(
  279. #{Name := #{Key := Value1}},
  280. redact(#{Name => #{Key => Value}})
  281. )
  282. end,
  283. [{case_name(Type, Key), fun() -> Case(Type, Key) end} || Key <- Keys, Type <- Types].
  284. case_name(Type, Key) ->
  285. lists:concat([Type, "-", Key]).
  286. -endif.