SnmpKit.SnmpLib.Security.Keys (snmpkit v2.0.1)

Key derivation and management for SNMPv3 User Security Model.

Implements RFC 3414 compliant key derivation functions for converting user passwords into cryptographic keys suitable for authentication and privacy operations.

Key Derivation Process

SNMPv3 uses a two-step key derivation process:

  1. Password Localization: Transform user password into a localized key using the authoritative engine ID
  2. Key Expansion: Derive authentication and privacy keys from the localized key based on protocol requirements

Security Properties

  • Keys are derived deterministically from passwords and engine IDs
  • Different engine IDs produce different keys for the same password
  • Key derivation uses cryptographic hash functions for security
  • Derived keys cannot be used to recover original passwords
  • Each protocol type (auth/priv) uses different key derivation parameters

Supported Algorithms

Authentication Key Derivation

  • MD5: RFC 3414 compliant (deprecated)
  • SHA-1: RFC 3414 compliant (deprecated)
  • SHA-224: RFC 7860 compliant
  • SHA-256: RFC 7860 compliant (recommended)
  • SHA-384: RFC 7860 compliant
  • SHA-512: RFC 7860 compliant

Privacy Key Derivation

Per RFC 3414 section 8.1.1.1 and RFC 3826 section 1.2 the privacy key is the authentication protocol's password-to-key algorithm applied to the privacy password and localized with the engine ID, truncated to the cipher's key size:

  • DES: 16 bytes (8-byte DES key followed by the 8-byte pre-IV)
  • AES-128: 16 bytes
  • AES-192: 24 bytes
  • AES-256: 32 bytes

When the hash output is shorter than the cipher key (e.g. MD5/SHA-1 with AES-192/256) the key is extended. Two extension schemes exist in the wild: :reeder (draft-reeder-snmpv3-usm-3desede, the net-snmp default) and :blumenthal (draft-blumenthal-aes-usm). Pass key_extension: to choose; the default is :reeder.

Usage Examples

Authentication Key Derivation

# Derive SHA-256 authentication key
engine_id = <<0x80, 0x00, 0x1f, 0x88, 0x80, 0x01, 0x02, 0x03, 0x04>>
password = "authentication_password"

{:ok, auth_key} = SnmpKit.SnmpLib.Security.Keys.derive_auth_key(:sha256, password, engine_id)

Privacy Key Derivation

# Derive AES-256 privacy key for a user authenticating with SHA-256
{:ok, priv_key} = SnmpKit.SnmpLib.Security.Keys.derive_priv_key(
  :aes256, password, engine_id, auth_protocol: :sha256
)

# Or reuse the localized authentication key when both passwords are the same
{:ok, priv_key} = SnmpKit.SnmpLib.Security.Keys.derive_priv_key_from_auth(:aes256, auth_key, engine_id)

Key Validation

# Validate key strength
:ok = SnmpKit.SnmpLib.Security.Keys.validate_password_strength(password)
{:error, :too_short} = SnmpKit.SnmpLib.Security.Keys.validate_password_strength("weak")

Summary

Functions

Derives authentication key from password and engine ID.

Derives multiple authentication keys for different protocols from the same password.

Derives privacy key from password and engine ID.

Derives privacy key from an existing localized authentication key.

Generates a cryptographically secure random password.

Securely compares two derived keys to prevent timing attacks.

Validates password strength according to SNMPv3 security guidelines.

Types

auth_protocol()

@type auth_protocol() :: :md5 | :sha1 | :sha224 | :sha256 | :sha384 | :sha512

derived_key()

@type derived_key() :: binary()

engine_id()

@type engine_id() :: binary()

password()

@type password() :: binary()

priv_protocol()

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

salt()

@type salt() :: binary()

Functions

derive_auth_key(protocol, password, engine_id)

@spec derive_auth_key(auth_protocol(), password(), engine_id()) ::
  {:ok, derived_key()} | {:error, atom()}

Derives authentication key from password and engine ID.

Implements RFC 3414 key localization algorithm for authentication protocols. The derived key is specific to the combination of password, protocol, and engine ID.

Parameters

  • protocol: Authentication protocol (:md5, :sha1, :sha256, etc.)
  • password: User password (minimum 8 characters recommended)
  • engine_id: Authoritative engine ID (5-32 bytes)

Returns

  • {:ok, key}: Successfully derived authentication key
  • {:error, reason}: Key derivation failed

Examples

# SHA-256 authentication key (recommended)
{:ok, key} = SnmpKit.SnmpLib.Security.Keys.derive_auth_key(
  :sha256, "my_secure_password", engine_id
)

# Legacy MD5 key derivation
{:ok, key} = SnmpKit.SnmpLib.Security.Keys.derive_auth_key(
  :md5, "legacy_password", engine_id
)

derive_auth_keys_multi(protocols, password, engine_id)

@spec derive_auth_keys_multi([auth_protocol()], password(), engine_id()) ::
  {:ok, %{required(auth_protocol()) => derived_key()}} | {:error, atom()}

Derives multiple authentication keys for different protocols from the same password.

Useful when supporting multiple authentication protocols simultaneously.

Examples

protocols = [:sha256, :sha384, :sha512]
{:ok, keys} = SnmpKit.SnmpLib.Security.Keys.derive_auth_keys_multi(protocols, password, engine_id)
# Returns: %{sha256: key1, sha384: key2, sha512: key3}

derive_priv_key(protocol, password, engine_id, opts \\ [])

@spec derive_priv_key(priv_protocol(), password(), engine_id(), keyword()) ::
  {:ok, derived_key()} | {:error, atom()}

Derives privacy key from password and engine ID.

Implements RFC 3414 section 8.1.1.1 / RFC 3826 section 1.2: the privacy password is run through the authentication protocol's password-to-key algorithm, localized with the engine ID, and cut (or extended) to the cipher key size.

Parameters

  • protocol: Privacy protocol (:des, :aes128, :aes192, :aes256)
  • password: User password for privacy
  • engine_id: Authoritative engine ID
  • opts:
    • :auth_protocol - the user's authentication protocol, which selects the hash (default :md5, the RFC 3414 baseline)
    • :key_extension - :reeder (default, net-snmp compatible) or :blumenthal for AES keys longer than the hash output

Returns

  • {:ok, key}: Successfully derived privacy key
  • {:error, reason}: Key derivation failed

Examples

# AES-256 privacy key for a SHA-256 user (recommended)
{:ok, key} = SnmpKit.SnmpLib.Security.Keys.derive_priv_key(
  :aes256, "privacy_password", engine_id, auth_protocol: :sha256
)

# DES privacy key (legacy, MD5 based)
{:ok, key} = SnmpKit.SnmpLib.Security.Keys.derive_priv_key(
  :des, "legacy_privacy_password", engine_id
)

derive_priv_key_from_auth(protocol, auth_key, engine_id, opts \\ [])

@spec derive_priv_key_from_auth(
  priv_protocol(),
  derived_key(),
  engine_id(),
  keyword()
) ::
  {:ok, derived_key()} | {:error, atom()}

Derives privacy key from an existing localized authentication key.

Only valid when the privacy password equals the authentication password: the localized key is then the same for both, and this skips the expensive password-to-key step. The hash used for key extension is inferred from the key length unless auth_protocol: is given.

Examples

{:ok, auth_key} = derive_auth_key(:sha256, password, engine_id)

{:ok, priv_key} = SnmpKit.SnmpLib.Security.Keys.derive_priv_key_from_auth(
  :aes256, auth_key, engine_id
)

generate_secure_password(length \\ 16)

@spec generate_secure_password(pos_integer()) :: password()

Generates a cryptographically secure random password.

Examples

password = SnmpKit.SnmpLib.Security.Keys.generate_secure_password(16)
# Returns: "K7mN9pQ2rT8vW3xZ" (example)

secure_compare(key1, key2)

@spec secure_compare(derived_key(), derived_key()) :: boolean()

Securely compares two derived keys to prevent timing attacks.

Examples

true = SnmpKit.SnmpLib.Security.Keys.secure_compare(key1, key1)
false = SnmpKit.SnmpLib.Security.Keys.secure_compare(key1, key2)

validate_password_strength(password)

@spec validate_password_strength(password()) ::
  :ok | {:error, atom()} | {:warning, atom()}

Validates password strength according to SNMPv3 security guidelines.

Requirements

  • Minimum 8 characters (RFC recommendation)
  • Should contain mix of character types for security
  • Should not be based on dictionary words

Examples

:ok = SnmpKit.SnmpLib.Security.Keys.validate_password_strength("strong_password_123")
{:error, :too_short} = SnmpKit.SnmpLib.Security.Keys.validate_password_strength("weak")
{:warning, :weak_complexity} = SnmpKit.SnmpLib.Security.Keys.validate_password_strength("password")