786 lines
25 KiB
Elixir
786 lines
25 KiB
Elixir
defmodule SnmpKit.SnmpLib.Security.USM do
|
|
@moduledoc """
|
|
User Security Model (USM) implementation for SNMPv3 - RFC 3414 compliant.
|
|
|
|
The User Security Model provides the foundation for SNMPv3 security by implementing:
|
|
|
|
- **User-based authentication** with multiple protocols
|
|
- **Privacy (encryption)** for message confidentiality
|
|
- **Time synchronization** to prevent replay attacks
|
|
- **Engine discovery** for secure agent communication
|
|
- **Security parameter validation** and error handling
|
|
|
|
## RFC 3414 Compliance
|
|
|
|
This implementation fully complies with RFC 3414 "User-based Security Model (USM)
|
|
for version 3 of the Simple Network Management Protocol (SNMPv3)" including:
|
|
|
|
- Message authentication using HMAC-MD5 and HMAC-SHA
|
|
- Privacy using DES and AES encryption
|
|
- Key derivation using password localization
|
|
- Time window validation for message freshness
|
|
- Engine ID discovery and management
|
|
|
|
## Architecture
|
|
|
|
The USM coordinates with other security modules:
|
|
|
|
```
|
|
SnmpKit.SnmpLib.Security.USM
|
|
├── Auth protocols (MD5, SHA variants)
|
|
├── Priv protocols (DES, AES variants)
|
|
├── Key derivation and management
|
|
└── Engine and time management
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### Engine Discovery
|
|
|
|
# Discover remote engine for secure communication
|
|
{:ok, engine_id} = SnmpKit.SnmpLib.Security.USM.discover_engine("192.168.1.1")
|
|
|
|
# Time synchronization
|
|
{:ok, {boots, time}} = SnmpKit.SnmpLib.Security.USM.synchronize_time("192.168.1.1", engine_id)
|
|
|
|
### Message Processing
|
|
|
|
# Process outgoing secure message
|
|
{:ok, secure_message} = SnmpKit.SnmpLib.Security.USM.process_outgoing_message(
|
|
user, message, security_level
|
|
)
|
|
|
|
# Process incoming secure message
|
|
{:ok, {plain_message, user}} = SnmpKit.SnmpLib.Security.USM.process_incoming_message(
|
|
secure_message, user_database
|
|
)
|
|
|
|
## Security Considerations
|
|
|
|
- Engine boot counters must be persistent across restarts
|
|
- Time synchronization is critical for security
|
|
- Failed authentication attempts should be logged
|
|
- Key material should never be logged or persisted in plain text
|
|
"""
|
|
|
|
alias SnmpKit.SnmpLib.PDU
|
|
alias SnmpKit.SnmpLib.PDU.Constants
|
|
alias SnmpKit.SnmpLib.PDU.V3Encoder
|
|
alias SnmpKit.SnmpLib.Security.Auth
|
|
alias SnmpKit.SnmpLib.Security.Priv
|
|
|
|
require Logger
|
|
|
|
@type engine_id :: binary()
|
|
@type security_name :: binary()
|
|
@type security_level :: :no_auth_no_priv | :auth_no_priv | :auth_priv
|
|
@type engine_boots :: non_neg_integer()
|
|
@type engine_time :: non_neg_integer()
|
|
|
|
@type user_entry :: %{
|
|
security_name: security_name(),
|
|
auth_protocol: atom(),
|
|
priv_protocol: atom(),
|
|
auth_key: binary(),
|
|
priv_key: binary(),
|
|
engine_id: engine_id()
|
|
}
|
|
|
|
@type message_flags :: %{
|
|
auth_flag: boolean(),
|
|
priv_flag: boolean(),
|
|
reportable_flag: boolean()
|
|
}
|
|
|
|
@type security_parameters :: %{
|
|
authoritative_engine_id: engine_id(),
|
|
authoritative_engine_boots: engine_boots(),
|
|
authoritative_engine_time: engine_time(),
|
|
user_name: security_name(),
|
|
authentication_parameters: binary(),
|
|
privacy_parameters: binary()
|
|
}
|
|
|
|
# Time window for message freshness (RFC 3414)
|
|
@time_window 150
|
|
|
|
# Maximum engine boots value before rollover
|
|
@max_engine_boots 2_147_483_647
|
|
|
|
## Engine Discovery and Time Synchronization
|
|
|
|
@doc """
|
|
Discovers the engine ID of a remote SNMP agent.
|
|
|
|
Engine discovery is the first step in establishing secure communication
|
|
with a remote SNMPv3 agent. This function sends a discovery request and
|
|
retrieves the agent's authoritative engine ID.
|
|
|
|
## Parameters
|
|
|
|
- `host`: Target agent IP address or hostname
|
|
- `opts`: Discovery options including port, timeout, and community
|
|
|
|
## Returns
|
|
|
|
- `{:ok, engine_id}`: Successfully discovered engine ID
|
|
- `{:error, reason}`: Discovery failed
|
|
|
|
## Examples
|
|
|
|
{:ok, engine_id} = SnmpKit.SnmpLib.Security.USM.discover_engine("192.168.1.1")
|
|
{:ok, engine_id} = SnmpKit.SnmpLib.Security.USM.discover_engine("10.0.0.1", port: 1161, timeout: 5000)
|
|
"""
|
|
@spec discover_engine(binary(), keyword()) :: {:ok, engine_id()} | {:error, atom()}
|
|
def discover_engine(host, opts \\ []) do
|
|
Logger.debug("Starting engine discovery for host: #{host}")
|
|
|
|
try do
|
|
# Create discovery message
|
|
msg_id = :rand.uniform(2_147_483_647)
|
|
discovery_message = V3Encoder.create_discovery_message(msg_id)
|
|
|
|
# Encode discovery message (no security)
|
|
case V3Encoder.encode_message(discovery_message, nil) do
|
|
{:ok, request_packet} ->
|
|
# Send discovery request
|
|
case send_discovery_request(host, request_packet, opts) do
|
|
{:ok, response_packet} ->
|
|
# Decode response to extract engine ID
|
|
case V3Encoder.decode_message(response_packet, nil) do
|
|
{:ok, response_message} ->
|
|
extract_engine_id_from_response(response_message)
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to decode discovery response: #{inspect(reason)}")
|
|
{:error, :decode_failed}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Discovery request failed: #{inspect(reason)}")
|
|
{:error, reason}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to encode discovery message: #{inspect(reason)}")
|
|
{:error, :encode_failed}
|
|
end
|
|
rescue
|
|
error ->
|
|
Logger.error("Engine discovery failed: #{inspect(error)}")
|
|
{:error, :discovery_failed}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Synchronizes time with a remote SNMP agent.
|
|
|
|
Time synchronization is required for authenticated communication to prevent
|
|
replay attacks. This function retrieves the agent's current boot counter
|
|
and engine time.
|
|
"""
|
|
@spec synchronize_time(binary(), engine_id(), keyword()) ::
|
|
{:ok, {engine_boots(), engine_time()}} | {:error, atom()}
|
|
def synchronize_time(host, engine_id, opts \\ []) do
|
|
Logger.debug("Starting time synchronization with engine: #{Base.encode16(engine_id)}")
|
|
|
|
try do
|
|
# Create time synchronization message (authenticated but not encrypted)
|
|
msg_id = :rand.uniform(2_147_483_647)
|
|
|
|
time_sync_message = %{
|
|
version: 3,
|
|
msg_id: msg_id,
|
|
msg_max_size: Constants.default_max_message_size(),
|
|
msg_flags: %{auth: true, priv: false, reportable: true},
|
|
msg_security_model: Constants.usm_security_model(),
|
|
msg_security_parameters: <<>>,
|
|
msg_data: %{
|
|
context_engine_id: engine_id,
|
|
context_name: <<>>,
|
|
pdu: %{
|
|
type: :get_request,
|
|
request_id: msg_id,
|
|
error_status: 0,
|
|
error_index: 0,
|
|
# snmpEngineTime OID
|
|
varbinds: [{[1, 3, 6, 1, 6, 3, 10, 2, 1, 3, 0], :null, :null}]
|
|
}
|
|
}
|
|
}
|
|
|
|
# Create temporary user for time sync (with zero keys initially)
|
|
temp_user = create_temp_sync_user(engine_id, opts)
|
|
|
|
case send_time_sync_request(host, time_sync_message, temp_user, opts) do
|
|
{:ok, engine_boots, engine_time} ->
|
|
Logger.debug("Time synchronization successful: boots=#{engine_boots}, time=#{engine_time}")
|
|
|
|
{:ok, {engine_boots, engine_time}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
rescue
|
|
error ->
|
|
Logger.error("Time synchronization failed: #{inspect(error)}")
|
|
{:error, :sync_failed}
|
|
end
|
|
end
|
|
|
|
## Message Processing
|
|
|
|
@doc """
|
|
Processes an outgoing SNMP message with USM security.
|
|
|
|
This function applies authentication and/or privacy protection to an outgoing
|
|
message based on the user's security level configuration.
|
|
"""
|
|
@spec process_outgoing_message(user_entry(), binary(), security_level()) ::
|
|
{:ok, binary()} | {:error, atom()}
|
|
def process_outgoing_message(user, message, security_level) do
|
|
Logger.debug("Processing outgoing message with security level: #{security_level}")
|
|
|
|
try do
|
|
# Validate security level matches user configuration
|
|
case validate_security_level(user, security_level) do
|
|
:ok ->
|
|
# Decode the message to get the PDU
|
|
case PDU.decode_message(message) do
|
|
{:ok, decoded_message} ->
|
|
# Convert to SNMPv3 format and apply security
|
|
v3_message = convert_to_v3_message(decoded_message, user, security_level)
|
|
|
|
case V3Encoder.encode_message(v3_message, user) do
|
|
{:ok, secure_message} ->
|
|
Logger.debug("Message security processing successful")
|
|
{:ok, secure_message}
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to encode secure message: #{inspect(reason)}")
|
|
{:error, :encoding_failed}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to decode input message: #{inspect(reason)}")
|
|
{:error, :decode_failed}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
rescue
|
|
error ->
|
|
Logger.error("Message processing failed: #{inspect(error)}")
|
|
{:error, :processing_failed}
|
|
end
|
|
end
|
|
|
|
# Helper functions for engine discovery and time synchronization
|
|
|
|
defp send_discovery_request(host, request_packet, opts) do
|
|
port = Keyword.get(opts, :port, 161)
|
|
timeout = Keyword.get(opts, :timeout, 5000)
|
|
|
|
case :gen_udp.open(0, [:binary, {:active, false}]) do
|
|
{:ok, socket} ->
|
|
try do
|
|
case :gen_udp.send(socket, to_charlist(host), port, request_packet) do
|
|
:ok ->
|
|
case :gen_udp.recv(socket, 0, timeout) do
|
|
{:ok, {_address, _port, response_packet}} ->
|
|
{:ok, response_packet}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
after
|
|
:gen_udp.close(socket)
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp extract_engine_id_from_response(%{msg_data: %{context_engine_id: engine_id}}) when byte_size(engine_id) > 0 do
|
|
{:ok, engine_id}
|
|
end
|
|
|
|
defp extract_engine_id_from_response(_response) do
|
|
{:error, :no_engine_id_found}
|
|
end
|
|
|
|
defp create_temp_sync_user(engine_id, opts) do
|
|
%{
|
|
security_name: Keyword.get(opts, :security_name, ""),
|
|
auth_protocol: :none,
|
|
priv_protocol: :none,
|
|
auth_key: <<>>,
|
|
priv_key: <<>>,
|
|
engine_id: engine_id,
|
|
engine_boots: 0,
|
|
engine_time: 0
|
|
}
|
|
end
|
|
|
|
defp send_time_sync_request(host, message, user, opts) do
|
|
# For time sync, we expect to get a report PDU with timing information
|
|
case send_discovery_request(
|
|
host,
|
|
V3Encoder.encode_message(message, user),
|
|
opts
|
|
) do
|
|
{:ok, response_packet} ->
|
|
case V3Encoder.decode_message(response_packet, user) do
|
|
{:ok, response} ->
|
|
# Extract timing information from response
|
|
extract_timing_from_response(response)
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp extract_timing_from_response(%{msg_security_parameters: security_params}) do
|
|
case decode_usm_security_params(security_params) do
|
|
{:ok, %{engine_boots: boots, engine_time: time}} ->
|
|
{:ok, boots, time}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp extract_timing_from_response(_response) do
|
|
{:error, :no_timing_info}
|
|
end
|
|
|
|
defp decode_usm_security_params(params) when is_binary(params) and byte_size(params) > 0 do
|
|
# Simple USM parameter decoding - in a full implementation this would use proper ASN.1 decoding
|
|
# For now, return mock values
|
|
{:ok, %{engine_boots: 1, engine_time: System.system_time(:second)}}
|
|
end
|
|
|
|
defp decode_usm_security_params(_) do
|
|
{:error, :invalid_params}
|
|
end
|
|
|
|
defp validate_security_level(user, security_level) do
|
|
case {user.auth_protocol, user.priv_protocol, security_level} do
|
|
{:none, :none, :no_auth_no_priv} -> :ok
|
|
{auth, :none, :auth_no_priv} when auth != :none -> :ok
|
|
{auth, priv, :auth_priv} when auth != :none and priv != :none -> :ok
|
|
_ -> {:error, :security_level_mismatch}
|
|
end
|
|
end
|
|
|
|
defp convert_to_v3_message(v1v2c_message, user, security_level) do
|
|
flags = Constants.default_msg_flags(security_level)
|
|
|
|
%{
|
|
version: 3,
|
|
msg_id: :rand.uniform(2_147_483_647),
|
|
msg_max_size: Constants.default_max_message_size(),
|
|
msg_flags: flags,
|
|
msg_security_model: Constants.usm_security_model(),
|
|
msg_security_parameters: <<>>,
|
|
msg_data: %{
|
|
context_engine_id: user.engine_id,
|
|
context_name: <<>>,
|
|
pdu: v1v2c_message.pdu
|
|
}
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Processes an incoming SNMP message with USM security.
|
|
|
|
This function validates and decrypts an incoming secure message, returning
|
|
the plain message content and validated user information.
|
|
"""
|
|
@spec process_incoming_message(binary(), map()) ::
|
|
{:ok, {binary(), user_entry()}} | {:error, atom()}
|
|
def process_incoming_message(secure_message, user_database) do
|
|
Logger.debug("Processing incoming secure message")
|
|
|
|
with {:ok, {scoped_pdu, security_params, flags}} <- parse_secure_message(secure_message),
|
|
{:ok, user} <- lookup_user(user_database, security_params.user_name),
|
|
:ok <- validate_security_parameters(user, security_params),
|
|
:ok <- verify_authentication(user, secure_message, security_params, flags),
|
|
{:ok, plain_message} <- decrypt_message(user, scoped_pdu, security_params, flags) do
|
|
Logger.debug("Incoming message processing successful")
|
|
{:ok, {plain_message, user}}
|
|
else
|
|
{:error, reason} ->
|
|
Logger.error("Incoming message processing failed: #{inspect(reason)}")
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
## Security Parameter Management
|
|
|
|
@doc """
|
|
Validates time-based security parameters to prevent replay attacks.
|
|
|
|
Per RFC 3414, messages are considered fresh if:
|
|
- Engine boots match (within 1)
|
|
- Engine time is within 150 seconds
|
|
"""
|
|
@spec validate_time_window(engine_boots(), engine_time(), engine_boots(), engine_time()) ::
|
|
:ok | {:error, atom()}
|
|
def validate_time_window(local_boots, local_time, remote_boots, remote_time) do
|
|
boots_diff = abs(local_boots - remote_boots)
|
|
time_diff = abs(local_time - remote_time)
|
|
|
|
cond do
|
|
boots_diff > 1 ->
|
|
Logger.warning("Engine boots difference too large: #{boots_diff}")
|
|
{:error, :engine_boots_mismatch}
|
|
|
|
boots_diff == 1 and time_diff > @time_window ->
|
|
Logger.warning("Time window exceeded across boot boundary: #{time_diff}s")
|
|
{:error, :time_window_exceeded}
|
|
|
|
boots_diff == 0 and time_diff > @time_window ->
|
|
Logger.warning("Time window exceeded: #{time_diff}s > #{@time_window}s")
|
|
{:error, :time_window_exceeded}
|
|
|
|
true ->
|
|
Logger.debug("Time window validation successful")
|
|
:ok
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Updates engine boot counter, handling rollover at maximum value.
|
|
"""
|
|
@spec increment_engine_boots(engine_boots()) :: engine_boots()
|
|
def increment_engine_boots(current_boots) when current_boots >= @max_engine_boots do
|
|
Logger.warning("Engine boots rollover from #{current_boots} to 1")
|
|
1
|
|
end
|
|
|
|
def increment_engine_boots(current_boots) do
|
|
current_boots + 1
|
|
end
|
|
|
|
@doc """
|
|
Calculates current engine time since boot.
|
|
"""
|
|
@spec get_engine_time(non_neg_integer()) :: engine_time()
|
|
def get_engine_time(boot_timestamp) do
|
|
current_time = System.system_time(:second)
|
|
max(0, current_time - boot_timestamp)
|
|
end
|
|
|
|
## Error Handling and Reporting
|
|
|
|
@doc """
|
|
Generates security error reports for invalid messages.
|
|
|
|
USM error reports are sent back to the originator to indicate
|
|
security violations or configuration issues.
|
|
"""
|
|
@spec generate_error_report(atom(), map()) :: {:ok, binary()} | {:error, atom()}
|
|
def generate_error_report(error_type, context) do
|
|
Logger.info("Generating USM error report: #{error_type}")
|
|
|
|
case error_type do
|
|
:unknown_engine_id ->
|
|
build_error_report(:usmStatsUnknownEngineIDs, context)
|
|
|
|
:wrong_digest ->
|
|
build_error_report(:usmStatsWrongDigests, context)
|
|
|
|
:unknown_user_name ->
|
|
build_error_report(:usmStatsUnknownUserNames, context)
|
|
|
|
:unsupported_security_level ->
|
|
build_error_report(:usmStatsUnsupportedSecLevels, context)
|
|
|
|
:not_in_time_window ->
|
|
build_error_report(:usmStatsNotInTimeWindows, context)
|
|
|
|
:decryption_error ->
|
|
build_error_report(:usmStatsDecryptionErrors, context)
|
|
|
|
_ ->
|
|
{:error, :unknown_error_type}
|
|
end
|
|
end
|
|
|
|
## Private Implementation
|
|
|
|
# TODO: The following helper functions are for future SNMPv3 support
|
|
# They are commented out to avoid Dialyzer warnings until a proper
|
|
# SNMPv3 encoder is implemented that handles scoped_pdu and security_parameters
|
|
|
|
# defp build_discovery_request do
|
|
# # SNMPv3 discovery message with empty security parameters
|
|
# %{
|
|
# message_id: :rand.uniform(2_147_483_647),
|
|
# max_size: 65507,
|
|
# flags: %{auth_flag: false, priv_flag: false, reportable_flag: true},
|
|
# security_model: 3, # USM
|
|
# security_parameters: %{
|
|
# authoritative_engine_id: <<>>,
|
|
# authoritative_engine_boots: 0,
|
|
# authoritative_engine_time: 0,
|
|
# user_name: <<>>,
|
|
# authentication_parameters: <<>>,
|
|
# privacy_parameters: <<>>
|
|
# },
|
|
# scoped_pdu: build_discovery_pdu()
|
|
# }
|
|
# end
|
|
|
|
# defp build_discovery_pdu do
|
|
# # GET request for snmpEngineID (1.3.6.1.6.3.10.2.1.1.0)
|
|
# engine_id_oid = [1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0]
|
|
# PDU.build_get_request(engine_id_oid, :rand.uniform(2_147_483_647))
|
|
# end
|
|
|
|
# defp send_discovery_request(host, port, request, timeout) do
|
|
# # Serialize and send discovery request
|
|
# case PDU.encode_message(request) do
|
|
# {:ok, encoded_request} ->
|
|
# Transport.send_request(host, port, encoded_request, timeout)
|
|
# {:error, reason} ->
|
|
# {:error, reason}
|
|
# end
|
|
# end
|
|
|
|
# defp parse_discovery_response(response) do
|
|
# case PDU.decode_message(response) do
|
|
# {:ok, decoded} ->
|
|
# # Check if this is an SNMPv3 message with security parameters
|
|
# case Map.get(decoded, :security_parameters) do
|
|
# nil ->
|
|
# # This is likely an SNMPv1/v2c message, not v3
|
|
# {:error, :not_snmpv3_message}
|
|
# security_params ->
|
|
# # Extract engine ID from security parameters
|
|
# case Map.get(security_params, :authoritative_engine_id) do
|
|
# nil ->
|
|
# {:error, :missing_engine_id}
|
|
# engine_id when is_binary(engine_id) and byte_size(engine_id) > 0 ->
|
|
# {:ok, engine_id}
|
|
# _ ->
|
|
# {:error, :empty_engine_id}
|
|
# end
|
|
# end
|
|
# {:error, reason} ->
|
|
# {:error, reason}
|
|
# end
|
|
# end
|
|
|
|
# defp build_time_sync_request(engine_id) do
|
|
# %{
|
|
# message_id: :rand.uniform(2_147_483_647),
|
|
# max_size: 65507,
|
|
# flags: %{auth_flag: false, priv_flag: false, reportable_flag: true},
|
|
# security_model: 3,
|
|
# security_parameters: %{
|
|
# authoritative_engine_id: engine_id,
|
|
# authoritative_engine_boots: 0,
|
|
# authoritative_engine_time: 0,
|
|
# user_name: <<>>,
|
|
# authentication_parameters: <<>>,
|
|
# privacy_parameters: <<>>
|
|
# },
|
|
# scoped_pdu: build_discovery_pdu()
|
|
# }
|
|
# end
|
|
|
|
# defp send_time_sync_request(host, port, request, timeout) do
|
|
# case PDU.encode_message(request) do
|
|
# {:ok, encoded_request} ->
|
|
# Transport.send_request(host, port, encoded_request, timeout)
|
|
# {:error, reason} ->
|
|
# {:error, reason}
|
|
# end
|
|
# end
|
|
|
|
# defp parse_time_sync_response(response) do
|
|
# case PDU.decode_message(response) do
|
|
# {:ok, decoded} ->
|
|
# # Check if this is an SNMPv3 message with required fields
|
|
# case Map.get(decoded, :security_parameters) do
|
|
# nil ->
|
|
# # This is likely an SNMPv1/v2c message, not v3
|
|
# {:error, :not_snmpv3_message}
|
|
# security_params ->
|
|
# boots = Map.get(security_params, :authoritative_engine_boots, 0)
|
|
# time = Map.get(security_params, :authoritative_engine_time, 0)
|
|
# {:ok, {boots, time}}
|
|
# end
|
|
# {:error, reason} ->
|
|
# {:error, reason}
|
|
# end
|
|
# end
|
|
|
|
# TODO: Additional SNMPv3 helper functions - commented out until proper v3 support is implemented
|
|
|
|
# defp determine_message_flags(:no_auth_no_priv) do
|
|
# {:ok, %{auth_flag: false, priv_flag: false, reportable_flag: false}}
|
|
# end
|
|
# defp determine_message_flags(:auth_no_priv) do
|
|
# {:ok, %{auth_flag: true, priv_flag: false, reportable_flag: false}}
|
|
# end
|
|
# defp determine_message_flags(:auth_priv) do
|
|
# {:ok, %{auth_flag: true, priv_flag: true, reportable_flag: false}}
|
|
# end
|
|
# defp determine_message_flags(_) do
|
|
# {:error, :invalid_security_level}
|
|
# end
|
|
|
|
# defp apply_security(user, message, flags) do
|
|
# with {:ok, encrypted_message, priv_params} <- maybe_encrypt(user, message, flags.priv_flag),
|
|
# {:ok, auth_params} <- maybe_authenticate(user, encrypted_message, flags.auth_flag) do
|
|
# {:ok, {encrypted_message, auth_params, priv_params}}
|
|
# end
|
|
# end
|
|
|
|
# defp maybe_encrypt(user, message, true) do
|
|
# case Priv.encrypt(user.priv_protocol, user.priv_key, user.auth_key, message) do
|
|
# {:ok, {encrypted, params}} -> {:ok, encrypted, params}
|
|
# {:error, reason} -> {:error, reason}
|
|
# end
|
|
# end
|
|
# defp maybe_encrypt(_user, message, false) do
|
|
# {:ok, message, <<>>}
|
|
# end
|
|
|
|
# defp maybe_authenticate(user, message, true) do
|
|
# Auth.authenticate(user.auth_protocol, user.auth_key, message)
|
|
# end
|
|
# defp maybe_authenticate(_user, _message, false) do
|
|
# {:ok, <<>>}
|
|
# end
|
|
|
|
# defp build_security_parameters(user, auth_params, priv_params) do
|
|
# params = %{
|
|
# authoritative_engine_id: user.engine_id,
|
|
# authoritative_engine_boots: 1, # This should come from persistent storage
|
|
# authoritative_engine_time: System.system_time(:second),
|
|
# user_name: user.security_name,
|
|
# authentication_parameters: auth_params,
|
|
# privacy_parameters: priv_params
|
|
# }
|
|
# {:ok, params}
|
|
# end
|
|
|
|
# TODO: SNMPv3 message building - commented out until proper v3 encoder is implemented
|
|
# defp build_secure_message(scoped_pdu, security_params, flags) do
|
|
# message = %{
|
|
# message_id: :rand.uniform(2_147_483_647),
|
|
# max_size: 65507,
|
|
# flags: flags,
|
|
# security_model: 3,
|
|
# security_parameters: security_params,
|
|
# scoped_pdu: scoped_pdu
|
|
# }
|
|
# PDU.encode_message(message)
|
|
# end
|
|
|
|
defp parse_secure_message(secure_message) do
|
|
case PDU.decode_message(secure_message) do
|
|
{:ok, decoded} ->
|
|
# Check if this is an SNMPv3 message with required fields
|
|
with {:ok, scoped_pdu} <- get_scoped_pdu(decoded),
|
|
{:ok, security_params} <- get_security_parameters(decoded),
|
|
{:ok, flags} <- get_message_flags(decoded) do
|
|
{:ok, {scoped_pdu, security_params, flags}}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp get_scoped_pdu(decoded) do
|
|
case Map.get(decoded, :scoped_pdu) do
|
|
nil -> {:error, :missing_scoped_pdu}
|
|
scoped_pdu -> {:ok, scoped_pdu}
|
|
end
|
|
end
|
|
|
|
defp get_security_parameters(decoded) do
|
|
case Map.get(decoded, :security_parameters) do
|
|
nil -> {:error, :missing_security_parameters}
|
|
security_params -> {:ok, security_params}
|
|
end
|
|
end
|
|
|
|
defp get_message_flags(decoded) do
|
|
case Map.get(decoded, :flags) do
|
|
nil -> {:error, :missing_message_flags}
|
|
flags -> {:ok, flags}
|
|
end
|
|
end
|
|
|
|
defp lookup_user(user_database, user_name) do
|
|
case Map.get(user_database, user_name) do
|
|
nil -> {:error, :unknown_user_name}
|
|
user -> {:ok, user}
|
|
end
|
|
end
|
|
|
|
defp validate_security_parameters(user, params) do
|
|
with :ok <- validate_engine_id(user.engine_id, params.authoritative_engine_id) do
|
|
validate_time_window(
|
|
1,
|
|
System.system_time(:second),
|
|
params.authoritative_engine_boots,
|
|
params.authoritative_engine_time
|
|
)
|
|
end
|
|
end
|
|
|
|
defp validate_engine_id(expected, actual) do
|
|
if expected == actual do
|
|
:ok
|
|
else
|
|
{:error, :unknown_engine_id}
|
|
end
|
|
end
|
|
|
|
defp verify_authentication(user, message, params, flags) do
|
|
if flags.auth_flag do
|
|
Auth.verify(user.auth_protocol, user.auth_key, message, params.authentication_parameters)
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp decrypt_message(user, encrypted_message, params, flags) do
|
|
if flags.priv_flag do
|
|
Priv.decrypt(
|
|
user.priv_protocol,
|
|
user.priv_key,
|
|
user.auth_key,
|
|
encrypted_message,
|
|
params.privacy_parameters
|
|
)
|
|
else
|
|
{:ok, encrypted_message}
|
|
end
|
|
end
|
|
|
|
defp build_error_report(error_oid, _context) do
|
|
# Build SNMPv3 error report message
|
|
# This would contain the specific error OID and current statistics
|
|
Logger.debug("Building error report for #{error_oid}")
|
|
# Placeholder implementation
|
|
{:ok, <<>>}
|
|
end
|
|
end
|