defmodule SnmpKit.SnmpLib.Security do @moduledoc """ SNMPv3 Security Framework - Phase 5.1A Implementation Provides comprehensive SNMPv3 User Security Model (USM) implementation including authentication and privacy protocols for secure SNMP communications. ## Features - **User Security Model (USM)** with complete RFC 3414 compliance - **Authentication Protocols**: MD5, SHA-1, SHA-256, SHA-384, SHA-512 - **Privacy Protocols**: DES, AES-128, AES-192, AES-256 - **Key Derivation**: Password-based and localized key generation - **Security Parameters**: Boot counter, time synchronization, message validation - **Error Handling**: Comprehensive security error classification and recovery ## Architecture The security framework is built on a modular architecture: - `SnmpKit.SnmpLib.Security.USM` - User Security Model implementation - `SnmpKit.SnmpLib.Security.Auth` - Authentication protocol handlers - `SnmpKit.SnmpLib.Security.Priv` - Privacy protocol handlers - `SnmpKit.SnmpLib.Security.Keys` - Key derivation and management ## Usage Examples ### Basic SNMPv3 Authentication # Create authenticated user {:ok, user} = SnmpKit.SnmpLib.Security.create_user("admin", auth_protocol: :sha256, auth_password: "secure_password", engine_id: "engine123" ) # Authenticate message {:ok, auth_params} = SnmpKit.SnmpLib.Security.authenticate_message(user, message) ### Privacy (Encryption) Support # Create user with privacy {:ok, user} = SnmpKit.SnmpLib.Security.create_user("secure_admin", auth_protocol: :sha256, auth_password: "auth_password", priv_protocol: :aes256, priv_password: "priv_password" ) # Encrypt message {:ok, encrypted} = SnmpKit.SnmpLib.Security.encrypt_message(user, message) ### Engine ID Management # Generate engine ID engine_id = SnmpKit.SnmpLib.Security.generate_engine_id("192.168.1.1") # Discover remote engine {:ok, remote_engine} = SnmpKit.SnmpLib.Security.discover_engine("10.0.0.1") ## Security Considerations - All key material is stored securely in memory - Authentication and privacy keys are derived using RFC-compliant algorithms - Time-based authentication prevents replay attacks - Boot counter management ensures message freshness - Comprehensive input validation prevents security bypasses """ alias SnmpKit.SnmpLib.Security.Auth alias SnmpKit.SnmpLib.Security.Keys alias SnmpKit.SnmpLib.Security.Priv alias SnmpKit.SnmpLib.Security.USM @type auth_protocol :: :none | :md5 | :sha1 | :sha256 | :sha384 | :sha512 @type priv_protocol :: :none | :des | :aes128 | :aes192 | :aes256 @type engine_id :: binary() @type security_name :: binary() @type security_level :: :no_auth_no_priv | :auth_no_priv | :auth_priv @type user_config :: [ auth_protocol: auth_protocol(), auth_password: binary(), priv_protocol: priv_protocol(), priv_password: binary(), engine_id: engine_id() ] @type security_user :: %{ security_name: security_name(), auth_protocol: auth_protocol(), priv_protocol: priv_protocol(), auth_key: binary(), priv_key: binary(), engine_id: engine_id(), engine_boots: non_neg_integer(), engine_time: non_neg_integer() } @type security_params :: %{ authoritative_engine_id: engine_id(), authoritative_engine_boots: non_neg_integer(), authoritative_engine_time: non_neg_integer(), user_name: security_name(), authentication_parameters: binary(), privacy_parameters: binary() } ## User Management @doc """ Creates a new SNMPv3 security user with specified authentication and privacy settings. ## Parameters - `security_name`: Unique identifier for the user - `config`: User configuration including protocols and passwords ## Returns - `{:ok, user}`: Successfully created security user - `{:error, reason}`: Creation failed ## Examples # Authentication only user {:ok, user} = SnmpKit.SnmpLib.Security.create_user("monitor_user", auth_protocol: :sha256, auth_password: "monitoring_secret", engine_id: "local_engine" ) # Full authentication and privacy user {:ok, admin} = SnmpKit.SnmpLib.Security.create_user("admin_user", auth_protocol: :sha512, auth_password: "admin_auth_pass", priv_protocol: :aes256, priv_password: "admin_priv_pass", engine_id: "management_engine" ) """ @spec create_user(security_name(), user_config()) :: {:ok, security_user()} | {:error, atom()} def create_user(security_name, config) do with {:ok, validated_config} <- validate_user_config(config), {:ok, auth_key} <- derive_auth_key(validated_config), {:ok, priv_key} <- derive_priv_key(validated_config) do user = %{ security_name: security_name, auth_protocol: validated_config[:auth_protocol] || :none, priv_protocol: validated_config[:priv_protocol] || :none, auth_key: auth_key, priv_key: priv_key, engine_id: validated_config[:engine_id], engine_boots: 1, engine_time: System.system_time(:second) } {:ok, user} end end @doc """ Updates security user credentials and regenerates keys. """ @spec update_user(security_user(), user_config()) :: {:ok, security_user()} | {:error, atom()} def update_user(user, new_config) do updated_config = Map.merge(user_to_config(user), Map.new(new_config)) create_user(user.security_name, Map.to_list(updated_config)) end @doc """ Validates user credentials against stored authentication data. """ @spec validate_user(security_user(), binary(), binary()) :: :ok | {:error, atom()} def validate_user(user, auth_password, priv_password \\ "") do with :ok <- validate_auth_password(user, auth_password) do validate_priv_password(user, priv_password) end end ## Message Security @doc """ Determines the security level for a message based on user configuration. ## Security Levels - `:no_auth_no_priv` - No authentication, no privacy - `:auth_no_priv` - Authentication only - `:auth_priv` - Authentication and privacy """ @spec get_security_level(security_user()) :: security_level() def get_security_level(user) do case {user.auth_protocol, user.priv_protocol} do {:none, :none} -> :no_auth_no_priv {auth, :none} when auth != :none -> :auth_no_priv {auth, priv} when auth != :none and priv != :none -> :auth_priv _ -> :no_auth_no_priv end end @doc """ Authenticates an SNMP message using the user's authentication protocol. Returns authentication parameters that should be included in the message. """ @spec authenticate_message(security_user(), binary()) :: {:ok, binary()} | {:error, atom()} def authenticate_message(user, message) do case user.auth_protocol do :none -> {:ok, <<>>} protocol -> Auth.authenticate(protocol, user.auth_key, message) end end @doc """ Verifies message authentication using provided authentication parameters. """ @spec verify_authentication(security_user(), binary(), binary()) :: :ok | {:error, atom()} def verify_authentication(user, message, auth_params) do case user.auth_protocol do :none -> :ok protocol -> Auth.verify(protocol, user.auth_key, message, auth_params) end end @doc """ Encrypts message data using the user's privacy protocol. """ @spec encrypt_message(security_user(), binary()) :: {:ok, {binary(), binary()}} | {:error, atom()} def encrypt_message(user, plaintext) do case user.priv_protocol do :none -> {:ok, {plaintext, <<>>}} protocol -> Priv.encrypt(protocol, user.priv_key, user.auth_key, plaintext) end end @doc """ Decrypts message data using the user's privacy protocol. """ @spec decrypt_message(security_user(), binary(), binary()) :: {:ok, binary()} | {:error, atom()} def decrypt_message(user, ciphertext, priv_params) do case user.priv_protocol do :none -> {:ok, ciphertext} protocol -> Priv.decrypt(protocol, user.priv_key, user.auth_key, ciphertext, priv_params) end end ## Engine Management @doc """ Generates a unique engine ID for an SNMP entity. Engine IDs are used to uniquely identify SNMP engines and are required for SNMPv3 security operations. """ @spec generate_engine_id(binary()) :: engine_id() def generate_engine_id(_identifier) do # RFC 3411 compliant engine ID generation timestamp = System.system_time(:second) random = :crypto.strong_rand_bytes(4) # Format: enterprise_id(4) + format(1) + timestamp(4) + random(4) <<0x00, 0x00, 0x00, 0x01, 0x02>> <> <> <> random end @doc """ Discovers the engine ID of a remote SNMP agent. This is typically done during the first communication with a remote agent to establish security context. """ @spec discover_engine(binary(), keyword()) :: {:ok, engine_id()} | {:error, atom()} def discover_engine(host, opts \\ []) do # Implementation delegates to USM module USM.discover_engine(host, opts) end @doc """ Updates engine time and boot counter for time synchronization. """ @spec update_engine_time(security_user(), non_neg_integer(), non_neg_integer()) :: security_user() def update_engine_time(user, engine_boots, engine_time) do %{user | engine_boots: engine_boots, engine_time: engine_time} end ## Security Parameters @doc """ Builds security parameters for inclusion in SNMPv3 messages. """ @spec build_security_params(security_user(), binary(), binary()) :: security_params() def build_security_params(user, auth_params \\ <<>>, priv_params \\ <<>>) do %{ authoritative_engine_id: user.engine_id, authoritative_engine_boots: user.engine_boots, authoritative_engine_time: user.engine_time, user_name: user.security_name, authentication_parameters: auth_params, privacy_parameters: priv_params } end @doc """ Validates security parameters from received messages. """ @spec validate_security_params(security_user(), security_params()) :: :ok | {:error, atom()} def validate_security_params(user, params) do with :ok <- validate_engine_id(user.engine_id, params.authoritative_engine_id), :ok <- validate_time_window(user, params) do validate_user_name(user.security_name, params.user_name) end end ## Configuration and Status @doc """ Returns comprehensive information about security capabilities and status. """ @spec info() :: map() def info do %{ version: "5.1.0", phase: "5.1A - Security Foundation", supported_auth_protocols: [:md5, :sha1, :sha256, :sha384, :sha512], supported_priv_protocols: [:des, :aes128, :aes192, :aes256], rfc_compliance: ["RFC 3411", "RFC 3414", "RFC 3826"], security_levels: [:no_auth_no_priv, :auth_no_priv, :auth_priv], features: [ "User Security Model (USM)", "Multiple authentication protocols", "Multiple privacy protocols", "Engine ID management", "Time synchronization", "Key derivation (RFC 3414)", "Security parameter validation" ] } end ## Private Implementation defp validate_user_config(config) do auth_protocol = config[:auth_protocol] || :none priv_protocol = config[:priv_protocol] || :none with :ok <- validate_auth_protocol(auth_protocol), :ok <- validate_priv_protocol(priv_protocol), :ok <- validate_protocol_compatibility(auth_protocol, priv_protocol), :ok <- validate_required_passwords(config, auth_protocol, priv_protocol), :ok <- validate_engine_id(config) do {:ok, config} end end defp validate_auth_protocol(protocol) when protocol in [:none, :md5, :sha1, :sha256, :sha384, :sha512], do: :ok defp validate_auth_protocol(_), do: {:error, :invalid_auth_protocol} defp validate_priv_protocol(protocol) when protocol in [:none, :des, :aes128, :aes192, :aes256], do: :ok defp validate_priv_protocol(_), do: {:error, :invalid_priv_protocol} defp validate_protocol_compatibility(:none, priv) when priv != :none, do: {:error, :priv_requires_auth} defp validate_protocol_compatibility(_, _), do: :ok defp validate_required_passwords(config, auth_protocol, priv_protocol) do cond do auth_protocol != :none and is_nil(config[:auth_password]) -> {:error, :missing_auth_password} priv_protocol != :none and is_nil(config[:priv_password]) -> {:error, :missing_priv_password} true -> :ok end end defp validate_engine_id(config) do if is_nil(config[:engine_id]), do: {:error, :missing_engine_id}, else: :ok end defp derive_auth_key(config) do case config[:auth_protocol] do :none -> {:ok, <<>>} protocol -> Keys.derive_auth_key( protocol, config[:auth_password], config[:engine_id] ) end end defp derive_priv_key(config) do case config[:priv_protocol] do :none -> {:ok, <<>>} nil -> {:ok, <<>>} protocol -> Keys.derive_priv_key( protocol, config[:priv_password], config[:engine_id] ) end end defp user_to_config(user) do %{ auth_protocol: user.auth_protocol, priv_protocol: user.priv_protocol, engine_id: user.engine_id } end defp validate_auth_password(user, password) do case user.auth_protocol do :none -> :ok protocol -> expected_key = Keys.derive_auth_key(protocol, password, user.engine_id) case expected_key do {:ok, key} when key == user.auth_key -> :ok _ -> {:error, :invalid_auth_password} end end end defp validate_priv_password(user, password) do case user.priv_protocol do :none -> :ok protocol when password != "" -> expected_key = Keys.derive_priv_key(protocol, password, user.engine_id) case expected_key do {:ok, key} when key == user.priv_key -> :ok _ -> {:error, :invalid_priv_password} end _ -> {:error, :missing_priv_password} end end defp validate_engine_id(local_engine, remote_engine) do if local_engine == remote_engine do :ok else {:error, :engine_id_mismatch} end end defp validate_time_window(user, params) do # RFC 3414 time window validation (150 seconds) current_time = System.system_time(:second) time_diff = abs(current_time - params.authoritative_engine_time) boot_diff = abs(user.engine_boots - params.authoritative_engine_boots) cond do boot_diff > 1 -> {:error, :engine_boots_mismatch} boot_diff == 1 and time_diff > 150 -> {:error, :time_window_exceeded} boot_diff == 0 and time_diff > 150 -> {:error, :time_window_exceeded} true -> :ok end end defp validate_user_name(local_name, remote_name) do if local_name == remote_name do :ok else {:error, :user_name_mismatch} end end end