defmodule Mix.Tasks.NervesHub.User do use Mix.Task import Mix.NervesHubCLI.Utils alias NervesHubCLI.{User, Config} alias Mix.NervesHubCLI.Shell alias X509.{Certificate, PrivateKey, CSR} @shortdoc "Manages your NervesHub user account" @moduledoc """ Manage your NervesHub user account. Users are authenticated to the NervesHub API by supplying a valid client certificate with every request. User certificates can be generated and managed on https://www.nerves-hub.org/account/certificates NervesHub will look for the following files in the location of $NERVES_HUB_HOME ca.pem: A file that contains all known NervesHub Certificate Authority certificates needed to authenticate. user.pem: A signed user account certificate. user-key.pem: The user account certificate private key. ## whoami mix nerves_hub.user whoami ## register mix nerves_hub.user register ## auth mix nerves_hub.user auth ## deauth mix nerves_hub.user deauth ## cert export mix nerves_hub.user cert export ### Command-line options * `--path` - (Optional) A local location for exporting certificate. """ @switches [path: :string] @data_dir "nerves-hub" def run(args) do Application.ensure_all_started(:nerves_hub_cli) {opts, args} = OptionParser.parse!(args, strict: @switches) show_api_endpoint() case args do ["whoami"] -> whoami() ["register"] -> register() ["auth"] -> auth() ["deauth"] -> deauth() ["cert", "export"] -> cert_export(opts) _ -> render_help() end end @spec render_help() :: no_return() def render_help() do Shell.raise(""" Invalid arguments to `mix nerves_hub.user`. Usage: mix nerves_hub.user whoami mix nerves_hub.user register mix nerves_hub.user auth mix nerves_hub.user deauth mix nerves_hub.user cert export Run `mix help nerves_hub.user` for more information. """) end def whoami do auth = Shell.request_auth() case NervesHubCore.User.me(auth) do {:ok, %{"data" => data}} -> %{"username" => username, "email" => email} = data Shell.info(""" username: #{username} email: #{email} """) error -> Shell.render_error(error) end end def register() do email = Shell.prompt("Email address:") |> String.trim() username = Shell.prompt("Username:") |> String.trim() password = Shell.password_get("NervesHub password:") |> String.trim() confirm = Shell.password_get("NervesHub password (confirm):") |> String.trim() unless String.equivalent?(password, confirm) do Mix.raise("Entered passwords do not match") end Shell.info("Registering account...") register(username, email, password) end def auth() do email = Shell.prompt("Email address:") |> String.trim() password = Shell.password_get("NervesHub password:") |> String.trim() Shell.info("Authenticating...") case NervesHubCore.User.auth(email, password) do {:ok, %{"data" => %{"email" => ^email, "username" => username}}} -> Shell.info("Success") generate_certificate(username, email, password) {:error, %{"errors" => errors}} -> Shell.error("Account authentication failed \n") Shell.render_error(errors) error -> Shell.render_error(error) end end def deauth() do if Shell.yes?("Deauthorize the current user?") do User.deauth() end end def cert_export(opts) do path = opts[:path] || Path.join(File.cwd!(), @data_dir) password = Shell.password_get("Local user password:") with :ok <- File.mkdir_p(path), {:ok, %{key: key, cert: cert}} <- User.auth(password), key_pem <- PrivateKey.to_pem(key), cert_pem <- Certificate.to_pem(cert), filename <- certs_tar_file_name(path), {:ok, tar} <- :erl_tar.open(to_charlist(filename), [:write, :compressed]), :ok <- :erl_tar.add(tar, {'cert.pem', cert_pem}, []), :ok <- :erl_tar.add(tar, {'key.pem', key_pem}, []), :ok <- :erl_tar.close(tar) do Shell.info("User certs exported to: #{filename}") else error -> Shell.render_error(error) end end defp certs_tar_file_name(path), do: Path.join(path, "nerves_hub-certs.tar.gz") defp register(username, email, account_password) do case NervesHubCore.User.register(username, email, account_password) do {:ok, %{"data" => %{"email" => ^email, "username" => ^username}}} -> Shell.info("Account created") generate_certificate(username, email, account_password) {:error, %{"errors" => errors}} -> Shell.error("Account creation failed \n") Shell.render_error(errors) error -> Shell.render_error(error) end end defp generate_certificate(username, email, account_password) do Shell.info("") Shell.info("NervesHub uses client-side SSL certificates to authenticate CLI requests.") Shell.info("") Shell.info("The next step will create an SSL certificate and store it in your ") Shell.info("'~/.nerves-hub' directory. A password is required to protect it. This password") Shell.info("does not need to be your NervesHub password. It will never be sent to NervesHub") Shell.info("or any other computer. If you lose it, you will need to run") Shell.info("'mix nerves_hub.user auth' and create a new certificate.") Shell.info("") local_password = Shell.password_get("Please enter a local password:") key = PrivateKey.new_ec(:secp256r1) pem_key = PrivateKey.to_pem(key) csr = CSR.new(key, "/O=#{username}") pem_csr = CSR.to_pem(csr) with safe_csr <- Base.encode64(pem_csr), description <- NervesHubCLI.default_description(), {:ok, %{"data" => %{"cert" => pem_cert}}} <- NervesHubCore.User.sign(email, account_password, safe_csr, description), :ok <- User.save_certs(pem_cert, pem_key, local_password), :ok <- Config.put(:email, email), :ok <- Config.put(:org, username) do Shell.info("Certificate created successfully.") else error -> User.deauth() Shell.render_error(error) end end end