message_verifier.ex (3467B)
1 defmodule Plug.Crypto.MessageVerifier do 2 @moduledoc """ 3 `MessageVerifier` makes it easy to generate and verify messages 4 which are signed to prevent tampering. 5 6 For example, the cookie store uses this verifier to send data 7 to the client. The data can be read by the client, but cannot be 8 tampered with. 9 10 The message and its verification are base64url encoded and returned 11 to you. 12 13 The current algorithm used is HMAC-SHA, with SHA256, SHA384, and 14 SHA512 as supported digest types. 15 """ 16 17 @doc """ 18 Signs a message according to the given secret. 19 """ 20 def sign(message, secret, digest_type \\ :sha256) 21 when is_binary(message) and byte_size(secret) > 0 and 22 digest_type in [:sha256, :sha384, :sha512] do 23 hmac_sha2_sign(message, secret, digest_type) 24 rescue 25 e -> reraise e, Plug.Crypto.prune_args_from_stacktrace(__STACKTRACE__) 26 end 27 28 @doc """ 29 Decodes and verifies the encoded binary was not tampered with. 30 """ 31 def verify(signed, secret) when is_binary(signed) and byte_size(secret) > 0 do 32 hmac_sha2_verify(signed, secret) 33 rescue 34 e -> reraise e, Plug.Crypto.prune_args_from_stacktrace(__STACKTRACE__) 35 end 36 37 ## Signature Algorithms 38 39 defp hmac_sha2_to_protected(:sha256), do: "HS256" 40 defp hmac_sha2_to_protected(:sha384), do: "HS384" 41 defp hmac_sha2_to_protected(:sha512), do: "HS512" 42 43 defp hmac_sha2_to_digest_type("HS256"), do: :sha256 44 defp hmac_sha2_to_digest_type("HS384"), do: :sha384 45 defp hmac_sha2_to_digest_type("HS512"), do: :sha512 46 47 defp hmac_sha2_sign(payload, key, digest_type) do 48 protected = hmac_sha2_to_protected(digest_type) 49 plain_text = signing_input(protected, payload) 50 signature = hmac(digest_type, key, plain_text) 51 encode_token(plain_text, signature) 52 end 53 54 defp hmac_sha2_verify(signed, key) when is_binary(signed) and is_binary(key) do 55 case decode_token(signed) do 56 {protected, payload, plain_text, signature} when protected in ["HS256", "HS384", "HS512"] -> 57 digest_type = hmac_sha2_to_digest_type(protected) 58 challenge = hmac(digest_type, key, plain_text) 59 60 if Plug.Crypto.secure_compare(challenge, signature) do 61 {:ok, payload} 62 else 63 :error 64 end 65 66 _ -> 67 :error 68 end 69 end 70 71 ## Helpers 72 73 defp encode_token(plain_text, signature) 74 when is_binary(plain_text) and is_binary(signature) do 75 plain_text <> "." <> Base.url_encode64(signature, padding: false) 76 end 77 78 defp decode_token(token) do 79 with [protected, payload, signature] <- String.split(token, ".", parts: 3), 80 plain_text = protected <> "." <> payload, 81 {:ok, protected} <- Base.url_decode64(protected, padding: false), 82 {:ok, payload} <- Base.url_decode64(payload, padding: false), 83 {:ok, signature} <- Base.url_decode64(signature, padding: false) do 84 {protected, payload, plain_text, signature} 85 else 86 _ -> :error 87 end 88 end 89 90 defp signing_input(protected, payload) when is_binary(protected) and is_binary(payload) do 91 protected 92 |> Base.url_encode64(padding: false) 93 |> Kernel.<>(".") 94 |> Kernel.<>(Base.url_encode64(payload, padding: false)) 95 end 96 97 # TODO: remove when we require OTP 22.1 98 if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :mac, 4) do 99 defp hmac(digest, key, data), do: :crypto.mac(:hmac, digest, key, data) 100 else 101 defp hmac(digest, key, data), do: :crypto.hmac(digest, key, data) 102 end 103 end