defmodule PEM do @moduledoc """ Implementation of rfc7468 (https://tools.ietf.org/html/rfc7468) encoding and decoding of PEM binaries. """ @regex ~r"-----BEGIN ([A-Z0-9- ]+)-----\n+((?:[A-Za-z0-9+\/=]{1,64}\n)*)-----END ([A-Z0-9- ]+)-----\n?" # must not start or end with whitespace or a hyphen, and must not contain consecutive hyphens or whitespace @strict_label_regex ~r"^(?:[A-Z0-9]{1,2}|^([A-Z0-9][- ]?(?:[A-Z0-9]+[ -]?)*[A-Z0-9]))$" @doc """ Encodes the given content with the given label. ```elixir iex> PEM.encode("Hello World", "TEST") "-----BEGIN TEST-----\nSGVsbG8gV29ybGQ=\n-----END TEST-----\n" ``` """ def encode(content, label, options \\ []) do trail_newline? = Keyword.get(options, :trail_newline, true) encapsulated_text = Base.encode64(content, padding: true) |> String.codepoints() |> Enum.chunk_every(64) |> Enum.map(&Enum.join/1) |> Enum.join("\n") header(label) <> "\n" <> encapsulated_text <> "\n" <> footer(label) <> (if trail_newline?, do: "\n", else: "") end @doc """ Decodes the given PEM encoded content. ```elixir iex> PEM.decode("-----BEGIN TEST-----\\nSGVsbG8gV29ybGQ=\\n-----END TEST-----\\n") {:ok, "Hello World"} ``` You can optionally pass `strict: true` to enforce stricter validation of the PEM content. ```elixir iex> PEM.decode("-----BEGIN TEST1-----\\nSGVsbG8gV29ybGQ=\\n-----END TEST2-----\\n", strict: true) {:error, :different_labels} ``` You can also pass `enforce_label: ""` to enforce the label of the PEM content. ```elixir iex> PEM.decode("-----BEGIN TEST-----\\nSGVsbG8gV29ybGQ=\\n-----END TEST-----\\n", enforce_label: "TEST1") {:error, :header_label_mismatch} ``` """ def decode(content, options \\ []) do strict? = Keyword.get(options, :strict, false) enforce_label = Keyword.get(options, :enforce_label, false) if Regex.match?(@regex, content) do [match, header_label, content, footer_label] = Regex.run(@regex, content) decoded = content |> String.split("\n", trim: true) |> Enum.join("") |> Base.decode64() cond do decoded == :error -> {:error, :invalid_base64} strict? && !Regex.match?(@strict_label_regex, header_label) -> {:error, :invalid_header_label} strict? && !Regex.match?(@strict_label_regex, footer_label) -> {:error, :invalid_footer_label} strict? && header_label != footer_label -> {:error, :different_labels} is_binary(enforce_label) && enforce_label != header_label -> {:error, :header_label_mismatch} is_binary(enforce_label) && enforce_label != footer_label -> {:error, :footer_label_mismatch} strict? && byte_size(content) != byte_size(match) -> {:error, :trailing_content} true -> decoded end else {:error, :invalid_format} end end defp header(label) do "-----BEGIN #{String.upcase(label)}-----" end defp footer(label) do "-----END #{String.upcase(label)}-----" end end