SnmpKit.SnmpLib.Security.Priv (snmpkit v1.4.0)

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), RFC 3414 section 8
  • :aes128 - AES-CFB128 (128-bit), RFC 3826
  • :aes192 - AES-CFB128 (192-bit), draft-blumenthal-aes-usm / draft-reeder-snmpv3-usm-3desede
  • :aes256 - AES-CFB128 (256-bit), same as above

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.
  • Privacy provides confidentiality only. Integrity comes from authentication; CFB-mode decryption with a wrong key silently yields garbage, which is why SNMPv3 forbids priv without auth.

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.

Keys

Privacy keys are localized keys produced by SnmpKit.SnmpLib.Security.Keys.

  • DES uses a 16-octet key: the first 8 octets are the DES key, the last 8 octets are the "pre-IV" (RFC 3414 section 8.1.1.1).
  • AES uses a 16, 24 or 32 octet key.

Initialization Vectors and privParameters

IVs are not random. They are derived from the authoritative engine's snmpEngineBoots / snmpEngineTime and a per-message salt, and only the salt travels on the wire as msgPrivacyParameters (8 octets for every protocol):

  • DES (RFC 3414 section 8.1.1.1): salt = engineBoots(4) || localInt(4), IV = preIV XOR salt.
  • AES (RFC 3826 section 3.1.2.1): IV = engineBoots(4) || engineTime(4) || salt(8).

The local integer is a 64-bit counter seeded randomly at first use and incremented for every message, so a (key, IV) pair is never reused.

Padding

DES-CBC requires the plaintext to be a multiple of 8 octets. RFC 3414 pads with arbitrary octets and does not transmit the padding length; the receiver relies on the BER length of the decrypted ScopedPDU. decrypt/6 therefore returns the padded plaintext for DES, and callers decode it as a BER SEQUENCE (trailing octets are ignored). AES-CFB is a stream mode and needs no padding.

Usage Examples

This module is typically used internally by the USM module.

Message Encryption

{:ok, {ciphertext, priv_params}} = SnmpKit.SnmpLib.Security.Priv.encrypt(
  :aes256, priv_key, auth_key, scoped_pdu,
  engine_boots: boots, engine_time: time
)

{:ok, decrypted} = SnmpKit.SnmpLib.Security.Priv.decrypt(
  :aes256, priv_key, auth_key, ciphertext, priv_params,
  engine_boots: boots, engine_time: time
)

Protocol Information

iex> SnmpKit.SnmpLib.Security.Priv.protocol_info(:aes128)
%{algorithm: :aes_128_cfb128, key_size: 16, iv_size: 16, block_size: 16, priv_params_size: 8}

Summary

Functions

Benchmarks the performance of a given privacy protocol.

Decrypts ciphertext using the specified privacy protocol.

Encrypts plaintext using the specified privacy protocol.

Encrypts a batch of plaintexts efficiently.

Retrieves the specification for a given privacy protocol.

Checks if a protocol is considered cryptographically secure.

Returns a list of cryptographically secure protocols.

Returns a list of all supported privacy protocols.

Validates if a privacy key is compliant with the protocol's requirements.

Types

auth_key()

@type auth_key() :: binary()

ciphertext()

@type ciphertext() :: binary()

initialization_vector()

@type initialization_vector() :: binary()

plaintext()

@type plaintext() :: binary()

priv_key()

@type priv_key() :: binary()

priv_opts()

@type priv_opts() :: [
  engine_boots: non_neg_integer(),
  engine_time: non_neg_integer(),
  salt: binary() | non_neg_integer()
]

priv_params()

@type priv_params() :: binary()

priv_protocol()

@type priv_protocol() :: :none | :des | :aes128 | :aes192 | :aes256

Functions

benchmark_protocol(protocol, priv_key, auth_key, test_plaintext, iterations \\ 1000)

@spec benchmark_protocol(
  priv_protocol(),
  priv_key(),
  auth_key(),
  plaintext(),
  non_neg_integer()
) :: %{encrypt_us: float(), decrypt_us: float(), ops_per_sec: float()}

Benchmarks the performance of a given privacy protocol.

decrypt(protocol, priv_key, auth_key, ciphertext, priv_params, opts \\ [])

@spec decrypt(
  priv_protocol(),
  priv_key(),
  auth_key(),
  ciphertext(),
  priv_params(),
  priv_opts()
) ::
  {:ok, plaintext()} | {:error, atom()}

Decrypts ciphertext using the specified privacy protocol.

Parameters

  • protocol: Privacy protocol used for encryption
  • priv_key: Localized privacy key (same as used for encryption)
  • auth_key: Authentication key (unused by the RFC algorithms, kept for API compatibility)
  • ciphertext: Encrypted data
  • priv_params: The 8-octet msgPrivacyParameters salt from the received message
  • opts: :engine_boots / :engine_time as carried in the received msgAuthoritativeEngineBoots / msgAuthoritativeEngineTime (needed for AES)

Returns

  • {:ok, plaintext}: Decryption successful. For DES the result still carries the RFC 3414 block padding; decode it as a BER SEQUENCE and ignore the tail.
  • {:error, reason}: Decryption failed

Note that a wrong key cannot be detected here: CFB and unpadded CBC produce garbage rather than an error. Verify authentication before decrypting.

decrypt_batch(protocol, priv_key, auth_key, encrypted_list, opts \\ [])

@spec decrypt_batch(
  priv_protocol(),
  priv_key(),
  auth_key(),
  [{ciphertext(), priv_params()}],
  priv_opts()
) :: [ok: plaintext(), error: atom()]

Decrypts a batch of ciphertexts efficiently.

encrypt(protocol, priv_key, auth_key, plaintext, opts \\ [])

@spec encrypt(priv_protocol(), priv_key(), auth_key(), plaintext(), priv_opts()) ::
  {:ok, {ciphertext(), priv_params()}} | {:error, atom()}

Encrypts plaintext using the specified privacy protocol.

Parameters

  • protocol: Privacy protocol to use
  • priv_key: Localized privacy key for the chosen protocol
  • auth_key: Authentication key (unused by the RFC algorithms, kept for API compatibility)
  • plaintext: Data to encrypt (normally the BER-encoded ScopedPDU)
  • opts:
    • :engine_boots - authoritative snmpEngineBoots placed in the message (default 0)
    • :engine_time - authoritative snmpEngineTime placed in the message (default 0)
    • :salt - explicit 8-octet salt / 64-bit integer, mainly for tests; a fresh counter value is used when omitted

Returns

  • {:ok, {ciphertext, priv_params}}: Encryption successful. priv_params is the 8-octet salt to send as msgPrivacyParameters.
  • {:error, reason}: Encryption failed

Examples

{:ok, {ciphertext, priv_params}} = SnmpKit.SnmpLib.Security.Priv.encrypt(
  :aes128, priv_key, auth_key, scoped_pdu, engine_boots: 3, engine_time: 1200
)

encrypt_batch(protocol, priv_key, auth_key, plaintexts, opts \\ [])

@spec encrypt_batch(
  priv_protocol(),
  priv_key(),
  auth_key(),
  [plaintext()],
  priv_opts()
) ::
  {:ok, [{ciphertext(), priv_params()}]} | {:error, atom()}

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

protocol_info(protocol)

@spec protocol_info(priv_protocol()) :: map() | nil

Retrieves the specification for a given privacy protocol.

Returns a map with :algorithm, :key_size, :iv_size, :block_size and :priv_params_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, priv_params_size: 8}

iex> Priv.protocol_info(:unsupported)
nil

secure_protocol?(protocol)

@spec secure_protocol?(priv_protocol()) :: boolean()

Checks if a protocol is considered cryptographically secure.

secure_protocols()

@spec secure_protocols() :: [priv_protocol()]

Returns a list of cryptographically secure protocols.

supported_protocols()

@spec supported_protocols() :: [priv_protocol()]

Returns a list of all supported privacy protocols.

validate_key(protocol, key)

@spec validate_key(priv_protocol(), priv_key()) :: :ok | {:error, atom()}

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}