Packages
mob_dev
0.6.18
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mob_dev/plugin/private_key_store.ex
defmodule MobDev.Plugin.PrivateKeyStore do
@moduledoc """
Author-side storage for the per-plugin Ed25519 private key.
Keys live at `~/.mob/keys/<plugin_name>.priv` as a single line of
base64-encoded raw 32-byte key (with a trailing newline). The file
is chmod'd 0600. Plain text is intentional — plugin authors should
be able to inspect and back up the key with standard tools.
This module is **author-only**; hosts never need it. The host-side
trust model (`TrustStore`) keys off the public key fingerprint
recorded in `mob.exs`.
"""
alias MobDev.Plugin.Crypto
@key_dir_relative ".mob/keys"
@key_extension ".priv"
@secure_mode 0o600
@typedoc "Errors `read_key/1` can return."
@type read_error :: :missing | :malformed
@doc """
Absolute path to the priv key file for `plugin_name`.
Always under `~/.mob/keys/`. Pure (no I/O) and used by both
`read_key/1` and `write_key/2`.
"""
@spec key_path(atom() | String.t()) :: Path.t()
def key_path(plugin_name) do
name = to_string(plugin_name)
Path.join([key_dir(), name <> @key_extension])
end
@doc "Absolute path of the directory all priv keys live in."
@spec key_dir() :: Path.t()
def key_dir do
Path.join(home_dir(), @key_dir_relative)
end
@doc """
Reads the priv key for `plugin_name` and returns the raw 32-byte
binary. Returns `{:error, :missing}` if the file is absent or
`{:error, :malformed}` if the contents don't decode to a 32-byte key.
"""
@spec read_key(atom() | String.t()) :: {:ok, Crypto.priv_key()} | {:error, read_error()}
def read_key(plugin_name) do
path = key_path(plugin_name)
case File.read(path) do
{:ok, contents} -> decode_priv_key(contents)
{:error, :enoent} -> {:error, :missing}
{:error, _} -> {:error, :malformed}
end
end
defp decode_priv_key(contents) do
trimmed = String.trim(contents)
case Base.decode64(trimmed) do
{:ok, priv} when byte_size(priv) == 32 -> {:ok, priv}
_ -> {:error, :malformed}
end
end
@doc """
Writes the priv key for `plugin_name` to disk with mode 0600.
Creates the key directory if needed. Overwrites any existing file —
callers (`mix mob.plugin.keygen`) gate this on a confirmation /
`--force` flag before invoking.
"""
@spec write_key(atom() | String.t(), Crypto.priv_key()) :: :ok
def write_key(plugin_name, priv_bin) when is_binary(priv_bin) do
path = key_path(plugin_name)
File.mkdir_p!(Path.dirname(path))
File.write!(path, Base.encode64(priv_bin) <> "\n")
File.chmod!(path, @secure_mode)
:ok
end
@doc "File mode applied to written keys (0o600 = owner read+write only)."
@spec secure_mode() :: integer()
def secure_mode, do: @secure_mode
# Resolved per-call so tests can override via the `:mob_dev` Application
# env (`:plugin_key_home`). Falls back to `System.user_home!/0`, which
# is what end users hit. `HOME` env-var overrides are intentionally
# not honoured — Erlang caches the user home at OTP boot and `HOME`
# changes inside a running BEAM don't flow through.
defp home_dir do
case Application.get_env(:mob_dev, :plugin_key_home) do
nil -> System.user_home!()
override when is_binary(override) -> override
end
end
end