defmodule DaProductApp.TerminalManagement.KeysConfigService do @moduledoc """ Handles RKI key generation and storage for device key loading. Flow: 1. Call getRki endpoint to fetch RKI key for device 2. Create keys.json from response with RKI key, KCV, and configuration 3. Store file in /ota/{serial_number}/keys.json 4. Return download URL for MQTT command Keys Configuration Structure: { "rki_key": "XA9A9A23CD841F63D20D1B17F24BED2CF", "rki_kcv": "CB9DEA", "kek_kcv": "112233", "slot_number": 1, "serial_number": "1234567890", "model": "MF919", "created_at": "2025-01-31T15:00:00Z" } """ require Logger alias DaProductApp.Repo alias DaProductApp.TerminalManagement.TmsTerminal @rki_endpoint Application.compile_env(:da_product_app, :rki_endpoint, "http://localhost:8300/api/v1/getRki") @ota_base_url Application.compile_env(:da_product_app, :ota_base_url, "http://demo.ctrmv.com/ota") @ota_storage_path Application.compile_env(:da_product_app, :ota_storage_path, "priv/ota") @rki_timeout Application.compile_env(:da_product_app, :rki_timeout, 10_000) @doc """ Generates and stores keys.json file from RKI endpoint. Returns {:ok, download_url} or {:error, reason} """ def generate_and_store_keys_file(serial_number, model) when is_binary(serial_number) and is_binary(model) do Logger.info("KeysConfigService: Generating keys.json for #{serial_number} (#{model})") with terminal when not is_nil(terminal) <- get_terminal(serial_number), {:ok, rki_data} <- fetch_rki_key(serial_number, model, terminal), {:ok, keys_json} <- create_keys_json(rki_data, terminal), {:ok, file_path} <- store_keys_file(serial_number, keys_json), download_url <- build_download_url(serial_number) do Logger.info("KeysConfigService: Successfully stored keys.json for #{serial_number} at #{file_path}") {:ok, download_url, file_path} else nil -> Logger.error("KeysConfigService: Terminal not found for #{serial_number}") {:error, "Terminal not found"} {:error, reason} -> Logger.error("KeysConfigService: Failed to generate keys for #{serial_number}: #{inspect(reason)}") {:error, reason} end end @doc """ Retrieves stored keys.json content for a device. Returns {:ok, keys_map} or {:error, reason} """ def get_keys_file(serial_number) when is_binary(serial_number) do file_path = Path.join(@ota_storage_path, serial_number) file_path = Path.join(file_path, "keys.json") case File.read(file_path) do {:ok, content} -> case Jason.decode(content) do {:ok, keys_map} -> {:ok, keys_map} {:error, reason} -> {:error, "Failed to parse keys.json: #{inspect(reason)}"} end {:error, reason} -> {:error, "Keys file not found: #{inspect(reason)}"} end end # Private Functions defp get_terminal(serial_number) do Repo.get_by(TmsTerminal, serial_number: serial_number) end defp fetch_rki_key(serial_number, model, terminal) do # Extract KEK KCV from terminal or use default kek_kcv = terminal.kek_kcv || "112233" slot_number = terminal.slot_number || 1 request_body = %{ "serialNumber" => serial_number, "model" => model, "kekKcv" => kek_kcv, "slotNumber" => slot_number } Logger.info("KeysConfigService daProduct direct: Calling getRki endpoint for #{serial_number} with params: #{inspect(request_body)}") case make_http_request(@rki_endpoint, request_body) do {:ok, rki_response} -> Logger.info("KeysConfigService: RKI response received: #{inspect(rki_response)}") {:ok, rki_response} {:error, reason} -> Logger.error("KeysConfigService: Failed to fetch RKI key: #{inspect(reason)}") {:error, "Failed to fetch RKI key: #{inspect(reason)}"} end end defp make_http_request(url, body) do try do case Req.post(url, json: body, receive_timeout: @rki_timeout) do {:ok, response} -> case response.status do 200 -> {:ok, response.body} status -> Logger.error("KeysConfigService: getRki returned status #{status}: #{inspect(response.body)}") {:error, "getRki endpoint returned status #{status}"} end {:error, reason} -> {:error, inspect(reason)} end rescue e -> Logger.error("KeysConfigService: HTTP request failed with exception: #{inspect(e)}") {:error, "HTTP request failed: #{inspect(e)}"} end end defp create_keys_json(rki_data, terminal) when is_map(rki_data) and is_map(terminal) do # Keep KCV and other config for later management and reference keys_json = %{ "rki_key" => Map.get(rki_data, "rkiKey"), "rki_kcv" => Map.get(rki_data, "rkiKcv"), "kek_kcv" => terminal.kek_kcv || "112233", "slot_number" => Map.get(rki_data, "slotNumber", terminal.slot_number || 1), "serial_number" => Map.get(rki_data, "serialNumber"), "model" => terminal.model, "vendor" => terminal.vendor, "created_at" => DateTime.utc_now() |> DateTime.to_iso8601(), "expires_at" => calculate_expiry_date() } |> Enum.reject(fn {_k, v} -> is_nil(v) end) |> Map.new() Logger.info("KeysConfigService: Generated keys.json content for #{terminal.serial_number}") {:ok, keys_json} end defp create_keys_json(rki_data, _terminal) do Logger.error("KeysConfigService: Invalid RKI data format: #{inspect(rki_data)}") {:error, "Invalid RKI response format"} end defp store_keys_file(serial_number, keys_json) do # Create directory structure: /ota/{serial_number}/ ota_dir = Path.join(@ota_storage_path, serial_number) case File.mkdir_p(ota_dir) do :ok -> file_path = Path.join(ota_dir, "keys.json") json_content = Jason.encode!(keys_json, pretty: true) case File.write(file_path, json_content) do :ok -> Logger.info("KeysConfigService: Stored keys.json at #{file_path}") {:ok, file_path} {:error, reason} -> Logger.error("KeysConfigService: Failed to write keys.json: #{inspect(reason)}") {:error, "Failed to store keys.json: #{inspect(reason)}"} end {:error, reason} -> Logger.error("KeysConfigService: Failed to create OTA directory: #{inspect(reason)}") {:error, "Failed to create OTA directory: #{inspect(reason)}"} end end defp build_download_url(serial_number) do Path.join(@ota_base_url, "#{serial_number}/keys.json") end defp calculate_expiry_date do DateTime.utc_now() |> DateTime.add(365 * 24 * 60 * 60, :second) # 1 year validity |> DateTime.to_iso8601() end end