2.3 KiB · text 5af55d9
defmodule GitGud.SshKeys.SshKey do
use Ecto.Schema
import Ecto.Changeset
alias GitGud.Accounts.User
# Algorithms we accept; rejects DSA (deprecated) and anything exotic.
@valid_algos ~w(ssh-ed25519 ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521)
schema "user_ssh_keys" do
field :title, :string
field :key, :string
field :fingerprint, :string
field :last_used_at, :utc_datetime
belongs_to :user, User
timestamps(type: :utc_datetime)
end
@doc """
Changeset for adding a key. Validates the algorithm prefix, b64 body,
and computes the SHA256 fingerprint.
"""
def changeset(key, attrs) do
key
|> cast(attrs, [:title, :key])
|> validate_required([:title, :key])
|> validate_length(:title, max: 100)
|> update_change(:key, &normalize_key/1)
|> validate_key_format()
|> compute_fingerprint()
|> unique_constraint(:fingerprint)
end
defp normalize_key(nil), do: nil
defp normalize_key(k), do: String.trim(k)
defp validate_key_format(changeset) do
case get_field(changeset, :key) do
nil ->
changeset
key ->
case String.split(key, " ", parts: 3) do
[algo, b64 | _] when algo in @valid_algos ->
case Base.decode64(b64) do
{:ok, _bytes} -> changeset
:error -> add_error(changeset, :key, "invalid base64 in key body")
end
[algo, _] ->
add_error(changeset, :key, "unsupported algorithm: #{algo}")
_ ->
add_error(changeset, :key, "expected `<algo> <base64> [<comment>]`")
end
end
end
defp compute_fingerprint(changeset) do
case {get_field(changeset, :key), changeset.valid?} do
{key, true} when is_binary(key) ->
case String.split(key, " ", parts: 3) do
[_algo, b64 | _] ->
case Base.decode64(b64) do
{:ok, bytes} ->
fp =
"SHA256:" <>
(bytes |> then(&:crypto.hash(:sha256, &1)) |> Base.encode64(padding: false))
put_change(changeset, :fingerprint, fp)
:error ->
changeset
end
_ ->
changeset
end
_ ->
changeset
end
end
def valid_algorithms, do: @valid_algos
end