Add API, initial DB tables, some docs
This commit is contained in:
parent
6fc4dfe3e9
commit
85961f6008
28 changed files with 659 additions and 20 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -36,3 +36,5 @@ npm-debug.log
|
|||
*.db
|
||||
*.db-*
|
||||
|
||||
# SSH test files for testing signing/verifying
|
||||
/test_files/
|
||||
|
|
|
@ -10,13 +10,18 @@ import Config
|
|||
config :minisome,
|
||||
ecto_repos: [Minisome.Storage.Repo]
|
||||
|
||||
# Configures the endpoint
|
||||
# Configures the endpoints
|
||||
config :minisome, Minisome.Web.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
render_errors: [view: Minisome.Web.ErrorView, accepts: ~w(html json), layout: false],
|
||||
pubsub_server: Minisome.PubSub,
|
||||
live_view: [signing_salt: "vfAnu01K"]
|
||||
|
||||
config :minisome, Minisome.API.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
render_errors: [view: Minisome.Web.ErrorView, accepts: ~w(html json), layout: false],
|
||||
pubsub_server: Minisome.PubSub
|
||||
|
||||
# Configure esbuild (the version is required)
|
||||
config :esbuild,
|
||||
version: "0.14.0",
|
||||
|
|
|
@ -25,6 +25,13 @@ config :minisome, Minisome.Web.Endpoint,
|
|||
esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
|
||||
]
|
||||
|
||||
config :minisome, Minisome.API.Endpoint,
|
||||
http: [ip: {127, 0, 0, 1}, port: 33101],
|
||||
check_origin: false,
|
||||
code_reloader: true,
|
||||
debug_errors: true,
|
||||
secret_key_base: "MR3TEt40ApczckU3+IRzAi91t5iNhbiRt4l0ChL30DdADwAwtdfa++i+NX0ezfc1"
|
||||
|
||||
# ## SSL Support
|
||||
#
|
||||
# In order to use HTTPS in development, a self-signed
|
||||
|
|
|
@ -37,7 +37,8 @@ if config_env() == :prod do
|
|||
"""
|
||||
|
||||
host = System.get_env("PHX_HOST") || "example.com"
|
||||
port = String.to_integer(System.get_env("PORT") || "4000")
|
||||
port = String.to_integer(System.get_env("WEB_PORT") || "4000")
|
||||
api_port = String.to_integer(System.get_env("API_PORT") || "33101")
|
||||
|
||||
config :minisome, Minisome.Web.Endpoint,
|
||||
url: [host: host, port: 443],
|
||||
|
@ -51,6 +52,18 @@ if config_env() == :prod do
|
|||
],
|
||||
secret_key_base: secret_key_base
|
||||
|
||||
config :minisome, Minisome.API.Endpoint,
|
||||
url: [host: host, port: api_port],
|
||||
http: [
|
||||
# Enable IPv6 and bind on all interfaces.
|
||||
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
|
||||
# See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
|
||||
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
|
||||
ip: {0, 0, 0, 0, 0, 0, 0, 0},
|
||||
port: api_port
|
||||
],
|
||||
secret_key_base: secret_key_base
|
||||
|
||||
# ## Using releases
|
||||
#
|
||||
# If you are doing OTP releases, you need to instruct Phoenix
|
||||
|
|
|
@ -17,6 +17,11 @@ config :minisome, Minisome.Web.Endpoint,
|
|||
secret_key_base: "nlj8uUOi1lhYBQz7cO87ICbxJSAOoKEEqmf0qWJs3Gs2h4XyJKqzBpM6MFmM2nkG",
|
||||
server: false
|
||||
|
||||
config :minisome, Minisome.API.Endpoint,
|
||||
http: [ip: {127, 0, 0, 1}, port: 33101],
|
||||
secret_key_base: "nlj8uUOi1lhYBQz7cO87ICbxJSAOoKEEqmf0qWJs3Gs2h4XyJKqzBpM6MFmM2nkG",
|
||||
server: false
|
||||
|
||||
# Print only warnings and errors during test
|
||||
config :logger, level: :warn
|
||||
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
defmodule Minisome.API.Client.Message do
|
||||
@moduledoc """
|
||||
Low level message handling tools.
|
||||
|
||||
This lower level does not have an opinion on what the message contains. It is only concerned
|
||||
about a blob payload and its signature.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
import Minisome.Utils.TypedStruct
|
||||
|
@ -6,12 +13,21 @@ defmodule Minisome.API.Client.Message do
|
|||
alias Minisome.Crypto.SSH
|
||||
|
||||
defmodule RawMessage do
|
||||
deftypedstruct(%{
|
||||
data: binary(),
|
||||
signature: SSH.Signature.t()
|
||||
})
|
||||
deftypedstruct(
|
||||
%{
|
||||
data: binary(),
|
||||
signature: SSH.Signature.t()
|
||||
},
|
||||
"Low level message and its signature."
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parse a binary into a RawMessage.
|
||||
|
||||
The binary must have the OpenSSH style signature right at the start, then the base 64 encoded
|
||||
payload after that.
|
||||
"""
|
||||
@spec parse_message(binary()) :: RawMessage.t() | nil
|
||||
def parse_message(data) do
|
||||
with {:split, [sig_part, data_part]} <- {:split, :binary.split(data, SSH.openssh_sig_end())},
|
||||
|
@ -37,6 +53,22 @@ defmodule Minisome.API.Client.Message do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encode given data using the key pair into a raw message.
|
||||
"""
|
||||
@spec encode_message(binary(), SSH.KeyPair.t()) :: RawMessage.t()
|
||||
def encode_message(data, keys) do
|
||||
signature = SSH.sign(data, keys)
|
||||
|
||||
%RawMessage{
|
||||
data: data,
|
||||
signature: signature
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Format the raw message into a message string.
|
||||
"""
|
||||
@spec format_message(RawMessage.t()) :: String.t()
|
||||
def format_message(%RawMessage{} = msg) do
|
||||
"""
|
||||
|
|
37
lib/api/client/message_libraries/auth/auth.ex
Normal file
37
lib/api/client/message_libraries/auth/auth.ex
Normal file
|
@ -0,0 +1,37 @@
|
|||
defmodule Minisome.API.Client.MessageLibraries.Auth do
|
||||
alias Minisome.API.Client.MessageLibraries.GenericLibrary
|
||||
alias Minisome.API.Client.ProtocolMessage
|
||||
alias Minisome.API.Client.MessageLibraries.Auth.KeyInfoMessage
|
||||
|
||||
@behaviour GenericLibrary
|
||||
|
||||
@message_mapping %{
|
||||
KeyInfoMessage.message_type() => KeyInfoMessage
|
||||
}
|
||||
|
||||
@impl GenericLibrary
|
||||
@spec library_name() :: String.t()
|
||||
def library_name(), do: "auth"
|
||||
|
||||
@impl GenericLibrary
|
||||
@spec encoded_version() :: Version.t()
|
||||
def encoded_version(),
|
||||
do: %Version{
|
||||
major: 1,
|
||||
minor: 0,
|
||||
patch: 0
|
||||
}
|
||||
|
||||
@impl GenericLibrary
|
||||
@spec accepted_version() :: Version.t()
|
||||
def accepted_version(), do: encoded_version()
|
||||
|
||||
@impl GenericLibrary
|
||||
@spec decode(ProtocolMessage.t()) :: {:ok, any()} | {:error, atom()}
|
||||
def decode(%ProtocolMessage{} = message) do
|
||||
case Map.get(@message_mapping, message.type) do
|
||||
nil -> {:error, :unknown_type}
|
||||
module -> module.decode(message)
|
||||
end
|
||||
end
|
||||
end
|
74
lib/api/client/message_libraries/auth/key_info.ex
Normal file
74
lib/api/client/message_libraries/auth/key_info.ex
Normal file
|
@ -0,0 +1,74 @@
|
|||
defmodule Minisome.API.Client.MessageLibraries.Auth.KeyInfoMessage do
|
||||
import Minisome.Utils.TypedStruct
|
||||
|
||||
alias Minisome.API.Client.MessageLibraries.GenericMessage
|
||||
alias Minisome.Crypto.SSH
|
||||
|
||||
@behaviour GenericMessage
|
||||
|
||||
defmodule Key do
|
||||
deftypedstruct(%{
|
||||
key: SSH.PublicKey.t(),
|
||||
expires: DateTime.t()
|
||||
})
|
||||
end
|
||||
|
||||
deftypedstruct(%{
|
||||
keys: [Key.t()]
|
||||
})
|
||||
|
||||
@impl GenericMessage
|
||||
@spec message_type() :: String.t()
|
||||
def message_type(), do: "key_info"
|
||||
|
||||
@impl GenericMessage
|
||||
@spec encode(t()) :: {String.t(), map()}
|
||||
def encode(%__MODULE__{} = message) do
|
||||
keys = Enum.map(message.keys, &Map.from_struct/1)
|
||||
|
||||
GenericMessage.encode(__MODULE__, %{
|
||||
"keys" => keys
|
||||
})
|
||||
end
|
||||
|
||||
@impl GenericMessage
|
||||
@spec decode(any()) :: {:ok, t()} | :error
|
||||
def decode(payload) do
|
||||
with keys when is_list(keys) <- payload,
|
||||
true <- Enum.all?(keys, &payload_key_match?/1),
|
||||
{:ok, parsed_keys} <- parse_keys(keys) do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
keys: parsed_keys
|
||||
}}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@spec payload_key_match?(any()) :: boolean()
|
||||
defp payload_key_match?(key_data)
|
||||
|
||||
defp payload_key_match?(%{"key" => key, "expires" => expires})
|
||||
when is_binary(key) and is_struct(expires, DateTime),
|
||||
do: true
|
||||
|
||||
defp payload_key_match?(_), do: false
|
||||
|
||||
@spec parse_keys([map()]) :: {:ok, [Key.t()]} | :error
|
||||
defp parse_keys(keys) do
|
||||
reduced =
|
||||
Enum.reduce_while(keys, [], fn %{"key" => key, "expires" => expires}, acc ->
|
||||
case SSH.load_public_key(key) do
|
||||
{:ok, parsed_key} -> {:cont, [%Key{key: parsed_key, expires: expires} | acc]}
|
||||
:error -> {:halt, :error}
|
||||
end
|
||||
end)
|
||||
|
||||
if reduced == :error do
|
||||
:error
|
||||
else
|
||||
{:ok, reduced}
|
||||
end
|
||||
end
|
||||
end
|
21
lib/api/client/message_libraries/generic_library.ex
Normal file
21
lib/api/client/message_libraries/generic_library.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule Minisome.API.Client.MessageLibraries.GenericLibrary do
|
||||
@callback library_name() :: String.t()
|
||||
@callback encoded_version() :: Version.t()
|
||||
@callback accepted_version() :: Version.t()
|
||||
@callback decode(Minisome.API.Client.TypedMessage.t()) :: any()
|
||||
|
||||
alias Minisome.API.Client.ProtocolMessage
|
||||
|
||||
def dispatch(message) do
|
||||
end
|
||||
|
||||
@spec encode(module(), {String.t(), any()}) :: ProtocolMessage.t()
|
||||
def encode(module, {type, payload}) do
|
||||
%ProtocolMessage{
|
||||
library: module,
|
||||
type: type,
|
||||
version: module.encoded_version(),
|
||||
payload: payload
|
||||
}
|
||||
end
|
||||
end
|
10
lib/api/client/message_libraries/generic_message.ex
Normal file
10
lib/api/client/message_libraries/generic_message.ex
Normal file
|
@ -0,0 +1,10 @@
|
|||
defmodule Minisome.API.Client.MessageLibraries.GenericMessage do
|
||||
@callback message_type() :: String.t()
|
||||
@callback encode(any()) :: {String.t(), any()}
|
||||
@callback decode(any()) :: {:ok, any()} | :error
|
||||
|
||||
@spec encode(module(), any()) :: {String.t(), any()}
|
||||
def encode(module, payload) do
|
||||
{module.message_type, payload}
|
||||
end
|
||||
end
|
70
lib/api/client/protocol_handler.ex
Normal file
70
lib/api/client/protocol_handler.ex
Normal file
|
@ -0,0 +1,70 @@
|
|||
defmodule Minisome.API.Client.ProtocolHandler do
|
||||
@moduledoc """
|
||||
Handler for protocol messages to/from raw messages.
|
||||
"""
|
||||
|
||||
import Minisome.Utils.WithHelper
|
||||
|
||||
alias Minisome.API.Client.Message
|
||||
alias Minisome.API.Client.ProtocolMessage
|
||||
alias Minisome.API.Client.MessageLibraries.Auth
|
||||
|
||||
@library_mapping %{
|
||||
Auth.library_name() => Auth
|
||||
}
|
||||
|
||||
@doc """
|
||||
Decode raw message into a protocol message.
|
||||
"""
|
||||
@spec decode_raw(Message.RawMessage.t()) :: {:ok, ProtocolMessage.t()} | {:error, any()}
|
||||
def decode_raw(%Message.RawMessage{} = raw) do
|
||||
with {:ok, data} = op(:unpack, Msgpax.unpack(raw.data)),
|
||||
%{library: library, type: type, version: version, payload: payload} <-
|
||||
op(:destructure, data),
|
||||
library_module <- op(:library_type, Map.get(@library_mapping, library), :permissive),
|
||||
{:version, [major_version, minor_version, patch_version]}
|
||||
when is_integer(major_version) and is_integer(minor_version) and
|
||||
is_integer(patch_version) <- {:version, version},
|
||||
true <- op(:type_type, is_binary(type)) do
|
||||
%ProtocolMessage{
|
||||
library: library_module,
|
||||
type: type,
|
||||
version: %Version{
|
||||
major: major_version,
|
||||
minor: minor_version,
|
||||
patch: patch_version
|
||||
},
|
||||
payload: payload
|
||||
}
|
||||
else
|
||||
{:unpack, err} -> {:error, {:invalid_msgpack, err}}
|
||||
{:destructure, _} -> {:error, :invalid_payload}
|
||||
{:library_type, _} -> {:error, :invalid_library_type}
|
||||
{:version, _} -> {:error, :invalid_version}
|
||||
{:type_type, _} -> {:error, :invalid_type_type}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Dispatch the protocol message to its library for decoding.
|
||||
"""
|
||||
@spec handle_message(ProtocolMessage.t()) :: any()
|
||||
def handle_message(%ProtocolMessage{} = message) do
|
||||
message.library.decode(message)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encode protocol message into payload binary that can be signed.
|
||||
"""
|
||||
@spec encode_message(ProtocolMessage.t()) :: {:ok, binary()} | {:error, any()}
|
||||
def encode_message(%ProtocolMessage{} = message) do
|
||||
data = %{
|
||||
library: message.library.library_name(),
|
||||
type: message.type,
|
||||
version: [message.version.major, message.version.minor, message.version.patch],
|
||||
payload: message.payload
|
||||
}
|
||||
|
||||
Msgpax.pack(data, iodata: false)
|
||||
end
|
||||
end
|
35
lib/api/client/protocol_message.ex
Normal file
35
lib/api/client/protocol_message.ex
Normal file
|
@ -0,0 +1,35 @@
|
|||
defmodule Minisome.API.Client.ProtocolMessage do
|
||||
@moduledoc """
|
||||
In Minisome, the message protocol defines messages as objects with certain properties:
|
||||
|
||||
```json
|
||||
{
|
||||
"library": "auth",
|
||||
"type": "key_info",
|
||||
"version": [1, 2, 1],
|
||||
"payload": {"keys": []}
|
||||
}
|
||||
```
|
||||
|
||||
* The **library** is a coherent grouping of messages into a set that has stand-alone value. For
|
||||
example the "auth" library contains messages related to authentication of nodes.
|
||||
* The **type** is a single message type within the library.
|
||||
* The **version** is used to prevent mistakes due to changed protocol versions. Each library is
|
||||
versioned separately.
|
||||
* The **payload** is specific to the library and message type.
|
||||
"""
|
||||
|
||||
import Minisome.Utils.TypedStruct
|
||||
|
||||
deftypedstruct(
|
||||
%{
|
||||
library: module(),
|
||||
type: binary(),
|
||||
version: Version.t(),
|
||||
payload: any()
|
||||
},
|
||||
"""
|
||||
A protocol message belonging to a message library.
|
||||
"""
|
||||
)
|
||||
end
|
29
lib/api/controllers/auth_controller.ex
Normal file
29
lib/api/controllers/auth_controller.ex
Normal file
|
@ -0,0 +1,29 @@
|
|||
defmodule Minisome.API.AuthController do
|
||||
use Phoenix.Controller, namespace: Minisome.API
|
||||
import Plug.Conn
|
||||
|
||||
alias Minisome.Crypto.SSH
|
||||
alias Minisome.Storage.Auth.MyKey
|
||||
alias Minisome.API.Client.MessageLibraries.Auth
|
||||
|
||||
@spec key_info(Plug.Conn.t(), Plug.Conn.params()) :: Plug.Conn.t()
|
||||
def key_info(conn, _params) do
|
||||
keys = MyKey.get_active_keys()
|
||||
|
||||
transformed_keys =
|
||||
Enum.map(keys, fn {%SSH.KeyPair{} = key, expires} ->
|
||||
%Auth.KeyInfoMessage.Key{key: key.public, expires: expires}
|
||||
end)
|
||||
|
||||
message = %Auth.KeyInfoMessage{keys: transformed_keys}
|
||||
payload = Auth.KeyInfoMessage.encode(message)
|
||||
protocol_message = Minisome.API.Client.MessageLibraries.GenericLibrary.encode(Auth, payload)
|
||||
{:ok, packed} = Minisome.API.Client.ProtocolHandler.encode_message(protocol_message)
|
||||
raw = Minisome.API.Client.Message.encode_message(packed, keys |> List.first() |> elem(0))
|
||||
data = Minisome.API.Client.Message.format_message(raw)
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("text/minisome")
|
||||
|> send_resp(200, data)
|
||||
end
|
||||
end
|
18
lib/api/endpoint.ex
Normal file
18
lib/api/endpoint.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Minisome.API.Endpoint do
|
||||
use Phoenix.Endpoint, otp_app: :minisome
|
||||
|
||||
# Code reloading can be explicitly enabled under the
|
||||
# :code_reloader configuration of your endpoint.
|
||||
if code_reloading? do
|
||||
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
|
||||
plug Phoenix.LiveReloader
|
||||
plug Phoenix.CodeReloader
|
||||
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :minisome
|
||||
end
|
||||
|
||||
plug Plug.RequestId
|
||||
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
|
||||
|
||||
plug Plug.Head
|
||||
plug Minisome.API.Router
|
||||
end
|
13
lib/api/protocols/public_key.ex
Normal file
13
lib/api/protocols/public_key.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defimpl Msgpax.Packer, for: Minisome.Crypto.SSH.PublicKey do
|
||||
@spec pack(Minisome.Crypto.SSH.PublicKey.t()) :: iodata()
|
||||
def pack(%Minisome.Crypto.SSH.PublicKey{} = key) do
|
||||
<<
|
||||
type_len::unsigned-integer-32,
|
||||
type::binary-size(type_len),
|
||||
key_len::unsigned-integer-32,
|
||||
str_key::binary-size(key_len)
|
||||
>> = :ssh_file.encode(key.data, :ssh2_pubkey)
|
||||
|
||||
"#{type} #{str_key}"
|
||||
end
|
||||
end
|
16
lib/api/router.ex
Normal file
16
lib/api/router.ex
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule Minisome.API.Router do
|
||||
use Phoenix.Router
|
||||
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller
|
||||
|
||||
pipeline :api do
|
||||
plug :accepts, ["text"]
|
||||
end
|
||||
|
||||
scope "/", Minisome.API do
|
||||
pipe_through :api
|
||||
|
||||
get "/key-info", AuthController, :key_info
|
||||
end
|
||||
end
|
|
@ -15,7 +15,8 @@ defmodule Minisome.Application do
|
|||
# Start the PubSub system
|
||||
{Phoenix.PubSub, name: Minisome.PubSub},
|
||||
# Start the Endpoint (http/https)
|
||||
Minisome.Web.Endpoint
|
||||
Minisome.Web.Endpoint,
|
||||
Minisome.API.Endpoint
|
||||
# Start a worker by calling: Minisome.Worker.start_link(arg)
|
||||
# {Minisome.Worker, arg}
|
||||
]
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
defmodule Minisome.Crypto.SSH do
|
||||
@moduledoc """
|
||||
Implementation of SSH signing and verifying in Elixir / OTP 24.
|
||||
|
||||
NOTE: Only supports ED25519 keys.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
import Minisome.Utils.TypedStruct
|
||||
|
||||
@default_namespace "minisome"
|
||||
|
@ -17,6 +17,9 @@ defmodule Minisome.Crypto.SSH do
|
|||
@openssh_sig_end "-----END SSH SIGNATURE-----"
|
||||
@openssh_sig_line_length 76
|
||||
|
||||
@typedoc """
|
||||
Algorithm used for hashing in the signature.
|
||||
"""
|
||||
@type hash_algo :: :sha256 | :sha512
|
||||
|
||||
defmodule PrivateKey do
|
||||
|
@ -47,20 +50,43 @@ defmodule Minisome.Crypto.SSH do
|
|||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Load SSH key pair from given binary.
|
||||
|
||||
The binary should be in OpenSSH private key format. There should only be one private/public key
|
||||
pair in the binary.
|
||||
"""
|
||||
@spec load_key_pair(binary()) :: {:ok, KeyPair.t()} | :error
|
||||
def load_key_pair(data) do
|
||||
case :ssh_file.decode(data, :public_key) do
|
||||
[{privk, _priv_meta}, {pubk, _pub_meta}] ->
|
||||
[{{:ed_pri, _, _, _} = privk, _priv_meta}, {{:ed_pub, _, _} = pubk, _pub_meta}] ->
|
||||
{:ok, %KeyPair{public: %PublicKey{data: pubk}, private: %PrivateKey{data: privk}}}
|
||||
|
||||
[] ->
|
||||
:error
|
||||
|
||||
{:error, _} ->
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Load public key from given binary.
|
||||
|
||||
The binary should be in the OpenSSH public key format. There should only be one public key in
|
||||
the binary.
|
||||
"""
|
||||
@spec load_public_key(binary()) :: {:ok, PublicKey.t()} | :error
|
||||
def load_public_key(data) do
|
||||
case :ssh_file.decode(data, :public_key) do
|
||||
[{{:ed_pub, _, _} = pubk, _pub_meta}] ->
|
||||
{:ok, %PublicKey{data: pubk}}
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sign data with key pair.
|
||||
"""
|
||||
@spec sign(binary(), KeyPair.t(), binary(), hash_algo()) :: Signature.t()
|
||||
def sign(data, %KeyPair{} = keys, namespace \\ @default_namespace, hash \\ @default_hash) do
|
||||
payload = form_payload(data, namespace, hash)
|
||||
|
@ -74,6 +100,11 @@ defmodule Minisome.Crypto.SSH do
|
|||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verify signed data using the signature and public key of the signer.
|
||||
|
||||
The keys embedded in the signature are ignored.
|
||||
"""
|
||||
@spec verify(binary(), Signature.t(), PublicKey.t()) :: boolean()
|
||||
def verify(
|
||||
data,
|
||||
|
@ -84,7 +115,10 @@ defmodule Minisome.Crypto.SSH do
|
|||
:public_key.verify(payload, hash, signature, key)
|
||||
end
|
||||
|
||||
@spec format_signature(Signature.t()) :: binary()
|
||||
@doc """
|
||||
Create SSH style signature string from given signature data.
|
||||
"""
|
||||
@spec format_signature(Signature.t()) :: String.t()
|
||||
def format_signature(%Signature{
|
||||
signature: signature,
|
||||
namespace: namespace,
|
||||
|
@ -124,6 +158,9 @@ defmodule Minisome.Crypto.SSH do
|
|||
"#{@openssh_sig_end}\n"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parse signature data from OpenSSH signature format.
|
||||
"""
|
||||
@spec parse_signature(binary()) :: {:ok, Signature.t()} | {:error, atom()}
|
||||
def parse_signature(signature) do
|
||||
signature = :binary.replace(signature, "\n", "", [:global])
|
||||
|
@ -151,6 +188,9 @@ defmodule Minisome.Crypto.SSH do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Form payload for signing according to the OpenSSH signature format.
|
||||
"""
|
||||
@spec form_payload(binary(), binary(), hash_algo()) :: binary()
|
||||
def form_payload(data, namespace, hash) do
|
||||
digest = :crypto.hash(hash, data)
|
||||
|
@ -162,24 +202,42 @@ defmodule Minisome.Crypto.SSH do
|
|||
ssh_string(digest)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert given binary to OpenSSH signature "string" type, i.e. a binary blob prefixed with an
|
||||
unsigned 32 bit integer length.
|
||||
"""
|
||||
@spec ssh_string(binary()) :: <<_::32, _::_*8>>
|
||||
def ssh_string(data), do: <<byte_size(data)::unsigned-integer-32>> <> data
|
||||
|
||||
@doc """
|
||||
Get the "key type" string for given public key, i.e. "ssh-ed25519".
|
||||
|
||||
Only ED25519 keys are supported.
|
||||
"""
|
||||
@spec pub_key_to_ssh_name(:public_key.public_key()) :: String.t()
|
||||
def pub_key_to_ssh_name(key)
|
||||
|
||||
def pub_key_to_ssh_name({:ed_pub, :ed25519, _}), do: "ssh-ed25519"
|
||||
def pub_key_to_ssh_name(key), do: raise("Unknown key type #{inspect(key)}")
|
||||
|
||||
@doc """
|
||||
Extract public key data bytes from key tuple.
|
||||
"""
|
||||
@spec pub_key_extract(:public_key.public_key()) :: binary()
|
||||
def pub_key_extract(key)
|
||||
|
||||
def pub_key_extract({:ed_pub, _, key}), do: key
|
||||
def pub_key_extract(key), do: raise("Unknown key type #{inspect(key)}")
|
||||
|
||||
@doc """
|
||||
Get the OpenSSH signature start delimiter.
|
||||
"""
|
||||
@spec openssh_sig_start() :: String.t()
|
||||
def openssh_sig_start(), do: @openssh_sig_start
|
||||
|
||||
@doc """
|
||||
Get the OpenSSH signature end delimiter.
|
||||
"""
|
||||
@spec openssh_sig_end() :: String.t()
|
||||
def openssh_sig_end(), do: @openssh_sig_end
|
||||
|
||||
|
|
11
lib/storage/auth/host.ex
Normal file
11
lib/storage/auth/host.ex
Normal file
|
@ -0,0 +1,11 @@
|
|||
defmodule Minisome.Storage.Auth.Host do
|
||||
use Ecto.Schema
|
||||
import Minisome.Storage.TypedSchema
|
||||
|
||||
deftypedschema "hosts" do
|
||||
field(:hostname, :string, String.t())
|
||||
field(:port, :integer, pos_integer())
|
||||
|
||||
has_many(:keys, Minisome.Storage.Auth.Key, [Minisome.Storage.Auth.Key.t()])
|
||||
end
|
||||
end
|
12
lib/storage/auth/key.ex
Normal file
12
lib/storage/auth/key.ex
Normal file
|
@ -0,0 +1,12 @@
|
|||
defmodule Minisome.Storage.Auth.Key do
|
||||
use Ecto.Schema
|
||||
import Ecto.Query, only: [from: 2]
|
||||
import Minisome.Storage.TypedSchema
|
||||
|
||||
deftypedschema "keys" do
|
||||
field(:key_blob, :string, String.t())
|
||||
field(:expires, :utc_datetime, DateTime.t())
|
||||
|
||||
belongs_to(:host, Minisome.Storage.Auth.Host, Minisome.Storage.Auth.Host.t())
|
||||
end
|
||||
end
|
22
lib/storage/auth/my_key.ex
Normal file
22
lib/storage/auth/my_key.ex
Normal file
|
@ -0,0 +1,22 @@
|
|||
defmodule Minisome.Storage.Auth.MyKey do
|
||||
use Ecto.Schema
|
||||
import Ecto.Query, only: [from: 2]
|
||||
import Minisome.Storage.TypedSchema
|
||||
|
||||
alias Minisome.Crypto.SSH
|
||||
|
||||
deftypedschema "my_keys" do
|
||||
field(:key_blob, :string, String.t())
|
||||
field(:expires, :utc_datetime, DateTime.t())
|
||||
end
|
||||
|
||||
@spec get_active_keys(Ecto.Repo.t()) :: [{SSH.KeyPair.t(), DateTime.t()}]
|
||||
def get_active_keys(repo \\ Minisome.Storage.Repo) do
|
||||
from(k in __MODULE__, where: k.expires >= ^DateTime.utc_now())
|
||||
|> repo.all()
|
||||
|> Enum.map(fn %__MODULE__{} = key ->
|
||||
{:ok, pair} = SSH.load_key_pair(key.key_blob)
|
||||
{pair, key.expires}
|
||||
end)
|
||||
end
|
||||
end
|
93
lib/storage/typed_schema.ex
Normal file
93
lib/storage/typed_schema.ex
Normal file
|
@ -0,0 +1,93 @@
|
|||
defmodule Minisome.Storage.TypedSchema do
|
||||
@doc """
|
||||
Define an Ecto schema with an associated `@type t` specification.
|
||||
|
||||
Works the same as normal Ecto schemas, but third argument of each field is the typespec to use
|
||||
for that field. Typespec for `timestamps` is automatically generated and cannot be specified.
|
||||
Supported Ecto macros are `field`, `belongs_to`, `has_many`, `has_one`. `many_to_many` is not
|
||||
supported.
|
||||
|
||||
Note: For `has_many`, remember to specify the typespec as a list.
|
||||
|
||||
Does not work for embedded schemas.
|
||||
"""
|
||||
defmacro deftypedschema(table, do: fields) do
|
||||
fields =
|
||||
case fields do
|
||||
{:__block__, _meta, flist} -> flist
|
||||
field -> [field]
|
||||
end
|
||||
|
||||
fielddatas = for field <- fields, do: parse_spec(field)
|
||||
|
||||
typespecs =
|
||||
Enum.reduce(fielddatas, [], fn
|
||||
%{field: :timestamps, fieldspec: {:timestamps, _, [opts]}}, acc ->
|
||||
acc =
|
||||
if Keyword.get(opts, :updated_at, true) != false do
|
||||
[{:updated_at, quote(do: DateTime.t())} | acc]
|
||||
else
|
||||
acc
|
||||
end
|
||||
|
||||
if Keyword.get(opts, :inserted_at, true) != false do
|
||||
[{:inserted_at, quote(do: DateTime.t())} | acc]
|
||||
else
|
||||
acc
|
||||
end
|
||||
|
||||
%{field: field, typespec: typespec, func: func}, acc ->
|
||||
acc = [{field, typespec} | acc]
|
||||
|
||||
if func == :belongs_to do
|
||||
# If given spec includes nil, add nil to ID spec too
|
||||
spec =
|
||||
case typespec do
|
||||
{:|, _, [x, y]} when is_nil(x) or is_nil(y) -> quote(do: pos_integer() | nil)
|
||||
_ -> quote(do: pos_integer())
|
||||
end
|
||||
|
||||
[{String.to_atom("#{field}_id"), spec} | acc]
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|> Enum.reverse()
|
||||
|
||||
fieldspecs = Enum.map(fielddatas, & &1.fieldspec)
|
||||
|
||||
quote do
|
||||
use Ecto.Schema
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
unquote_splicing(typespecs),
|
||||
__meta__: Ecto.Schema.Metadata.t(),
|
||||
id: pos_integer()
|
||||
}
|
||||
|
||||
schema unquote(table) do
|
||||
(unquote_splicing(fieldspecs))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_spec(ast)
|
||||
|
||||
defp parse_spec({:timestamps, _meta, _args} = ast) do
|
||||
%{
|
||||
field: :timestamps,
|
||||
func: :timestamps,
|
||||
fieldspec: ast,
|
||||
typespec: nil
|
||||
}
|
||||
end
|
||||
|
||||
defp parse_spec({func, meta, [field, type, typespec | rest]}) do
|
||||
%{
|
||||
field: field,
|
||||
func: func,
|
||||
fieldspec: {func, meta, [field, type | rest]},
|
||||
typespec: typespec
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
defmodule Minisome.Utils.TypedStruct do
|
||||
@doc """
|
||||
@doc ~S/
|
||||
Create typed struct with a type, default values, and enforced keys.
|
||||
|
||||
Input should be a map where the key names are names of the struct keys and values are the
|
||||
|
@ -24,10 +24,12 @@ defmodule Minisome.Utils.TypedStruct do
|
|||
|
||||
# Non-enforced field with default value
|
||||
baz: {any(), ""}
|
||||
})
|
||||
}, """
|
||||
Optional typedoc for the struct type `t`.
|
||||
""")
|
||||
```
|
||||
"""
|
||||
defmacro deftypedstruct(fields) do
|
||||
/
|
||||
defmacro deftypedstruct(fields, typedoc \\ "") do
|
||||
fields_list =
|
||||
case fields do
|
||||
{:%{}, _, flist} -> flist
|
||||
|
@ -63,6 +65,7 @@ defmodule Minisome.Utils.TypedStruct do
|
|||
end)
|
||||
|
||||
quote do
|
||||
@typedoc unquote(typedoc)
|
||||
@type t :: %__MODULE__{unquote_splicing(field_specs)}
|
||||
@enforce_keys unquote(enforced_list)
|
||||
defstruct unquote(field_vals)
|
||||
|
|
18
lib/utils/with_helper.ex
Normal file
18
lib/utils/with_helper.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Minisome.Utils.WithHelper do
|
||||
@spec op(atom(), any(), :strict | :permissive) :: any()
|
||||
def op(label, thing, mode \\ :strict) do
|
||||
if mode == :permissive do
|
||||
opt_permissive(label, thing)
|
||||
else
|
||||
op_strict(label, thing)
|
||||
end
|
||||
end
|
||||
|
||||
defp opt_permissive(label, err) when err in [:error, false, nil], do: {label, err}
|
||||
defp opt_permissive(label, {:error, _} = err), do: {label, err}
|
||||
defp opt_permissive(_label, val), do: val
|
||||
|
||||
defp op_strict(_label, val) when val in [:ok, true], do: val
|
||||
defp op_strict(_label, {:ok, _} = success), do: success
|
||||
defp op_strict(label, other), do: {label, other}
|
||||
end
|
4
mix.exs
4
mix.exs
|
@ -47,7 +47,9 @@ defmodule Minisome.MixProject do
|
|||
{:telemetry_poller, "~> 1.0"},
|
||||
{:gettext, "~> 0.18"},
|
||||
{:jason, "~> 1.2"},
|
||||
{:plug_cowboy, "~> 2.5"}
|
||||
{:plug_cowboy, "~> 2.5"},
|
||||
{:msgpax, "~> 2.3"},
|
||||
{:ex_doc, "~> 0.28.0", runtime: false}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
7
mix.lock
7
mix.lock
|
@ -6,18 +6,25 @@
|
|||
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
|
||||
"db_connection": {:hex, :db_connection, "2.4.1", "6411f6e23f1a8b68a82fa3a36366d4881f21f47fc79a9efb8c615e62050219da", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ea36d226ec5999781a9a8ad64e5d8c4454ecedc7a4d643e4832bf08efca01f00"},
|
||||
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"},
|
||||
"ecto": {:hex, :ecto, "3.7.1", "a20598862351b29f80f285b21ec5297da1181c0442687f9b8329f0445d228892", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d36e5b39fc479e654cffd4dbe1865d9716e4a9b6311faff799b6f90ab81b8638"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.7.2", "55c60aa3a06168912abf145c6df38b0295c34118c3624cf7a6977cd6ce043081", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c218ea62f305dcaef0b915fb56583195e7b91c91dcfb006ba1f669bfacbff2a"},
|
||||
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.7.3", "119e5142f23b9868ac17449cd945557897c18f30c0b39e3eb96659729d38310e", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.9", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "5a149b96e6c2e2ebcca60d23cbcf89130f7fbbcdba62956a70aa3d6d002a8e54"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
|
||||
"esbuild": {:hex, :esbuild, "0.4.0", "9f17db148aead4cf1e6e6a584214357287a93407b5fb51a031f122b61385d4c2", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "b61e4e6b92ffe45e4ee4755a22de6211a67c67987dc02afb35a425a0add1d447"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.28.0", "7eaf526dd8c80ae8c04d52ac8801594426ae322b52a6156cd038f30bafa8226f", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e55cdadf69a5d1f4cfd8477122ebac5e1fadd433a8c1022dafc5025e48db0131"},
|
||||
"exqlite": {:hex, :exqlite, "0.9.3", "57c80e742584dc4486d717681956d4152c7d03fb34ddbfb269844b504824528d", [:make, :mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "5108f84bcc91fd7ae5b1b247e2be3860e449de5f8383ccaa1454278ffa1fc509"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"floki": {:hex, :floki, "0.32.0", "f915dc15258bc997d49be1f5ef7d3992f8834d6f5695270acad17b41f5bcc8e2", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "1c5a91cae1fd8931c26a4826b5e2372c284813904c8bacb468b5de39c7ececbd"},
|
||||
"gettext": {:hex, :gettext, "0.19.1", "564953fd21f29358e68b91634799d9d26989f8d039d7512622efb3c3b1c97892", [:mix], [], "hexpm", "10c656c0912b8299adba9b061c06947511e3f109ab0d18b44a866a4498e77222"},
|
||||
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
|
||||
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
|
||||
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
|
||||
"mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"},
|
||||
"msgpax": {:hex, :msgpax, "2.3.0", "14f52ad249a3f77b5e2d59f6143e6c18a6e74f34666989e22bac0a465f9835cc", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "65c36846a62ed5615baf7d7d47babb6541313a6c0b6d2ff19354bd518f52df7e"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.2.2", "b99ca56bbce410e9d5ee4f9155a212e942e224e259c7ebbf8f2c86ac21d4fa3c", [:mix], [], "hexpm", "98d51bd64d5f6a2a9c6bb7586ee8129e27dfaab1140b5a4753f24dac0ba27d2f"},
|
||||
"phoenix": {:hex, :phoenix, "1.6.6", "281c8ce8dccc9f60607346b72cdfc597c3dde134dd9df28dff08282f0b751754", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "807bd646e64cd9dc83db016199715faba72758e6db1de0707eef0a2da4924364"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
|
||||
|
|
15
priv/repo/migrations/20220212215803_add_hosts.exs
Normal file
15
priv/repo/migrations/20220212215803_add_hosts.exs
Normal file
|
@ -0,0 +1,15 @@
|
|||
defmodule Minisome.Storage.Repo.Migrations.AddHosts do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:hosts) do
|
||||
add(:hostname, :text, null: false)
|
||||
add(:port, :integer, null: false)
|
||||
end
|
||||
|
||||
create table(:keys) do
|
||||
add(:key_blob, :text, null: false)
|
||||
add(:expires, :utc_datetime, null: false)
|
||||
end
|
||||
end
|
||||
end
|
10
priv/repo/migrations/20220212231803_add_my_keys.exs
Normal file
10
priv/repo/migrations/20220212231803_add_my_keys.exs
Normal file
|
@ -0,0 +1,10 @@
|
|||
defmodule Minisome.Storage.Repo.Migrations.AddMyKeys do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:my_keys) do
|
||||
add(:key_blob, :text, null: false)
|
||||
add(:expires, :utc_datetime, null: false)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue