Current section
Files
Jump to
Current section
Files
lib/myspace_object.ex
defmodule MyspaceObject do
@moduledoc """
An object that can be stored in a Myspace.
Receives message from IPFS and conserves it state as a dag and can state it config to the Myspace.
"""
use GenServer
require Logger
alias MyspaceIPFS.Key
alias MyspaceObject.Ipid
# This starts with an empty object, but this could very well contain lots of stuff.
@dag "bafyreihpfkdvib5muloxlj5b3tgdwibjdcu3zdsuhyft33z7gtgnlzlkpm"
@enforce_keys [:id, :created, :dag, :ipns, :object]
defstruct id: nil,
created: DateTime.utc_now(),
dag: @dag,
ipns: nil,
object: %{},
public_key: %{
pem: nil,
cid: nil
},
ipid: %{
cid: nil,
dag: nil
}
@typedoc """
The MyspaceObject is created with an atom as its name.
This name, the MyspaceID or `myid`, is used to name the object locally.
The dag is the IPFS hash of the object.
The public key is the public key of the object, as derived from the IPNS.
As such the IPNS name and the public key are the same, but the public_key is added here for quick local (trusted) lookup.
The reason for doing it this way is that it is immediately available, where as publication to IPNS can take minutes,
especially the first time.
The default dag is `bafyreihpfkdvib5muloxlj5b3tgdwibjdcu3zdsuhyft33z7gtgnlzlkpm` which is the IPFS hash of an empty object.
"""
@type t :: %__MODULE__{
id: atom(),
created: DateTime.t(),
dag: binary(),
ipns: binary() | nil,
public_key:
%{
pem: binary(),
cid: binary()
}
| nil,
object: map(),
ipid: %{
cid: binary() | nil,
dag: binary() | nil
}
}
@typedoc """
The IPID is the object that is stored in the Myspace.
"""
@type ipid :: %{
id: binary(),
context: list(),
created: binary(),
updated: binary(),
public_key: %{/: binary()},
verification_method: binary()
}
@doc """
Recreate a MyspaceObject from a map or a dag. When a map is given, it is assumed that the map is a valid MyspaceObject.
When a dag is given, it is assumed that the dag is a valid IPLD object.
When an object is passed a new keypair is generated and the public key is added to the object.
If no parameter is given, a new default MyspaceObject is created.
"""
@spec start_link(MyspaceObject.t() | binary()) ::
:ignore | {:error, any} | {:ok, pid}
def start_link(object) when is_map(object) do
GenServer.start_link(__MODULE__, object, name: object.id)
end
def start_link(dag) when is_binary(dag) do
object = new(dag)
GenServer.start_link(__MODULE__, object, name: object.id)
end
@spec start_link() :: :ignore | {:error, any} | {:ok, pid}
def start_link() do
object = new()
GenServer.start_link(__MODULE__, object, name: object.id)
end
@spec init(t()) :: {:ok, t()}
def init(object) when is_map(object) do
ipns = get_or_create_key!(Atom.to_string(object.id))
object = %__MODULE__{object | ipns: ipns, public_key: generate_public_key()}
GenServer.cast(self(), :ipid_publish)
GenServer.cast(self(), :ipid_put)
{:ok, %__MODULE__{object | object: ipld(object.dag)}}
end
# Returns a skeleton object. It lacks a public key, but that is added later.
# This is because the public key is derived from a secret key, which is not available at this point.
@spec new(binary() | atom()) :: t()
def new(dag \\ @dag) when is_binary(dag) do
id = String.to_atom(Nanoid.generate())
%__MODULE__{
id: id,
created: DateTime.utc_now(),
dag: dag,
ipns: get_or_create_key!(Atom.to_string(id)),
object: ipld(dag),
public_key: nil
}
end
# Getters
def handle_call(:public_key, _from, state) do
{:reply, state.public_key.key, state}
end
def handle_call(:public_key_cid!, _from, state) do
{:reply, state.public_key.cid.hash, state}
end
def handle_call(:public_key_pem, _from, state) do
{:reply, state.public_key.pem, state}
end
def handle_call(:ipns, _from, state) do
{:reply, state.ipns, state}
end
# Methods
def handle_call({:decrypt, message}, _from, state) do
{:reply, ExPublicKey.decrypt_private(message, Process.get(:private_key)), state}
end
def handle_call({:sign, message}, _from, state) do
{:reply, ExPublicKey.sign(message, Process.get(:private_key)), state}
end
def handle_call({:message, message, recipient}, _from, state) do
# By using the recipient as the key, we can use the same key to encrypt the message and the signature.
{:ok, encrypted_message} = ExPublicKey.encrypt_public(message, state.public_key)
message = MyspaceObject.Message.new(encrypted_message, recipient, state.id)
signed_message = ExPublicKey.sign(message, Process.get(:private_key))
{:reply, signed_message, state}
end
def handle_call(msg, _from, state) do
{:reply, msg, state}
end
# Casts
# Don't wait for answers and how to handle them at this point.
# Just assume it works.
@spec handle_cast(:ipid_publish, MyspaceObject.t()) :: {:noreply, MyspaceObject.t()}
def handle_cast(:ipid_publish, state) do
Logger.debug("Publishing object #{inspect(state)}")
Task.async(fn -> Ipid.publish(state) end)
{:noreply, state}
end
@spec handle_cast(:ipid_put, MyspaceObject.t()) :: {:noreply, MyspaceObject.t()}
def handle_cast(:ipid_put, state) do
Logger.debug("Put object #{inspect(state)}")
Task.async(fn -> Ipid.put(state) end)
{:noreply, state}
end
@spec handle_cast(any, any, any) :: {:reply, any, any}
def handle_cast(msg, _from, state) do
{:reply, msg, state}
end
# Info
def handle_info({_task, {:ipid_publish, cid}}, state) do
Logger.debug("IPID published: #{inspect(cid)}")
{:noreply, %{state | ipid: %{state.ipid | cid: cid}}}
end
def handle_info({_task, {:ipid_put, dag}}, state) do
Logger.debug("IPID put: #{inspect(dag)}")
{:noreply, %{state | ipid: %{state.ipid | dag: dag}}}
end
def handle_info(msg, state) do
case msg do
{:DOWN, _ref, :process, _pid, _reason} ->
Logger.debug("DOWN: #{inspect(msg)}")
{:noreply, state}
end
end
# Helpers
defp get_or_create_key!(id) when is_binary(id) do
case get_ipns_key(id) do
{:error, :not_found} -> create_key!(id)
{:ok, key} -> key
end
end
defp create_key!(id) when is_binary(id) do
{:ok, key} = Key.gen(id)
key["Id"]
end
# Lookup the IPNS key for the given id.
defp get_ipns_key(id) when is_binary(id) do
{:ok, %{"Keys" => keys}} = Key.list(l: true)
case Enum.find(keys, fn key -> key["Name"] == id end) do
nil -> {:error, :not_found}
key -> {:ok, key["Id"]}
end
end
# Simply get the public key as a PEM. (For publication in IPNS)
defp public_key_pem do
{:ok, pub} = ExPublicKey.public_key_from_private_key(Process.get(:private_key))
{:ok, public_key_pem} = ExPublicKey.pem_encode(pub)
public_key_pem
end
# Get the CID of the public key. (For linking in IPLD)
defp public_key_cid!(public_key_pem) do
{:ok, result} = MyspaceIPFS.add(public_key_pem)
Logger.debug("Public key CID: #{inspect(result.hash)}")
result.hash
end
# Fetch the contents of the dag and decode it.
defp ipld(dag) do
{:ok, ipld} = MyspaceIPFS.Dag.get(dag)
Logger.debug("IPLD: #{inspect(ipld)}")
ipld
end
defp generate_public_key() do
Process.flag(:sensitive, true)
{:ok, priv} = ExPublicKey.generate_key()
Process.put(:private_key, priv)
{:ok, pub} = ExPublicKey.public_key_from_private_key(priv)
Process.put(:public_key, pub)
%{
pem: public_key_pem(),
cid: public_key_cid!(public_key_pem())
}
end
end