Packages

A comprehensive SNMP toolkit for Elixir featuring a unified API, pure Elixir implementation, and powerful device simulation. Perfect for network monitoring, testing, and development with support for SNMP operations, MIB management, and realistic device simulation.

Current section

Files

Jump to
snmpkit lib snmpkit snmp_lib security priv.ex
Raw

lib/snmpkit/snmp_lib/security/priv.ex

defmodule SnmpKit.SnmpLib.Security.Priv do
@moduledoc """
Privacy (encryption) protocols for SNMPv3 User Security Model.
Implements encryption protocols as specified in RFC 3414 and RFC 3826,
providing message confidentiality for SNMPv3 communications.
## Supported Protocols
- **DES-CBC** (RFC 3414) - 56-bit key, legacy support
- **AES-128** (RFC 3826) - 128-bit key, good security
- **AES-192** (RFC 3826) - 192-bit key, enhanced security
- **AES-256** (RFC 3826) - 256-bit key, maximum security
## Security Considerations
- DES is deprecated and should only be used for legacy compatibility
- AES-128 provides adequate security for most applications
- AES-256 is recommended for high-security environments
- All encryption uses CBC mode with random initialization vectors
- Privacy requires authentication (cannot use privacy without authentication)
## Protocol Selection Guidelines
- **AES-256**: Recommended for high-security environments
- **AES-128**: Good balance of security and performance for most deployments
- **DES**: Legacy compatibility only, not recommended for new deployments
## Technical Details
### Key Derivation
Privacy keys are derived from privacy passwords using the same engine ID
and algorithm as authentication keys, but with different key usage.
### Initialization Vectors
Each encryption operation uses a unique initialization vector (IV) to ensure
that identical plaintexts produce different ciphertexts.
### Padding
Block ciphers use PKCS#7 padding to handle messages that don't align
with block boundaries.
## Usage Examples
### Message Encryption
# Encrypt message with AES-256
priv_key = derived_privacy_key
auth_key = derived_authentication_key # Required for IV generation
plaintext = "confidential SNMP data"
{:ok, {ciphertext, priv_params}} = SnmpKit.SnmpLib.Security.Priv.encrypt(
:aes256, priv_key, auth_key, plaintext
)
# Decrypt message
{:ok, decrypted} = SnmpKit.SnmpLib.Security.Priv.decrypt(
:aes256, priv_key, auth_key, ciphertext, priv_params
)
### Protocol Information
# Get encryption protocol details
info = SnmpKit.SnmpLib.Security.Priv.protocol_info(:aes256)
# Returns: %{algorithm: :aes_256_cbc, key_size: 32, block_size: 16, ...}
"""
require Logger
@type priv_protocol :: :none | :des | :aes128 | :aes192 | :aes256
@type priv_key :: binary()
@type auth_key :: binary()
@type priv_params :: binary()
@type plaintext :: binary()
@type ciphertext :: binary()
@type initialization_vector :: binary()
# Protocol specifications per RFC 3414 and RFC 3826
@protocol_specs %{
none: %{
algorithm: :none,
key_size: 0,
block_size: 0,
iv_size: 0,
secure: false,
rfc: "N/A"
},
des: %{
algorithm: :des_cbc,
# 56-bit effective + 8 parity bits
key_size: 8,
block_size: 8,
iv_size: 8,
# Deprecated
secure: false,
rfc: "RFC 3414"
},
aes128: %{
algorithm: :aes_128_cbc,
key_size: 16,
block_size: 16,
iv_size: 16,
secure: true,
rfc: "RFC 3826"
},
aes192: %{
algorithm: :aes_192_cbc,
key_size: 24,
block_size: 16,
iv_size: 16,
secure: true,
rfc: "RFC 3826"
},
aes256: %{
algorithm: :aes_256_cbc,
key_size: 32,
block_size: 16,
iv_size: 16,
secure: true,
rfc: "RFC 3826"
}
}
## Protocol Information
@doc """
Returns information about a specific privacy protocol.
## Examples
iex> SnmpKit.SnmpLib.Security.Priv.protocol_info(:aes256)
%{algorithm: :aes_256_cbc, key_size: 32, block_size: 16, iv_size: 16, secure: true, rfc: "RFC 3826"}
iex> SnmpKit.SnmpLib.Security.Priv.protocol_info(:des)
%{algorithm: :des_cbc, key_size: 8, block_size: 8, iv_size: 8, secure: false, rfc: "RFC 3414"}
"""
@spec protocol_info(priv_protocol()) :: map() | nil
def protocol_info(protocol) do
Map.get(@protocol_specs, protocol)
end
@doc """
Returns list of all supported privacy protocols.
"""
@spec supported_protocols() :: [priv_protocol()]
def supported_protocols do
Map.keys(@protocol_specs)
end
@doc """
Returns list of cryptographically secure protocols (excludes deprecated ones).
"""
@spec secure_protocols() :: [priv_protocol()]
def secure_protocols do
@protocol_specs
|> Enum.filter(fn {_protocol, spec} -> spec.secure end)
|> Enum.map(fn {protocol, _spec} -> protocol end)
end
@doc """
Checks if a protocol is considered cryptographically secure.
"""
@spec secure_protocol?(priv_protocol()) :: boolean()
def secure_protocol?(protocol) do
case protocol_info(protocol) do
%{secure: secure} -> secure
nil -> false
end
end
## Encryption Operations
@doc """
Encrypts plaintext using the specified privacy protocol.
## Parameters
- `protocol`: Privacy protocol to use (:des, :aes128, :aes192, :aes256)
- `priv_key`: Privacy key (must be correct length for protocol)
- `auth_key`: Authentication key (used for IV generation in some protocols)
- `plaintext`: Data to encrypt
## Returns
- `{:ok, {ciphertext, priv_params}}`: Encryption successful
- `{:error, reason}`: Encryption failed
## Examples
# AES-256 encryption (recommended)
{:ok, {ciphertext, priv_params}} = SnmpKit.SnmpLib.Security.Priv.encrypt(
:aes256, priv_key, auth_key, "secret data"
)
# DES encryption (legacy)
{:ok, {ciphertext, priv_params}} = SnmpKit.SnmpLib.Security.Priv.encrypt(
:des, priv_key, auth_key, "legacy data"
)
"""
@spec encrypt(priv_protocol(), priv_key(), auth_key(), plaintext()) ::
{:ok, {ciphertext(), priv_params()}} | {:error, atom()}
def encrypt(:none, _priv_key, _auth_key, plaintext) do
{:ok, {plaintext, <<>>}}
end
def encrypt(protocol, priv_key, auth_key, plaintext) when is_atom(protocol) do
case protocol_info(protocol) do
nil ->
Logger.error("Unsupported privacy protocol: #{protocol}")
{:error, :unsupported_protocol}
spec ->
with :ok <- validate_encryption_params(spec, priv_key, plaintext),
{:ok, iv} <- generate_iv(protocol, spec, auth_key),
{:ok, padded_plaintext} <- apply_padding(plaintext, spec.block_size),
{:ok, ciphertext} <- perform_encryption(spec, priv_key, iv, padded_plaintext) do
priv_params = build_privacy_parameters(protocol, iv)
Logger.debug(
"Encryption successful with #{protocol}, ciphertext size: #{byte_size(ciphertext)}"
)
{:ok, {ciphertext, priv_params}}
else
{:error, reason} ->
Logger.error("Encryption failed for #{protocol}: #{reason}")
{:error, reason}
end
end
end
def encrypt(protocol, _priv_key, _auth_key, _plaintext) do
Logger.error("Invalid privacy protocol type: #{inspect(protocol)}")
{:error, :invalid_protocol_type}
end
@doc """
Decrypts ciphertext using the specified privacy protocol.
## Parameters
- `protocol`: Privacy protocol used for encryption
- `priv_key`: Privacy key (same as used for encryption)
- `auth_key`: Authentication key (used for IV validation)
- `ciphertext`: Encrypted data
- `priv_params`: Privacy parameters from encryption (contains IV)
## Returns
- `{:ok, plaintext}`: Decryption successful
- `{:error, reason}`: Decryption failed
## Examples
# AES-256 decryption
{:ok, plaintext} = SnmpKit.SnmpLib.Security.Priv.decrypt(
:aes256, priv_key, auth_key, ciphertext, priv_params
)
# Handle decryption errors
case SnmpKit.SnmpLib.Security.Priv.decrypt(:des, priv_key, auth_key, ciphertext, priv_params) do
{:ok, plaintext} -> process_plaintext(plaintext)
{:error, :decryption_failed} -> handle_corruption()
{:error, :invalid_padding} -> handle_padding_error()
end
"""
@spec decrypt(priv_protocol(), priv_key(), auth_key(), ciphertext(), priv_params()) ::
{:ok, plaintext()} | {:error, atom()}
def decrypt(:none, _priv_key, _auth_key, ciphertext, _priv_params) do
{:ok, ciphertext}
end
def decrypt(protocol, priv_key, _auth_key, ciphertext, priv_params) when is_atom(protocol) do
case protocol_info(protocol) do
nil ->
Logger.error("Unsupported privacy protocol: #{protocol}")
{:error, :unsupported_protocol}
spec ->
with :ok <- validate_decryption_params(spec, priv_key, ciphertext, priv_params),
{:ok, iv} <- extract_iv(protocol, priv_params),
{:ok, padded_plaintext} <- perform_decryption(spec, priv_key, iv, ciphertext),
{:ok, plaintext} <- remove_padding(padded_plaintext, spec.block_size) do
Logger.debug(
"Decryption successful with #{protocol}, plaintext size: #{byte_size(plaintext)}"
)
{:ok, plaintext}
else
{:error, reason} ->
Logger.error("Decryption failed for #{protocol}: #{reason}")
{:error, reason}
end
end
end
def decrypt(protocol, _priv_key, _auth_key, _ciphertext, _priv_params) do
Logger.error("Invalid privacy protocol type: #{inspect(protocol)}")
{:error, :invalid_protocol_type}
end
## Key Validation
@doc """
Validates that a privacy key is appropriate for the specified protocol.
## Examples
:ok = SnmpKit.SnmpLib.Security.Priv.validate_key(:aes256, key_32_bytes)
{:error, :key_wrong_size} = SnmpKit.SnmpLib.Security.Priv.validate_key(:aes128, key_32_bytes)
"""
@spec validate_key(priv_protocol(), priv_key()) :: :ok | {:error, atom()}
def validate_key(:none, _key) do
:ok
end
def validate_key(protocol, key) when is_atom(protocol) and is_binary(key) do
case protocol_info(protocol) do
nil ->
{:error, :unsupported_protocol}
spec ->
key_length = byte_size(key)
expected_length = spec.key_size
cond do
key_length == 0 ->
{:error, :empty_key}
key_length != expected_length ->
Logger.error(
"Privacy key wrong size for #{protocol}: #{key_length} != #{expected_length}"
)
{:error, :key_wrong_size}
not spec.secure ->
Logger.warning("Using deprecated privacy protocol: #{protocol}")
:ok
true ->
:ok
end
end
end
def validate_key(_protocol, _key) do
{:error, :invalid_parameters}
end
## Batch Operations
@doc """
Encrypts multiple plaintexts using the same protocol and key.
Each plaintext gets a unique IV, ensuring security even for identical plaintexts.
## Examples
plaintexts = ["data1", "data2", "data3"]
{:ok, encrypted_list} = SnmpKit.SnmpLib.Security.Priv.encrypt_batch(:aes256, priv_key, auth_key, plaintexts)
# Returns list of {ciphertext, priv_params} tuples
"""
@spec encrypt_batch(priv_protocol(), priv_key(), auth_key(), [plaintext()]) ::
{:ok, [{ciphertext(), priv_params()}]} | {:error, atom()}
def encrypt_batch(protocol, priv_key, auth_key, plaintexts) when is_list(plaintexts) do
case protocol_info(protocol) do
nil ->
{:error, :unsupported_protocol}
_spec ->
try do
encrypted_list =
Enum.map(plaintexts, fn plaintext ->
{:ok, {ciphertext, priv_params}} = encrypt(protocol, priv_key, auth_key, plaintext)
{ciphertext, priv_params}
end)
{:ok, encrypted_list}
rescue
_error ->
{:error, :batch_encryption_failed}
end
end
end
@doc """
Decrypts multiple ciphertexts in batch.
"""
@spec decrypt_batch(priv_protocol(), priv_key(), auth_key(), [{ciphertext(), priv_params()}]) ::
[:ok | {:error, atom()}]
def decrypt_batch(protocol, priv_key, auth_key, encrypted_list) when is_list(encrypted_list) do
Enum.map(encrypted_list, fn {ciphertext, priv_params} ->
case decrypt(protocol, priv_key, auth_key, ciphertext, priv_params) do
{:ok, plaintext} -> {:ok, plaintext}
{:error, reason} -> {:error, reason}
end
end)
end
## Performance Testing
@doc """
Measures encryption/decryption performance for a given protocol.
"""
@spec benchmark_protocol(priv_protocol(), priv_key(), auth_key(), plaintext(), pos_integer()) ::
map()
def benchmark_protocol(protocol, priv_key, auth_key, test_plaintext, iterations \\ 1000) do
Logger.info("Benchmarking #{protocol} privacy with #{iterations} iterations")
# Warm up
{:ok, {test_ciphertext, test_priv_params}} =
encrypt(protocol, priv_key, auth_key, test_plaintext)
# Time encryption operations
{encrypt_time, _} =
:timer.tc(fn ->
Enum.each(1..iterations, fn _i ->
encrypt(protocol, priv_key, auth_key, test_plaintext)
end)
end)
# Time decryption operations
{decrypt_time, _} =
:timer.tc(fn ->
Enum.each(1..iterations, fn _i ->
decrypt(protocol, priv_key, auth_key, test_ciphertext, test_priv_params)
end)
end)
plaintext_size = byte_size(test_plaintext)
ciphertext_size = byte_size(test_ciphertext)
%{
protocol: protocol,
iterations: iterations,
plaintext_size: plaintext_size,
ciphertext_size: ciphertext_size,
encrypt_time_microseconds: encrypt_time,
decrypt_time_microseconds: decrypt_time,
encrypt_ops_per_second: round(iterations / (encrypt_time / 1_000_000)),
decrypt_ops_per_second: round(iterations / (decrypt_time / 1_000_000)),
encrypt_throughput_mbps: round(plaintext_size * iterations / encrypt_time),
decrypt_throughput_mbps: round(ciphertext_size * iterations / decrypt_time),
avg_encrypt_microseconds: round(encrypt_time / iterations),
avg_decrypt_microseconds: round(decrypt_time / iterations)
}
end
## Private Implementation
defp validate_encryption_params(spec, priv_key, plaintext) do
with :ok <- validate_key_size(spec, priv_key),
:ok <- validate_plaintext(plaintext) do
:ok
end
end
defp validate_decryption_params(spec, priv_key, ciphertext, priv_params) do
with :ok <- validate_key_size(spec, priv_key),
:ok <- validate_ciphertext(spec, ciphertext),
:ok <- validate_privacy_params(spec, priv_params) do
:ok
end
end
defp validate_key_size(spec, priv_key) do
if byte_size(priv_key) == spec.key_size do
:ok
else
{:error, :invalid_key_size}
end
end
defp validate_plaintext(plaintext) when is_binary(plaintext) do
:ok
end
defp validate_plaintext(_), do: {:error, :invalid_plaintext}
defp validate_ciphertext(spec, ciphertext) do
if byte_size(ciphertext) > 0 and rem(byte_size(ciphertext), spec.block_size) == 0 do
:ok
else
{:error, :invalid_ciphertext}
end
end
defp validate_privacy_params(spec, priv_params) do
expected_size = spec.iv_size
if byte_size(priv_params) >= expected_size do
:ok
else
{:error, :invalid_privacy_params}
end
end
defp generate_iv(:des, spec, _auth_key) do
# DES uses random IV
iv = :crypto.strong_rand_bytes(spec.iv_size)
{:ok, iv}
end
defp generate_iv(protocol, spec, _auth_key) when protocol in [:aes128, :aes192, :aes256] do
# AES can use auth_key for IV generation or random
# For simplicity, using random IV (more secure)
iv = :crypto.strong_rand_bytes(spec.iv_size)
{:ok, iv}
end
defp apply_padding(data, block_size) when block_size > 1 do
# PKCS#7 padding
padding_length = block_size - rem(byte_size(data), block_size)
padding = binary_part(<<padding_length::8>>, 0, 1) |> String.duplicate(padding_length)
{:ok, data <> padding}
end
defp apply_padding(data, _block_size) do
{:ok, data}
end
defp remove_padding(padded_data, block_size) when block_size > 1 do
if byte_size(padded_data) == 0 do
{:error, :empty_data}
else
# Extract padding length from last byte
<<padding_length::8>> = binary_part(padded_data, byte_size(padded_data) - 1, 1)
if padding_length > 0 and padding_length <= block_size and
padding_length <= byte_size(padded_data) do
data_length = byte_size(padded_data) - padding_length
{:ok, binary_part(padded_data, 0, data_length)}
else
{:error, :invalid_padding}
end
end
end
defp remove_padding(data, _block_size) do
{:ok, data}
end
defp perform_encryption(spec, key, iv, plaintext) do
try do
ciphertext = :crypto.crypto_one_time(spec.algorithm, key, iv, plaintext, true)
{:ok, ciphertext}
rescue
_error ->
{:error, :encryption_failed}
end
end
defp perform_decryption(spec, key, iv, ciphertext) do
try do
plaintext = :crypto.crypto_one_time(spec.algorithm, key, iv, ciphertext, false)
{:ok, plaintext}
rescue
_error ->
{:error, :decryption_failed}
end
end
defp build_privacy_parameters(_protocol, iv) do
# Privacy parameters contain the IV for the receiving side
iv
end
defp extract_iv(protocol, priv_params) do
case protocol_info(protocol) do
%{iv_size: iv_size} when byte_size(priv_params) >= iv_size ->
iv = binary_part(priv_params, 0, iv_size)
{:ok, iv}
_ ->
{:error, :invalid_iv}
end
end
end