Packages

Elixir implementation of the PEM encoding.

Current section

Files

Jump to
pem lib pem.ex
Raw

lib/pem.ex

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