emqx_utils_redact.erl 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  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(L, Checker) when is_list(L) ->
  61. lists:map(fun(E) -> do_redact(E, Checker) end, L);
  62. do_redact(M, Checker) when is_map(M) ->
  63. maps:map(
  64. fun(K, V) ->
  65. do_redact(K, V, Checker)
  66. end,
  67. M
  68. );
  69. do_redact({Headers, Value}, _Checker) when ?IS_KEY_HEADERS(Headers) ->
  70. {Headers, do_redact_headers(Value)};
  71. do_redact({Key, Value}, Checker) ->
  72. case Checker(Key) of
  73. true ->
  74. {Key, redact_v(Value)};
  75. false ->
  76. {do_redact(Key, Checker), do_redact(Value, Checker)}
  77. end;
  78. do_redact(T, Checker) when is_tuple(T) ->
  79. Elements = erlang:tuple_to_list(T),
  80. Redact = do_redact(Elements, Checker),
  81. erlang:list_to_tuple(Redact);
  82. do_redact(Any, _Checker) ->
  83. Any.
  84. do_redact(Headers, V, _Checker) when ?IS_KEY_HEADERS(Headers) ->
  85. do_redact_headers(V);
  86. do_redact(K, V, Checker) ->
  87. case Checker(K) of
  88. true ->
  89. redact_v(V);
  90. false ->
  91. do_redact(V, Checker)
  92. end.
  93. do_redact_headers(List) when is_list(List) ->
  94. lists:map(
  95. fun
  96. ({K, V} = Pair) ->
  97. case check_is_sensitive_header(K) of
  98. true ->
  99. {K, redact_v(V)};
  100. _ ->
  101. Pair
  102. end;
  103. (Any) ->
  104. Any
  105. end,
  106. List
  107. );
  108. do_redact_headers(Map) when is_map(Map) ->
  109. maps:map(
  110. fun(K, V) ->
  111. case check_is_sensitive_header(K) of
  112. true ->
  113. redact_v(V);
  114. _ ->
  115. V
  116. end
  117. end,
  118. Map
  119. );
  120. do_redact_headers(Value) ->
  121. Value.
  122. check_is_sensitive_header(Key) ->
  123. Key1 = string:trim(emqx_utils_conv:str(Key)),
  124. is_sensitive_header(string:lowercase(Key1)).
  125. is_sensitive_header("authorization") ->
  126. true;
  127. is_sensitive_header("proxy-authorization") ->
  128. true;
  129. is_sensitive_header(_Any) ->
  130. false.
  131. redact_v(V) when is_binary(V) -> <<?REDACT_VAL>>;
  132. %% The HOCON schema system may generate sensitive values with this format
  133. redact_v([{str, Bin}]) when is_binary(Bin) ->
  134. [{str, <<?REDACT_VAL>>}];
  135. redact_v(_V) ->
  136. ?REDACT_VAL.
  137. deobfuscate(NewConf, OldConf) ->
  138. maps:fold(
  139. fun(K, V, Acc) ->
  140. case maps:find(K, OldConf) of
  141. error ->
  142. case is_redacted(K, V) of
  143. %% don't put redacted value into new config
  144. true -> Acc;
  145. false -> Acc#{K => V}
  146. end;
  147. {ok, OldV} when is_map(V), is_map(OldV) ->
  148. Acc#{K => deobfuscate(V, OldV)};
  149. {ok, OldV} ->
  150. case is_redacted(K, V) of
  151. true ->
  152. Acc#{K => OldV};
  153. _ ->
  154. Acc#{K => V}
  155. end
  156. end
  157. end,
  158. #{},
  159. NewConf
  160. ).
  161. is_redacted(K, V) ->
  162. do_is_redacted(K, V, fun is_sensitive_key/1).
  163. is_redacted(K, V, Fun) ->
  164. do_is_redacted(K, V, fun(E) ->
  165. is_sensitive_key(E) orelse Fun(E)
  166. end).
  167. do_is_redacted(K, ?REDACT_VAL, Fun) ->
  168. Fun(K);
  169. do_is_redacted(K, <<?REDACT_VAL>>, Fun) ->
  170. Fun(K);
  171. do_is_redacted(K, WrappedFun, Fun) when is_function(WrappedFun, 0) ->
  172. %% wrapped by `emqx_secret' or other module
  173. do_is_redacted(K, WrappedFun(), Fun);
  174. do_is_redacted(_K, _V, _Fun) ->
  175. false.
  176. -ifdef(TEST).
  177. -include_lib("eunit/include/eunit.hrl").
  178. redact_test_() ->
  179. Case = fun(Type, KeyT) ->
  180. Key =
  181. case Type of
  182. atom -> KeyT;
  183. string -> erlang:atom_to_list(KeyT);
  184. binary -> erlang:atom_to_binary(KeyT)
  185. end,
  186. ?assert(is_sensitive_key(Key)),
  187. %% direct
  188. ?assertEqual({Key, ?REDACT_VAL}, redact({Key, foo})),
  189. ?assertEqual(#{Key => ?REDACT_VAL}, redact(#{Key => foo})),
  190. ?assertEqual({Key, Key, Key}, redact({Key, Key, Key})),
  191. ?assertEqual({[{Key, ?REDACT_VAL}], bar}, redact({[{Key, foo}], bar})),
  192. %% 1 level nested
  193. ?assertEqual([{Key, ?REDACT_VAL}], redact([{Key, foo}])),
  194. ?assertEqual([#{Key => ?REDACT_VAL}], redact([#{Key => foo}])),
  195. %% 2 level nested
  196. ?assertEqual(#{opts => [{Key, ?REDACT_VAL}]}, redact(#{opts => [{Key, foo}]})),
  197. ?assertEqual(#{opts => #{Key => ?REDACT_VAL}}, redact(#{opts => #{Key => foo}})),
  198. ?assertEqual({opts, [{Key, ?REDACT_VAL}]}, redact({opts, [{Key, foo}]})),
  199. %% 3 level nested
  200. ?assertEqual([#{opts => [{Key, ?REDACT_VAL}]}], redact([#{opts => [{Key, foo}]}])),
  201. ?assertEqual([{opts, [{Key, ?REDACT_VAL}]}], redact([{opts, [{Key, foo}]}])),
  202. ?assertEqual([{opts, [#{Key => ?REDACT_VAL}]}], redact([{opts, [#{Key => foo}]}]))
  203. end,
  204. Types = [atom, string, binary],
  205. Keys = [
  206. aws_secret_access_key,
  207. password,
  208. secret,
  209. secret_key,
  210. secret_access_key,
  211. security_token,
  212. token,
  213. bind_password
  214. ],
  215. [{case_name(Type, Key), fun() -> Case(Type, Key) end} || Key <- Keys, Type <- Types].
  216. redact2_test_() ->
  217. Case = fun(Key, Checker) ->
  218. ?assertEqual({Key, ?REDACT_VAL}, redact({Key, foo}, Checker)),
  219. ?assertEqual(#{Key => ?REDACT_VAL}, redact(#{Key => foo}, Checker)),
  220. ?assertEqual({Key, Key, Key}, redact({Key, Key, Key}, Checker)),
  221. ?assertEqual({[{Key, ?REDACT_VAL}], bar}, redact({[{Key, foo}], bar}, Checker))
  222. end,
  223. Checker = fun(E) -> E =:= passcode end,
  224. Keys = [secret, passcode],
  225. [{case_name(atom, Key), fun() -> Case(Key, Checker) end} || Key <- Keys].
  226. deobfuscate_test() ->
  227. NewConf0 = #{foo => <<"bar0">>, password => <<"123456">>},
  228. ?assertEqual(NewConf0, deobfuscate(NewConf0, #{foo => <<"bar">>, password => <<"654321">>})),
  229. NewConf1 = #{foo => <<"bar1">>, password => <<?REDACT_VAL>>},
  230. ?assertEqual(
  231. #{foo => <<"bar1">>, password => <<"654321">>},
  232. deobfuscate(NewConf1, #{foo => <<"bar">>, password => <<"654321">>})
  233. ),
  234. %% Don't have password before and ignore to put redact_val into new config
  235. NewConf2 = #{foo => <<"bar2">>, password => ?REDACT_VAL},
  236. ?assertEqual(#{foo => <<"bar2">>}, deobfuscate(NewConf2, #{foo => <<"bar">>})),
  237. %% Don't have password before and should allow put non-redact-val into new config
  238. NewConf3 = #{foo => <<"bar3">>, password => <<"123456">>},
  239. ?assertEqual(NewConf3, deobfuscate(NewConf3, #{foo => <<"bar">>})),
  240. ok.
  241. redact_header_test_() ->
  242. Types = [string, binary, atom],
  243. Keys = [
  244. "auThorization",
  245. "Authorization",
  246. "authorizaTion",
  247. "proxy-authorizaTion",
  248. "proXy-authoriZaTion"
  249. ],
  250. Case = fun(Type, Key0) ->
  251. Converter =
  252. case Type of
  253. binary ->
  254. fun erlang:list_to_binary/1;
  255. atom ->
  256. fun erlang:list_to_atom/1;
  257. _ ->
  258. fun(Any) -> Any end
  259. end,
  260. Name = Converter("headers"),
  261. Key = Converter(Key0),
  262. Value = Converter("value"),
  263. Value1 = redact_v(Value),
  264. ?assertMatch(
  265. {Name, [{Key, Value1}]},
  266. redact({Name, [{Key, Value}]})
  267. ),
  268. ?assertMatch(
  269. #{Name := #{Key := Value1}},
  270. redact(#{Name => #{Key => Value}})
  271. )
  272. end,
  273. [{case_name(Type, Key), fun() -> Case(Type, Key) end} || Key <- Keys, Type <- Types].
  274. case_name(Type, Key) ->
  275. lists:concat([Type, "-", Key]).
  276. -endif.