Current section
Files
Jump to
Current section
Files
lib/crypto/public_key.ex
defmodule Crypto.PublicKey do
@moduledoc false
# @moduledoc """
# API module for public-key infrastructure.
# ## Description
# Mostly wrappers Erlang' `:public_key` module, to help simplify using public/private key encryption in Elixir.
# """
# Erlang public_key v1.4.1 corresponds to Erlang/OTP 20.0
@otp_20_public_key_version [1, 4, 1]
defmacro __using__(_) do
quote do
import Crypto.PublicKey
alias Crypto.RSAPublicKey, as: RSAPublicKey
alias Crypto.PublicKey.RSAPrivateKey, as: RSAPrivateKey
end
end
def normalize_error(stacktrace, kind, error) do
case Exception.normalize(kind, error) do
%{message: message} ->
{:error, message}
x ->
{kind, x, stacktrace}
end
end
@doc """
Loads PEM string from the specified file path and returns a `Crypto.PublicKey.RSAPrivateKey` or a `Crypto.RSAPublicKey` key.
Optionally, a passphrase can be given to decode the PEM certificate.
## Examples
{:ok, key} = Crypto.PublicKey.load("/file/to/cert.pem")
{:ok, key} = Crypto.PublicKey.load("/file/to/cert.pem", "pem_password")
"""
def load(file_path, passphrase \\ nil) do
case File.read(file_path) do
{:ok, key_string} ->
Crypto.PublicKey.loads(key_string, passphrase)
{:error, reason} ->
{:error, reason}
end
end
@doc """
Loads PEM string from the specified file path and returns a `Crypto.PublicKey.RSAPrivateKey` or a `Crypto.RSAPublicKey` key.
Optionally, a passphrase can be given to decode the PEM certificate.
Identical to `Crypto.PublicKey.load/2`, except that load! raises an Crypto.Error when an exception occurs.
## Examples
key = Crypto.PublicKey.load("/file/to/cert.pem")
key = Crypto.PublicKey.load("/file/to/cert.pem", "pem_password")
"""
def load!(file_path, passphrase \\ nil) do
case load(file_path, passphrase) do
{:ok, key} ->
key
{:error, reason} ->
raise Crypto.Error, reason: reason
end
end
defp validate_pem_length(pem_entries) do
case length(pem_entries) do
0 -> {:error, "invalid argument"}
x when x > 1 -> {:error, "found multiple PEM entries, expected only 1"}
x when x == 1 -> {:ok, Enum.at(pem_entries, 0)}
end
end
@doc """
Converts a PEM string into an `Crypto.PublicKey.RSAPrivateKey` or an `Crypto.RSAPublicKey` key.
Optionally, a passphrase can be given to decode the PEM certificate.
## Examples
{:ok, key} = Crypto.PublicKey.loads(pem_string)
{:ok, key} = Crypto.PublicKey.loads(pem_string, "pem_password")
"""
def loads(pem_string, passphrase \\ nil) do
pem_entries = :public_key.pem_decode(pem_string)
with {:ok, pem_entry} <- validate_pem_length(pem_entries),
{:ok, rsa_key} <- load_pem_entry(pem_entry, passphrase),
do: sort_key_tup(rsa_key)
end
@doc """
Converts a PEM string into an `Crypto.PublicKey.RSAPrivateKey` or an `Crypto.RSAPublicKey` key.
Identical to `Crypto.PublicKey.loads/2`, except that loads! raises an Crypto.Error when an exception occurs.
## Example
key = Crypto.PublicKey.loads!(pem_string)
"""
def loads!(pem_string, passphrase \\ nil) do
case loads(pem_string, passphrase) do
{:ok, key} ->
key
{:error, reason} ->
raise Crypto.Error, reason: reason
end
end
defp load_pem_entry(pem_entry, passphrase) do
cond do
is_binary(passphrase) ->
load_pem_entry(pem_entry, String.to_charlist(passphrase))
is_nil(passphrase) ->
{:ok, :public_key.pem_entry_decode(pem_entry)}
true ->
{:ok, :public_key.pem_entry_decode(pem_entry, passphrase)}
end
catch
kind, error ->
Crypto.PublicKey.normalize_error(__STACKTRACE__, kind, error)
end
defp sort_key_tup(key_tup) do
case elem(key_tup, 0) do
:RSAPrivateKey ->
{:ok, Crypto.PublicKey.RSAPrivateKey.from_sequence(key_tup)}
:RSAPublicKey ->
{:ok, Crypto.RSAPublicKey.from_sequence(key_tup)}
x ->
{:error,
"invalid argument, expected one of[Crypto.RSAPublicKey, Crypto.PublicKey.RSAPrivateKey], found: #{x}"}
end
end
defp sign_0(rsa_priv_key_seq, msg, sha) do
{:ok, :public_key.sign(msg, sha, rsa_priv_key_seq)}
catch
kind, error ->
Crypto.PublicKey.normalize_error(__STACKTRACE__, kind, error)
end
def sign(msg, sha, private_key) do
with {:ok, priv_key_sequence} <- Crypto.PublicKey.RSAPrivateKey.as_sequence(private_key),
do: sign_0(priv_key_sequence, msg, sha)
end
def sign(msg, private_key) do
Crypto.PublicKey.sign(msg, :sha256, private_key)
end
defp verify_0(rsa_pub_key_seq, msg, sha, signature) do
{:ok, :public_key.verify(msg, sha, signature, rsa_pub_key_seq)}
catch
kind, error -> Crypto.PublicKey.normalize_error(__STACKTRACE__, kind, error)
end
def verify(msg, sha, signature, public_key) do
with {:ok, pub_key_sequence} <- Crypto.RSAPublicKey.as_sequence(public_key),
do: verify_0(pub_key_sequence, msg, sha, signature)
end
def verify(msg, signature, public_key) do
Crypto.PublicKey.verify(msg, :sha256, signature, public_key)
end
defp encrypt_private_0(rsa_priv_key_seq, clear_text) do
{:ok, :public_key.encrypt_private(clear_text, rsa_priv_key_seq)}
catch
kind, error ->
Crypto.PublicKey.normalize_error(__STACKTRACE__, kind, error)
end
def encrypt_private(clear_text, private_key, opts \\ []) do
url_safe = Keyword.get(opts, :url_safe, true)
with {:ok, priv_key_sequence} <- Crypto.PublicKey.RSAPrivateKey.as_sequence(private_key),
{:ok, cipher_bytes} <- encrypt_private_0(priv_key_sequence, clear_text),
encoded_cipher_text = encode(cipher_bytes, url_safe),
do: {:ok, encoded_cipher_text}
end
defp encrypt_public_0(rsa_pub_key_seq, clear_text) do
{:ok, :public_key.encrypt_public(clear_text, rsa_pub_key_seq)}
catch
kind, error ->
Crypto.PublicKey.normalize_error(__STACKTRACE__, kind, error)
end
def encrypt_public(clear_text, public_key, opts \\ []) do
url_safe = Keyword.get(opts, :url_safe, true)
with {:ok, pub_key_sequence} <- Crypto.RSAPublicKey.as_sequence(public_key),
{:ok, cipher_bytes} <- encrypt_public_0(pub_key_sequence, clear_text),
encoded_cipher_text = encode(cipher_bytes, url_safe),
do: {:ok, encoded_cipher_text}
end
defp decrypt_private_0(cipher_bytes, private_key) do
case Crypto.PublicKey.RSAPrivateKey.as_sequence(private_key) do
{:ok, rsa_priv_key_seq} -> {:ok, [cipher_bytes, rsa_priv_key_seq]}
{:error, reason} -> {:error, reason}
end
end
defp decrypt_private_1([cipher_bytes, rsa_priv_key_seq]) do
{:ok, :public_key.decrypt_private(cipher_bytes, rsa_priv_key_seq)}
catch
kind, error ->
Crypto.PublicKey.normalize_error(__STACKTRACE__, kind, error)
end
def decrypt_private(cipher_text, private_key, opts \\ []) do
url_safe = Keyword.get(opts, :url_safe, true)
with {:ok, decoded_cipher_text} <- decode(cipher_text, url_safe),
{:ok, [cipher_bytes, rsa_priv_key_seq]} <-
decrypt_private_0(decoded_cipher_text, private_key),
do: decrypt_private_1([cipher_bytes, rsa_priv_key_seq])
end
defp decrypt_public_0(cipher_bytes, public_key) do
case Crypto.RSAPublicKey.as_sequence(public_key) do
{:ok, rsa_pub_key_seq} -> {:ok, [cipher_bytes, rsa_pub_key_seq]}
{:error, reason} -> {:error, reason}
end
end
defp decrypt_public_1([cipher_bytes, rsa_pub_key_seq]) do
{:ok, :public_key.decrypt_public(cipher_bytes, rsa_pub_key_seq)}
catch
kind, error ->
Crypto.PublicKey.normalize_error(__STACKTRACE__, kind, error)
end
def decrypt_public(cipher_text, public_key, opts \\ []) do
url_safe = Keyword.get(opts, :url_safe, true)
with {:ok, decoded_cipher_text} <- decode(cipher_text, url_safe),
{:ok, [cipher_bytes, rsa_pub_key_seq]} <-
decrypt_public_0(decoded_cipher_text, public_key),
do: decrypt_public_1([cipher_bytes, rsa_pub_key_seq])
end
def generate_key, do: generate_key(:rsa, 2048, 65537)
def generate_key(bits), do: generate_key(:rsa, bits, 65537)
def generate_key(bits, public_exp), do: generate_key(:rsa, bits, public_exp)
def generate_key(:rsa, bits, public_exp),
do: generate_key(:rsa, bits, public_exp, otp_has_rsa_gen_support())
@doc """
Generate a new key.
Note: To ensure Backwards compatibility when generating rsa keys on OTP < 20, we fall back to openssl via System.cmd.
## Example
{:ok, rsa_priv_key} = Crypto.PublicKey.generate_key(:rsa, 2048)
"""
def generate_key(type, bits, public_exp) do
{:ok, :public_key.generate_key({type, bits, public_exp})}
catch
kind, error ->
Crypto.PublicKey.normalize_error(__STACKTRACE__, kind, error)
end
def generate_key(:rsa, bits, _public_exp, false) do
# Fallback support for OTP 18 & 19.
generate_rsa_openssl_fallback(bits)
end
def generate_key(:rsa, bits, public_exp, true) do
new_rsa_key =
:public_key.generate_key({:rsa, bits, public_exp})
|> Crypto.PublicKey.RSAPrivateKey.from_sequence()
{:ok, new_rsa_key}
end
@doc """
Extract the public part of a private string and return the results as a Crypto.RSAPublicKey struct.
## Example
{:ok, rsa_pub_key} = Crypto.PublicKey.public_key_from_private_key(rsa_priv_key)
"""
def public_key_from_private_key(private_key = %Crypto.PublicKey.RSAPrivateKey{}) do
{:ok,
Crypto.RSAPublicKey.from_sequence(
{:RSAPublicKey, private_key.public_modulus, private_key.public_exponent}
)}
end
@doc """
Encode a key into a PEM string.
To decode, use `Crypto.PublicKey.loads/1`
## Example
{:ok, pem_string} = Crypto.PublicKey.pem_encode(key)
"""
def pem_encode(key = %Crypto.PublicKey.RSAPrivateKey{}) do
with {:ok, key_sequence} <- Crypto.PublicKey.RSAPrivateKey.as_sequence(key),
do: pem_entry_encode(key_sequence, :RSAPrivateKey)
end
def pem_encode(key = %Crypto.RSAPublicKey{}) do
with {:ok, key_sequence} <- Crypto.RSAPublicKey.as_sequence(key),
do: pem_entry_encode(key_sequence, :RSAPublicKey)
end
# Helpers
defp pem_entry_encode(key, type) do
pem_entry = :public_key.pem_entry_encode(type, key)
{:ok, :public_key.pem_encode([pem_entry])}
catch
kind, error ->
Crypto.PublicKey.normalize_error(__STACKTRACE__, kind, error)
end
defp decode(encoded_payload, _url_safe = true) do
Base.url_decode64(encoded_payload)
end
defp decode(encoded_payload, _url_safe = false) do
Base.decode64(encoded_payload)
end
defp encode(payload_bytes, _url_safe = true) do
Base.url_encode64(payload_bytes)
end
defp encode(payload_bytes, _url_safe = false) do
Base.encode64(payload_bytes)
end
# Erlang public_key v1.4.1 corresponds to Erlang/OTP 20.0
defp otp_has_rsa_gen_support() do
Application.spec(:public_key, :vsn)
|> Kernel.to_string()
|> String.split(".")
|> Enum.map(fn i ->
{i_int, _} = Integer.parse(i)
i_int
end)
|> otp_has_rsa_gen_support_z()
end
defp otp_has_rsa_gen_support_z(version_int_list) do
version_int_list >= @otp_20_public_key_version
end
defp generate_rsa_openssl_fallback(bits) do
with {pem_entry, 0} <- System.cmd("openssl", ["genrsa", to_string(bits)]) do
loads(pem_entry)
else
{result, ret_code} ->
{:error, "result=#{result} ret_code=#{ret_code}"}
end
end
end