520 lines
15 KiB
Elixir
520 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> length(Priv.encrypt_batch(:aes128, <<1::128>>, <<1::128>>, ["msg1"]))
|
|
1
|
|
"""
|
|
@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
|