lib/wechat/pay/crypto.ex

defmodule WeChat.Pay.Crypto do
  @moduledoc "用于支付加密相关"
  import WeChat.Utils, only: [pay_doc_link_prefix: 0]

  def decrypt_aes_256_gcm(client, ciphertext, associated_data, iv) do
    data = Base.decode64!(ciphertext, padding: false)
    len = byte_size(data) - 16
    <<data::binary-size(len), tag::binary-size(16)>> = data

    :crypto.crypto_one_time_aead(
      :aes_256_gcm,
      client.api_secret_key(),
      iv,
      data,
      associated_data,
      tag,
      false
    )
  end

  @doc false
  def load_pem!({:app_dir, app, path}), do: load_pem!({:file, Application.app_dir(app, path)})
  def load_pem!({:file, path}), do: path |> File.read!() |> decode_key()

  @doc false
  def decode_key(binary) do
    binary |> :public_key.pem_decode() |> hd() |> :public_key.pem_entry_decode()
  end

  @doc """
  加密敏感信息 -
  [官方文档](#{pay_doc_link_prefix()}/merchant/development/interface-rules/sensitive-data-encryption.html){:target="_blank"}
  """
  def encrypt_secret_data(data, public_key) do
    :public_key.encrypt_public(data, public_key, rsa_pad: :rsa_pkcs1_oaep_padding)
  end

  @doc """
  解密敏感信息 -
  [官方文档](#{pay_doc_link_prefix()}/merchant/development/interface-rules/sensitive-data-encryption.html){:target="_blank"}
  """
  def decrypt_secret_data(cipher_text, private_key) do
    :public_key.decrypt_private(cipher_text, private_key, rsa_pad: :rsa_pkcs1_oaep_padding)
  end

  @doc """
  验签 -
  [官方文档](#{pay_doc_link_prefix()}/merchant/development/interface-rules/signature-verification.html){:target="_blank"}
  """
  def verify(signature, timestamp, nonce, body, public_key) do
    case Base.decode64(signature, padding: false) do
      {:ok, signature} ->
        :public_key.verify("#{timestamp}\n#{nonce}\n#{body}\n", :sha256, signature, public_key)

      _ ->
        false
    end
  end

  @doc """
  签名 -
  [官方文档](#{pay_doc_link_prefix()}/merchant/development/interface-rules/signature-generation.html){:target="_blank"}
  """
  def sign(env, timestamp, nonce_str, private_key) do
    method = to_string(env.method) |> String.upcase()
    %{path: path} = URI.parse(env.url)

    path =
      case env.query do
        [] -> path
        query -> path <> "?" <> URI.encode_query(query)
      end

    "#{method}\n#{path}\n#{timestamp}\n#{nonce_str}\n#{env.body}\n"
    |> :public_key.sign(:sha256, private_key)
    |> Base.encode64()
  end
end