towerops/lib/snmpkit/snmp_lib/security/priv.ex
2026-01-23 12:52:17 -06:00

522 lines
15 KiB
Elixir

defmodule SnmpKit.SnmpLib.Security.Priv do
@moduledoc """
Implements SNMPv3 privacy protocols for message encryption and decryption.
This module provides support for standard SNMPv3 privacy protocols like DES and
AES, ensuring data confidentiality in SNMP communications.
## Supported Protocols
- `:none` - No privacy
- `:des` - DES-CBC (56-bit)
- `:aes128` - AES-CFB128 (128-bit)
- `:aes192` - AES-CFB128 (192-bit)
- `:aes256` - AES-CFB128 (256-bit)
## Security Considerations
- **DES is considered weak** and should only be used for compatibility with
legacy devices.
- **AES protocols are recommended** for strong encryption.
- Keys should be derived securely using the functions in `SnmpKit.SnmpLib.Security.Keys`.
## Protocol Selection Guidelines
- For new deployments, prefer `:aes256` for the strongest security.
- Use `:aes128` for a balance of performance and security.
- Use `:des` only when required for interoperability.
## Technical Details
This module implements the privacy aspects of the User-Based Security Model
(USM) as defined in RFC 3414 and RFC 3826.
### Key Derivation
Privacy keys are derived from the user's password and the authoritative SNMP
engine's ID. This process is handled by the `Keys` module.
### Initialization Vectors
For CBC and CFB modes, a unique Initialization Vector (IV) is required for each
encryption operation. This IV is generated and included in the `privParameters`
field of the SNMPv3 message.
### Padding
The plaintext data is padded to match the block size of the cipher before
encryption. This padding is removed upon decryption.
## Usage Examples
This module is typically used internally by the `USM` module.
### Message Encryption
# Assuming keys are derived and user is configured
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
)
assert decrypted == plaintext
### Protocol Information
iex> SnmpKit.SnmpLib.Security.Priv.protocol_info(:aes128)
%{algorithm: :aes_128_cfb128, key_size: 16, iv_size: 16, 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: %{
key_size: 0,
iv_size: 0,
block_size: 0,
algorithm: nil
},
des: %{
key_size: 8,
iv_size: 8,
block_size: 8,
algorithm: :des_cbc
},
aes128: %{
key_size: 16,
iv_size: 16,
block_size: 16,
algorithm: :aes_128_cfb128
},
aes192: %{
key_size: 24,
iv_size: 16,
block_size: 16,
algorithm: :aes_192_cfb128
},
aes256: %{
key_size: 32,
iv_size: 16,
block_size: 16,
algorithm: :aes_256_cfb128
}
}
@doc """
Retrieves the specification for a given privacy protocol.
Returns a map with `:algorithm`, `:key_size`, `:iv_size`, and `:block_size`,
or `nil` if the protocol is unsupported.
## Examples
iex> Priv.protocol_info(:aes128)
%{algorithm: :aes_128_cfb128, key_size: 16, iv_size: 16, block_size: 16}
iex> Priv.protocol_info(:unsupported)
nil
"""
@spec protocol_info(priv_protocol()) :: map() | nil
def protocol_info(protocol) do
@protocol_specs[protocol]
end
@doc """
Returns a list of all supported privacy protocols.
"""
@spec supported_protocols() :: [priv_protocol()]
def supported_protocols do
Map.keys(@protocol_specs)
end
@doc """
Returns a list of cryptographically secure protocols.
"""
@spec secure_protocols() :: [priv_protocol()]
def secure_protocols do
[:aes128, :aes192, :aes256]
end
@doc """
Checks if a protocol is considered cryptographically secure.
"""
@spec secure_protocol?(priv_protocol()) :: boolean()
def secure_protocol?(protocol) do
protocol in secure_protocols()
end
@doc """
Encrypts plaintext using the specified privacy protocol.
## Parameters
- `protocol`: Privacy protocol to use
- `priv_key`: Privacy key for the chosen protocol
- `auth_key`: Authentication key (used for IV generation)
- `plaintext`: Data to encrypt
## Returns
- `{:ok, {ciphertext, priv_params}}`: Encryption successful
- `{:error, reason}`: Encryption failed
## Examples
# AES-128 encryption
{:ok, {ciphertext, priv_params}} = SnmpKit.SnmpLib.Security.Priv.encrypt(
:aes128, priv_key, auth_key, "secret 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)
{: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
@doc """
Validates if a privacy key is compliant with the protocol's requirements.
## Examples
iex> Priv.validate_key(:aes128, :crypto.strong_rand_bytes(16))
:ok
iex> Priv.validate_key(:des, <<1, 2, 3>>)
{:error, :invalid_key_size}
"""
@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 ->
if byte_size(key) == spec.key_size do
:ok
else
Logger.warning("Privacy key wrong size for #{protocol}: #{byte_size(key)} != #{spec.key_size}")
{:error, :invalid_key_size}
end
end
end
def validate_key(_protocol, _key) do
{:error, :invalid_key_type}
end
@doc """
Encrypts a batch of plaintexts efficiently.
## Examples
iex> plaintexts = ["msg1", "msg2"]
iex> {:ok, encrypted_list} = Priv.encrypt_batch(:aes128, priv_key, auth_key, plaintexts)
iex> length(encrypted_list)
2
"""
@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) do
results =
Enum.map(plaintexts, fn plaintext ->
encrypt(protocol, priv_key, auth_key, plaintext)
end)
if Enum.all?(results, fn
{:ok, _} -> true
_ -> false
end) do
{:ok, Enum.map(results, fn {:ok, val} -> val end)}
else
{:error, :batch_encryption_failed}
end
end
@doc """
Decrypts a batch of ciphertexts efficiently.
"""
@spec decrypt_batch(
priv_protocol(),
priv_key(),
auth_key(),
[{ciphertext(), priv_params()}]
) :: [{:ok, plaintext()} | {:error, atom()}]
def decrypt_batch(protocol, priv_key, auth_key, encrypted_list) do
Enum.map(encrypted_list, fn {ciphertext, priv_params} ->
decrypt(protocol, priv_key, auth_key, ciphertext, priv_params)
end)
end
@doc """
Benchmarks the performance of a given privacy protocol.
"""
@spec benchmark_protocol(
priv_protocol(),
priv_key(),
auth_key(),
plaintext(),
non_neg_integer()
) ::
%{
encrypt_us: float(),
decrypt_us: float(),
ops_per_sec: float()
}
def benchmark_protocol(protocol, priv_key, auth_key, test_plaintext, iterations \\ 1000) do
# Warm-up run
case encrypt(protocol, priv_key, auth_key, test_plaintext) do
{:ok, {ciphertext, priv_params}} ->
decrypt(protocol, priv_key, auth_key, ciphertext, priv_params)
_ ->
:ok
end
# Encryption benchmark
encrypt_time =
fn ->
for _ <- 1..iterations do
encrypt(protocol, priv_key, auth_key, test_plaintext)
end
end
|> :timer.tc()
|> elem(0)
# Decryption benchmark
{:ok, {ciphertext, priv_params}} = encrypt(protocol, priv_key, auth_key, test_plaintext)
decrypt_time =
fn ->
for _ <- 1..iterations do
decrypt(protocol, priv_key, auth_key, ciphertext, priv_params)
end
end
|> :timer.tc()
|> elem(0)
total_time_us = encrypt_time + decrypt_time
ops = iterations * 2
ops_per_sec = ops / (total_time_us / 1_000_000)
%{
encrypt_us: encrypt_time / iterations,
decrypt_us: decrypt_time / iterations,
ops_per_sec: ops_per_sec
}
end
# --- Private Helper Functions ---
defp validate_encryption_params(spec, priv_key, plaintext) do
with :ok <- validate_key_size(spec, priv_key) do
validate_plaintext(plaintext)
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) do
validate_privacy_params(spec, priv_params)
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 rem(byte_size(ciphertext), spec.block_size) == 0 do
:ok
else
{:error, :invalid_ciphertext_size}
end
end
defp validate_privacy_params(spec, priv_params) do
if byte_size(priv_params) >= spec.iv_size do
:ok
else
{:error, :invalid_priv_params}
end
end
defp generate_iv(:des, spec, _auth_key) do
# DES uses a simpler IV generation
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 protocols use engineBoots and engineTime for IV, but for simplicity
# in this context, we'll use a strong random value.
# A full USM implementation would use the other parameters.
iv = :crypto.strong_rand_bytes(spec.iv_size)
{:ok, iv}
end
defp apply_padding(data, block_size) when is_binary(data) and block_size > 0 do
padding_size = block_size - rem(byte_size(data), block_size)
padding = :binary.copy(<<padding_size>>, padding_size)
{:ok, data <> padding}
end
defp apply_padding(data, _block_size) do
{:ok, data}
end
defp remove_padding(padded_data, block_size) when byte_size(padded_data) >= block_size do
padding_size = :binary.last(padded_data)
if padding_size > 0 and padding_size <= block_size do
data_size = byte_size(padded_data) - padding_size
if data_size >= 0 do
{:ok, :binary.part(padded_data, 0, data_size)}
else
{:error, :invalid_padding}
end
else
{:error, :invalid_padding}
end
end
defp remove_padding(data, _block_size) do
{:ok, data}
end
defp perform_encryption(spec, key, iv, plaintext) do
ciphertext = :crypto.crypto_one_time(spec.algorithm, key, iv, plaintext, true)
{:ok, ciphertext}
rescue
error ->
Logger.error("Encryption failed with algorithm #{spec.algorithm}: #{inspect(error)}")
{:error, :encryption_failed}
end
defp perform_decryption(spec, key, iv, ciphertext) do
plaintext = :crypto.crypto_one_time(spec.algorithm, key, iv, ciphertext, false)
{:ok, plaintext}
rescue
_error ->
{:error, :decryption_failed}
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