# -*- mode: perl -*-
# ============================================================================

package Net::SNMP::Security::USM;

# $Id: USM.pm,v 3.1 2005/10/20 14:17:01 dtown Rel $

# Object that implements the SNMPv3 User-based Security Model.

# Copyright (c) 2001-2005 David M. Town <dtown@cpan.org>
# All rights reserved.

# This program is free software; you may redistribute it and/or modify it
# under the same terms as Perl itself.

# ============================================================================

use strict;

use Net::SNMP::Security qw( :ALL );

use Net::SNMP::Message qw(
   :msgFlags asn1_itoa OCTET_STRING SEQUENCE INTEGER SNMP_VERSION_3 TRUE FALSE
);

use Crypt::DES();
use Digest::MD5();
use Digest::SHA1();
use Digest::HMAC();

## Version of the Net::SNMP::Security::USM module

our $VERSION = v3.0.1;

## Handle importing/exporting of symbols

use Exporter();

our @ISA = qw( Net::SNMP::Security Exporter );

our @EXPORT_OK;

our %EXPORT_TAGS = (
   authprotos => [
      qw( AUTH_PROTOCOL_NONE AUTH_PROTOCOL_HMACMD5 AUTH_PROTOCOL_HMACSHA )
   ],
   levels     => [
      qw( SECURITY_LEVEL_NOAUTHNOPRIV SECURITY_LEVEL_AUTHNOPRIV
          SECURITY_LEVEL_AUTHPRIV )
   ],
   models     => [
      qw( SECURITY_MODEL_ANY SECURITY_MODEL_SNMPV1 SECURITY_MODEL_SNMPV2C
          SECURITY_MODEL_USM )
   ],
   privprotos => [
      qw( PRIV_PROTOCOL_NONE PRIV_PROTOCOL_DES PRIV_PROTOCOL_AESCFB128
          PRIV_PROTOCOL_DRAFT_3DESEDE PRIV_PROTOCOL_DRAFT_AESCFB128
          PRIV_PROTOCOL_DRAFT_AESCFB192 PRIV_PROTOCOL_DRAFT_AESCFB256 )  
   ]
);

Exporter::export_ok_tags( qw( authprotos levels models privprotos ) );

$EXPORT_TAGS{ALL} = [ @EXPORT_OK ];

## RCC 3414 - Authentication protocols

sub AUTH_PROTOCOL_NONE()    { '1.3.6.1.6.3.10.1.1.1' } # usmNoAuthProtocol
sub AUTH_PROTOCOL_HMACMD5() { '1.3.6.1.6.3.10.1.1.2' } # usmHMACMD5AuthProtocol
sub AUTH_PROTOCOL_HMACSHA() { '1.3.6.1.6.3.10.1.1.3' } # usmHMACSHAAuthProtocol

## RFC 3414 - Privacy protocols

sub PRIV_PROTOCOL_NONE()    { '1.3.6.1.6.3.10.1.2.1' } # usmNoPrivProtocol
sub PRIV_PROTOCOL_DES()     { '1.3.6.1.6.3.10.1.2.2' } # usmDESPrivProtocol

## RFC 3826 - The AES Cipher Algorithm in the SNMP USM 

# usmAesCfb128Protocol
sub PRIV_PROTOCOL_AESCFB128()        {  '1.3.6.1.6.3.10.1.2.4' }

# The privacy protocols below have been implemented using the draft 
# specifications intended to extend the User-based Security Model 
# defined in RFC 3414.  Since the object definitions have not been 
# standardized, they have been based on the Extended Security Options 
# Consortium MIB found at http://www.snmp.com/eso/esoConsortiumMIB.txt.

# Extension to Support Triple-DES EDE <draft-reeder-snmpv3-usm-3desede-00.txt> 
# Reeder and Gudmunsson; October 1999, expired April 2000 

# usm3DESPrivProtocol 
sub PRIV_PROTOCOL_DRAFT_3DESEDE()    { '1.3.6.1.4.1.14832.1.1' }

# AES Cipher Algorithm in the USM <draft-blumenthal-aes-usm-04.txt>
# Blumenthal, Maino, and McCloghrie; October 2002, expired April 2003 

# usmAESCfb128PrivProtocol 
sub PRIV_PROTOCOL_DRAFT_AESCFB128()  { '1.3.6.1.4.1.14832.1.2' } 

# usmAESCfb192PrivProtocol 
sub PRIV_PROTOCOL_DRAFT_AESCFB192()  { '1.3.6.1.4.1.14832.1.3' } 

# usmAESCfb256PrivProtocol
sub PRIV_PROTOCOL_DRAFT_AESCFB256()  { '1.3.6.1.4.1.14832.1.4' } 

## Package variables

our $ENGINE_ID;               # Our authoritative snmpEngineID                                                         
# [public methods] -----------------------------------------------------------

sub new
{
   my ($class, %argv) = @_;

   # Create a new data structure for the object
   my $this = bless {
      '_error'              => undef,                 # Error message
      '_version'            => SNMP_VERSION_3,        # version 
      '_authoritative'      => FALSE,                 # Authoritative flag
      '_discovered'         => FALSE,                 # Engine discovery flag
      '_synchronized'       => FALSE,                 # Synchronization flag
      '_engine_id'          => '',                    # snmpEngineID
      '_engine_boots'       => 0,                     # snmpEngineBoots
      '_engine_time'        => 0,                     # snmpEngineTime
      '_latest_engine_time' => 0,                     # latestReceivedEngineTime
      '_time_epoc'          => time(),                # snmpEngineBoots epoc
      '_user_name'          => '',                    # securityName 
      '_auth_data'          => undef,                 # Authentication data
      '_auth_key'           => undef,                 # authKey 
      '_auth_password'      => undef,                 # Authentication password 
      '_auth_protocol'      => AUTH_PROTOCOL_HMACMD5, # authProtocol
      '_priv_data'          => undef,                 # Privacy data
      '_priv_key'           => undef,                 # privKey 
      '_priv_password'      => undef,                 # Privacy password
      '_priv_protocol'      => PRIV_PROTOCOL_DES,     # privProtocol
      '_security_level'     => SECURITY_LEVEL_NOAUTHNOPRIV
   }, $class;

   # We first need to find out if we are an authoritative SNMP
   # engine and set the authProtocol and privProtocol if they 
   # have been provided.

   foreach (keys %argv) {

      if (/^-?authoritative$/i) {
         $this->{_authoritative} = (delete($argv{$_})) ? TRUE : FALSE;
      } elsif (/^-?authprotocol$/i) {
         $this->_auth_protocol(delete($argv{$_}));
      } elsif (/^-?privprotocol$/i) {
         $this->_priv_protocol(delete($argv{$_}));
      }

      if (defined($this->{_error})) {
         return wantarray ? (undef, $this->{_error}) : undef;
      }
   }

   # Now validate the rest of the passed arguments

   foreach (keys %argv) {

      if (/^-?version$/i) {
         $this->_version($argv{$_});
      } elsif (/^-?debug$/i) {
         $this->debug($argv{$_});
      } elsif ((/^-?engineid$/i) && ($this->{_authoritative})) {
         $this->_engine_id($argv{$_});
      } elsif (/^-?username$/i) {
         $this->_user_name($argv{$_});
      } elsif (/^-?authkey$/i) {
         $this->_auth_key($argv{$_}); 
      } elsif (/^-?authpassword$/i) {
         $this->_auth_password($argv{$_});
      } elsif (/^-?privkey$/i) {
         $this->_priv_key($argv{$_});
      } elsif (/^-?privpassword$/i) {
         $this->_priv_password($argv{$_});
      } else {
         $this->_error("Invalid argument '%s'", $_);
      }

      if (defined($this->{_error})) {
         return wantarray ? (undef, $this->{_error}) : undef;
      }

   }

   # Generate a snmpEngineID and populate the object accordingly
   # if we are an authoritative snmpEngine.
 
   $this->_snmp_engine_init if ($this->{_authoritative});

   # Define the securityParameters
   if (!defined($this->_security_params)) {
      return wantarray ? (undef, $this->{_error}) : undef;
   }

   # Return the object and an empty error message (in list context)
   wantarray ? ($this, '') : $this;
}

sub generate_request_msg
{
   my ($this, $pdu, $msg) = @_;

   # Clear any previous errors
   $this->_error_clear;

   return $this->_error('Required PDU and/or Message missing') unless (@_ == 3);

   # Validate the SNMP version of the PDU
   if ($pdu->version != $this->{_version}) {
      return $this->_error('Invalid version [%d]', $pdu->version);
   }

   # Validate the securityLevel of the PDU
   if ($pdu->security_level > $this->{_security_level}) {
      return $this->_error(
         'Unsupported securityLevel [%d]', $pdu->security_level
      );
   }

   # Validate PDU type with snmpEngine type
   if ($pdu->expect_response) {
      if ($this->{_authoritative}) {
         return $this->_error(
            'Must be a non-authoritative SNMP engine to generate a %s', 
            asn1_itoa($pdu->pdu_type)
         );
      }
   } else {
      if (!$this->{_authoritative}) {
         return $this->_error(
            'Must be an authoritative SNMP engine to generate a %s',
            asn1_itoa($pdu->pdu_type)
         );
      }
   }

   # Extract the msgGlobalData out of the message
   my $msg_global_data = $msg->clear;

   # AES in the USM Section 3.1.2.1 - "The 128-bit IV is obtained as
   # the concatenation of the... ...snmpEngineBoots, ...snmpEngineTime,
   # and a local 64-bit integer.  We store the current snmpEngineBoots
   # and snmpEngineTime before encrypting the PDU so that the computed
   # IV matches the transmitted msgAuthoritativeEngineBoots and
   # msgAuthoritativeEngineTime.

   my $msg_engine_time  = $this->_engine_time;
   my $msg_engine_boots = $this->_engine_boots;

   # Copy the PDU into a "plain text" buffer
   my $pdu_buffer  = $pdu->copy;
   my $priv_params = '';

   # encryptedPDU::=OCTET STRING
   if ($pdu->security_level > SECURITY_LEVEL_AUTHNOPRIV) {
      if (!defined($this->_encrypt_data($msg, $priv_params, $pdu_buffer))) {
         return $this->_error;
      }
   }

   # msgPrivacyParameters::=OCTET STRING
   if (!defined($msg->prepare(OCTET_STRING, $priv_params))) {
      return $this->_error($msg->error);
   }

   # msgAuthenticationParameters::=OCTET STRING

   my $auth_params = '';
   my $auth_location = 0;

   if ($pdu->security_level > SECURITY_LEVEL_NOAUTHNOPRIV) {
   
      # Save the location to fill in msgAuthenticationParameters later
      $auth_location = $msg->length + 12 + length($pdu_buffer);

      # Set the msgAuthenticationParameters to all zeros
      $auth_params = pack('x12');
   }

   if (!defined($msg->prepare(OCTET_STRING, $auth_params))) {
      return $this->_error($msg->error);
   }

   # msgUserName::=OCTET STRING 
   if (!defined($msg->prepare(OCTET_STRING, $pdu->security_name))) {
      return $this->_error($msg->error);
   } 

   # msgAuthoritativeEngineTime::=INTEGER  
   if (!defined($msg->prepare(INTEGER, $msg_engine_time))) {
      return $this->_error($msg->error);
   }

   # msgAuthoritativeEngineBoots::=INTEGER
   if (!defined($msg->prepare(INTEGER, $msg_engine_boots))) {
      return $this->_error($msg->error);
   }

   # msgAuthoritativeEngineID
   if (!defined($msg->prepare(OCTET_STRING, $this->_engine_id))) {
      return $this->_error($msg->error);
   }

   # UsmSecurityParameters::= SEQUENCE
   if (!defined($msg->prepare(SEQUENCE))) {
      return $this->_error($msg->error);
   }

   # msgSecurityParameters::=OCTET STRING
   if (!defined($msg->prepare(OCTET_STRING, $msg->clear))) {
      return $this->_error($msg->error);
   }

   # Append the PDU
   if (!defined($msg->append($pdu_buffer))) {
      return $this->_error($msg->error);
   }

   # Prepend the msgGlobalData
   if (!defined($msg->prepend($msg_global_data))) {
      return $this->_error($msg->error);
   }

   # version::=INTEGER
   if (!defined($msg->prepare(INTEGER, $this->{_version}))) {
      return $this->_error($msg->error);
   }

   # message::=SEQUENCE
   if (!defined($msg->prepare(SEQUENCE))) {
      return $this->_error($msg->error);
   }

   # Apply authentication
   if ($pdu->security_level > SECURITY_LEVEL_NOAUTHNOPRIV) {
      if (!defined($this->_authenticate_outgoing_msg($msg, $auth_location))) {
         return $this->_error($msg->error);
      }
   }

   # Return the Message
   $msg;
}

sub process_incoming_msg
{
   my ($this, $msg) = @_;

   # Clear any previous errors
   $this->_error_clear;

   return $this->_error('Required Message missing') unless (@_ == 2);

   # msgSecurityParameters::=OCTET STRING

   my $msg_params = $msg->process(OCTET_STRING);
   return $this->_error($msg->error) unless defined($msg_params);

   # Need to move the buffer index back to the begining of the data
   # portion of the OCTET STRING that contains the msgSecurityParameters.

   $msg->index($msg->index - length($msg_params));

   # UsmSecurityParameters::=SEQUENCE
   return $this->_error($msg->error) unless defined($msg->process(SEQUENCE));

   # msgAuthoritativeEngineID::=OCTET STRING
   my $msg_engine_id;
   if (!defined($msg_engine_id = $msg->process(OCTET_STRING))) {
      return $this->_error($msg->error);
   }

   # msgAuthoritativeEngineBoots::=INTEGER (0..2147483647)
   my $msg_engine_boots;
   if (!defined($msg_engine_boots = $msg->process(INTEGER))) {
      return $this->_error($msg->error); 
   }
   if (($msg_engine_boots < 0) || ($msg_engine_boots > 2147483647)) {
      return $this->_error(
         'Invalid incoming msgAuthoritativeEngineBoots value [%d]', 
         $msg_engine_boots 
      );
   }

   # msgAuthoritativeEngineTime::=INTEGER (0..2147483647)
   my $msg_engine_time;
   if (!defined($msg_engine_time = $msg->process(INTEGER))) {
      return $this->_error($msg->error);
   }
   if (($msg_engine_time < 0) || ($msg_engine_time > 2147483647)) {
      return $this->_error(
         'Invalid incoming msgAuthoritativeEngineTime value [%d]', 
         $msg_engine_time
      );
   }

   # msgUserName::=OCTET STRING (SIZE(0..32))
   if (!defined($msg->security_name($msg->process(OCTET_STRING)))) {
      return $this->_error($msg->error); 
   }

   # msgAuthenticationParameters::=OCTET STRING
   my $auth_params;
   if (!defined($auth_params = $msg->process(OCTET_STRING))) {
      return $this->_error($msg->error); 
   }

   # We need to zero out the msgAuthenticationParameters in order 
   # to compute the HMAC properly.

   if (my $len = length($auth_params)) {
      if ($len != 12) {
         return $this->_error(
            'Invalid incoming msgAuthenticationParameters length [%d octet%s]',
            $len, $len != 1 ? 's' : ''
         );
      }
      substr(${$msg->reference}, ($msg->index - 12), 12) = pack('x12');
   }

   # msgPrivacyParameters::=OCTET STRING
   my $priv_params;
   if (!defined($priv_params = $msg->process(OCTET_STRING))) {
      return $this->_error($msg->error); 
   }

   # Validate the msgAuthoritativeEngineID and msgUserName
  
   if ($this->{_discovered}) {

      if ($msg_engine_id ne $this->_engine_id) {
         return $this->_error(
            'Unknown incoming msgAuthoritativeEngineID [%s]', 
            unpack('H*', $msg_engine_id)
         );
      }

      if ($msg->security_name ne $this->_user_name) {
         return $this->_error(
            'Unknown incoming msgUserName [%s]', $msg->security_name 
         );
      }

   } else {

      # Handle authoritativeEngineID discovery
      if (!defined($this->_engine_id_discovery($msg_engine_id))) {
         return $this->_error;
      }

   }

   # Validate the incoming securityLevel

   my $security_level = $msg->security_level;

   if ($security_level > $this->{_security_level}) {
      return $this->_error(
          'Unsupported incoming securityLevel [%d]', $security_level
      );
   }
 
   if ($security_level > SECURITY_LEVEL_NOAUTHNOPRIV) {

      # Authenticate the message
      if (!defined($this->_authenticate_incoming_msg($msg, $auth_params))) { 
         return $this->_error;
      }

      # Synchronize the time
      if (!$this->_synchronize($msg_engine_boots, $msg_engine_time)) {
         return $this->_error;
      }

      # Check for timeliness
      if (!defined($this->_timeliness($msg_engine_boots, $msg_engine_time))) {
         return $this->_error;
      }

      if ($security_level > SECURITY_LEVEL_AUTHNOPRIV) {

         # Validate the msgPrivacyParameters length.

         if (length($priv_params) != 8) {
            return $this->_error(
               'Invalid incoming msgPrivacyParameters length [%d octet%s]', 
               length($priv_params), length($priv_params) != 1 ? 's' : '' 
            );
         }

         # AES in the USM Section 3.1.2.1 - "The 128-bit IV is
         # obtained as the concatenation of the... ...snmpEngineBoots,
         # ...snmpEngineTime, and a local 64-bit integer.  ...The
         # 64-bit integer must be placed in the msgPrivacyParameters
         # field..."  We must prepend the snmpEngineBoots and
         # snmpEngineTime as received in order to compute the IV.

         if (($this->{_priv_protocol} eq PRIV_PROTOCOL_AESCFB128)       ||
             ($this->{_priv_protocol} eq PRIV_PROTOCOL_DRAFT_AESCFB192) ||
             ($this->{_priv_protocol} eq PRIV_PROTOCOL_DRAFT_AESCFB256))
         {
            substr($priv_params, 0, 0) = pack(
               'NN', $msg_engine_boots, $msg_engine_time
            );
         }

         # encryptedPDU::=OCTET STRING

         $this->_decrypt_data($msg, $priv_params, $msg->process(OCTET_STRING));

      } else {

         TRUE;

      }

   } else {

      TRUE;

   }
}

sub user_name
{
   $_[0]->{_user_name};
}

sub auth_protocol
{
   my ($this) = @_;

   if ($this->{_security_level} > SECURITY_LEVEL_NOAUTHNOPRIV) {
      $this->{_auth_protocol};
   } else {
      AUTH_PROTOCOL_NONE;
   }
}

sub auth_key
{
   $_[0]->{_auth_key};
}

sub priv_protocol
{
   my ($this) = @_;

   if ($this->{_security_level} > SECURITY_LEVEL_AUTHNOPRIV) {
      $this->{_priv_protocol};
   } else {
      PRIV_PROTOCOL_NONE;
   }
}

sub priv_key
{
   $_[0]->{_priv_key};
}

sub engine_id
{
   $_[0]->{_engine_id};
}

sub engine_boots
{
   $_[0]->_engine_boots;
}

sub engine_time
{
   $_[0]->_engine_time;
}

sub security_level
{
   $_[0]->{_security_level};
}

sub security_model
{
   # RFC 3411 - SnmpSecurityModel::=TEXTUAL-CONVENTION

   SECURITY_MODEL_USM;
}

sub security_name
{
   $_[0]->_user_name;
}

sub discovered
{
   my ($this) = @_;

   if ($this->{_security_level} > SECURITY_LEVEL_NOAUTHNOPRIV) {
      ($this->{_discovered} && $this->{_synchronized});
   } else {
      $this->{_discovered};
   }
}

# [private methods] ----------------------------------------------------------

sub _version
{
   my ($this, $version) = @_;

   if ($version != SNMP_VERSION_3) {
      return $this->_error('Invalid SNMP version specified [%s]', $version);
   }

   $this->{_version} = $version;
}

sub _engine_id
{
   my ($this, $engine_id) = @_;

   if (@_ == 2) {
      if ($engine_id =~ /^(?i:0x)?([a-fA-F0-9]{10,64})$/) {
         $this->{_engine_id} = pack('H*', length($1) % 2 ? '0'.$1 : $1);
      } else {
         return $this->_error('Invalid authoritativeEngineID format specified');
      }
   }

   $this->{_engine_id};
}

sub _user_name
{
   my ($this, $user_name) = @_;

   if (@_ == 2) {
      if ($user_name eq '') {
         return $this->_error('Empty userName specified');
      } elsif (length($user_name) > 32) {
         return $_[0]->_error(
            'Invalid userName length [%d octet%s]', 
            length($user_name), length($user_name) != 1 ? 's' : ''
         );
      }
      $this->{_user_name} = $user_name;
   }

   # RFC 3414 Section 4 - "Discovery... ...msgUserName of zero-length..."

   ($this->{_discovered}) ? $this->{_user_name} : '';
}

sub _snmp_engine_init
{
   my ($this) = @_;

   if ($this->{_engine_id} eq '') {

      # Initialize our snmpEngineID using the algorithm described 
      # in RFC 3411 - SnmpEngineID::=TEXTUAL-CONVENTION.

      # The first bit is set to one to indicate that the RFC 3411
      # algorithm is being used.  The first fours bytes are to be
      # the agent's SNMP management private enterprise number, but
      # they are set to all zeros. The fifth byte is set to one to
      # indicate that the final four bytes are an IPv4 address.

      if (!defined($ENGINE_ID)) {
         eval {
            require Sys::Hostname;
            $ENGINE_ID = pack('H10', '8000000001') .
                         scalar(gethostbyname(Sys::Hostname::hostname()));
         };

         # Fallback in case gethostbyname() or hostname() fail
         $ENGINE_ID = pack('x11H2', '01') if ($@);    
      }

      $this->{_engine_id} = $ENGINE_ID;
   }

   $this->{_engine_boots} = 1;
   $this->{_time_epoc}    = $^T;
   $this->{_synchronized} = TRUE;
   $this->{_discovered}   = TRUE;
}

sub _auth_key
{
   my ($this, $auth_key) = @_;

   if (@_ == 2) {
      if ($auth_key =~ /^(?i:0x)?([a-fA-F0-9]+)$/) {
         $this->{_auth_key} = pack('H*', length($1) % 2 ? '0'.$1 : $1); 
         if (!defined($this->_auth_key_validate)) {
            return $this->_error;
         }
      } else {
         return $this->_error('Invalid authKey format specified');
      }
   }

   $this->{_auth_key};
}

sub _auth_password
{
   my ($this, $auth_password) = @_;

   if (@_ == 2) {
      if ($auth_password eq '') {
         return $_[0]->_error('Empty authentication password specified');
      } 
      $this->{_auth_password} = $auth_password;
   }

   $this->{_auth_password};
}

sub _auth_protocol
{
   my ($this, $proto) = @_;

   if (@_ == 2) {
    
      my $protocols = {
         'md5',                 AUTH_PROTOCOL_HMACMD5,
         'hmacmd5',             AUTH_PROTOCOL_HMACMD5,
         AUTH_PROTOCOL_HMACMD5, AUTH_PROTOCOL_HMACMD5,
         'sha1',                AUTH_PROTOCOL_HMACSHA,
         'hmacsha',             AUTH_PROTOCOL_HMACSHA,
         AUTH_PROTOCOL_HMACSHA, AUTH_PROTOCOL_HMACSHA 
      };

      if ($proto eq '') {
         return $this->_error('Empty authProtocol specified');
      }

      my @match = grep(/^\Q$proto/i, keys(%{$protocols}));

      if (@match > 1) {
         return $this->_error('Ambiguous authProtocol specified [%s]', $proto);
      } elsif (@match != 1) {
         return $this->_error('Unknown authProtocol specified [%s]', $proto);
      }

      $this->{_auth_protocol} = $protocols->{$match[0]};
   }

   $this->{_auth_protocol};
}

sub _priv_key
{
   my ($this, $priv_key) = @_;

   if (@_ == 2) {

      if ($priv_key =~ /^(?i:0x)?([a-fA-F0-9]+)$/) {

         $this->{_priv_key} = pack('H*', length($1) % 2 ? '0'.$1 : $1);

         # For backwards compatibility with previous versions of
         # this module truncate 20 byte SHA1 keys to 16 bytes. 

         if (length($this->{_priv_key}) == 20) {
            $this->{_priv_key} = substr($this->{_priv_key}, 0, 16);
         }

         if (!defined($this->_priv_key_validate)) {
            return $this->_error;
         }

      } else {

         return $this->_error('Invalid privKey format specified');

      }

   }

   $this->{_priv_key};
}

sub _priv_password
{
   my ($this, $priv_password) = @_;

   if (@_ == 2) {
      if ($priv_password eq '') {
         return $this->_error('Empty privacy password specified');
      }
      $this->{_priv_password} = $priv_password;
   }

   $this->{_priv_password};
}

sub _priv_protocol
{
   my ($this, $proto) = @_;

   if (@_ == 2) {

      my $protocols = {
         'des',                          PRIV_PROTOCOL_DES,
         'cbcdes',                       PRIV_PROTOCOL_DES,
         PRIV_PROTOCOL_DES,              PRIV_PROTOCOL_DES,
         '3desede',                      PRIV_PROTOCOL_DRAFT_3DESEDE,
         'cbc3desede',                   PRIV_PROTOCOL_DRAFT_3DESEDE,
         PRIV_PROTOCOL_DRAFT_3DESEDE,    PRIV_PROTOCOL_DRAFT_3DESEDE,
         'aes128cfb',                    PRIV_PROTOCOL_AESCFB128,
         'aescfb128',                    PRIV_PROTOCOL_AESCFB128,
         'cfbaes128',                    PRIV_PROTOCOL_AESCFB128,
         'cfb128aes128',                 PRIV_PROTOCOL_AESCFB128,
         PRIV_PROTOCOL_AESCFB128,        PRIV_PROTOCOL_AESCFB128,
         PRIV_PROTOCOL_DRAFT_AESCFB128,  PRIV_PROTOCOL_AESCFB128,
         'aes192cfb',                    PRIV_PROTOCOL_DRAFT_AESCFB192,
         'aescfb192',                    PRIV_PROTOCOL_DRAFT_AESCFB192,
         'cfbaes192',                    PRIV_PROTOCOL_DRAFT_AESCFB192,
         'cfb128aes192',                 PRIV_PROTOCOL_DRAFT_AESCFB192,
         PRIV_PROTOCOL_DRAFT_AESCFB192,  PRIV_PROTOCOL_DRAFT_AESCFB192,
         'aes256cfb',                    PRIV_PROTOCOL_DRAFT_AESCFB256,
         'aescfb256',                    PRIV_PROTOCOL_DRAFT_AESCFB256,
         'cfbaes256',                    PRIV_PROTOCOL_DRAFT_AESCFB256,
         'cfb128ae256',                  PRIV_PROTOCOL_DRAFT_AESCFB256,
         PRIV_PROTOCOL_DRAFT_AESCFB256,  PRIV_PROTOCOL_DRAFT_AESCFB256,
      };

      if ($proto eq '') {
         return $this->_error('Empty privProtocol specified');
      }

      my @match = grep(/^\Q$proto/i, keys(%{$protocols}));

      if (@match > 1) {
         if (lc($proto) eq 'aes') {
             $match[0] = 'aescfb128';
         } else {
             return $this->_error(
                 'Ambiguous privProtocol specified [%s]', $proto
             );
         }
      } elsif (@match != 1) {
         return $this->_error('Unknown privProtocol specified [%s]', $proto);
      }

      $this->{_priv_protocol} = $protocols->{$match[0]};

      # Validate the support of the AES cipher algorithm.  Attempt to 
      # load the Crypt::Rijndael module.  If this module is not found, 
      # do not provide support for the AES Cipher Algorithm.

      if (($this->{_priv_protocol} eq PRIV_PROTOCOL_AESCFB128)       ||
          ($this->{_priv_protocol} eq PRIV_PROTOCOL_DRAFT_AESCFB192) ||
          ($this->{_priv_protocol} eq PRIV_PROTOCOL_DRAFT_AESCFB256))
      {
         if (defined(my $error = load_module('Crypt::Rijndael'))) {
            return $this->_error(
               'Support unavailable for privProtocol [%s] %s', $proto, $error 
            );
         }
      }
   }

   $this->{_priv_protocol};
}

sub _engine_boots
{
   ($_[0]->{_synchronized}) ? $_[0]->{_engine_boots} : 0;
}

sub _engine_time
{
   my ($this) = @_;

   return 0 unless ($this->{_synchronized});

   $this->{_engine_time} = time() - $this->{_time_epoc};

   if ($this->{_engine_time} > 2147483647) {
      DEBUG_INFO('snmpEngineTime rollover');
      if (++$this->{_engine_boots} == 2147483647) {
         die('FATAL: Unable to handle snmpEngineBoots value');
      }
      $this->{_engine_time} -= 2147483647;
      $this->{_time_epoc} = time() - $this->{_engine_time};
      if (!$this->{_authoritative}) {
         $this->{_synchronized} = FALSE;
         return $this->{_latest_engine_time} = 0;
      } 
   }

   if ($this->{_engine_time} < 0) {
      die('FATAL: Unable to handle snmpEngineTime value');
   }

   $this->{_engine_time};
}

sub _security_params
{
   my ($this) = @_;

   # Clear any previous error messages
   $this->_error_clear;

   # We must have an usmUserName
   if ($this->{_user_name} eq '') {
      return $this->_error('Required userName not specified');
   }

   # Define the authentication parameters

   if ((defined($this->{_auth_password})) && ($this->{_discovered})) {
      if (!defined($this->{_auth_key})) {
         return $this->_error unless defined($this->_auth_key_generate);
      }
      $this->{_auth_password} = undef;
   }

   if (defined($this->{_auth_key})) {

      # Validate the key based on the protocol
      if (!defined($this->_auth_key_validate)) {
         return $this->_error('Invalid authKey specified');
      }

      # Initialize the authentication data 
      if (!defined($this->_auth_data_init)) {
         return $this->_error('Failed to initialize authentication data');
      }

      if ($this->{_discovered}) {
         $this->{_security_level} = SECURITY_LEVEL_AUTHNOPRIV;
      }

   }

   # You must have authentication to have privacy

   if (!defined($this->{_auth_key}) && !defined($this->{_auth_password})) {
      if (defined($this->{_priv_key}) || defined($this->{_priv_password})) {
         return $this->_error(
            'Unsupported securityLevel (privacy requires authentication)'
         );
      }
   }

   # Define the privacy parameters

   if ((defined($this->{_priv_password})) && ($this->{_discovered})) {
      if (!defined($this->{_priv_key})) {
         return $this->_error unless defined($this->_priv_key_generate);
      }
      $this->{_priv_password} = undef;
   }

   if (defined($this->{_priv_key})) {

      # Validate the key based on the protocol
      if (!defined($this->_priv_key_validate)) {
         return $this->_error('Invalid privKey specified');
      }

      # Initialize the privacy data 
      if (!defined($this->_priv_data_init)) {
         return $this->_error('Failed to initialize privacy data');
      }

      if ($this->{_discovered}) {
         $this->{_security_level} = SECURITY_LEVEL_AUTHPRIV;
      }

   }   

   DEBUG_INFO('securityLevel = %d', $this->{_security_level});
      
   $this->{_security_level};         
}

sub _engine_id_discovery
{
   my ($this, $engine_id) = @_;

   return TRUE if ($this->{_authoritative});

   if ((length($engine_id) >= 5) && (length($engine_id) <= 32)) {
      DEBUG_INFO('engineID = 0x%s', unpack('H*', $engine_id));
      $this->{_engine_id}  = $engine_id;
      $this->{_discovered} = TRUE;
      if (!defined($this->_security_params)) {
         $this->{_discovered} = FALSE;
         return $this->_error;
      }
   } else {
      return $this->_error(
         'Invalid incoming msgAuthoritativeEngineID length [%d octet%s]', 
         length($engine_id), length($engine_id) != 1 ? 's' : ''
      );
   }

   TRUE;
}

sub _synchronize
{
   my ($this, $msg_boots, $msg_time) = @_;

   return TRUE if ($this->{_authoritative});
   return TRUE if ($this->{_security_level} < SECURITY_LEVEL_AUTHNOPRIV);

   if (($msg_boots > $this->_engine_boots) ||
       (($msg_boots == $this->_engine_boots) && 
        ($msg_time > $this->{_latest_engine_time})))
   {

      DEBUG_INFO(
         'update: engineBoots = %d, engineTime = %d', $msg_boots, $msg_time 
      );

      $this->{_engine_boots} = $msg_boots;
      $this->{_latest_engine_time} = $this->{_engine_time} = $msg_time;
      $this->{_time_epoc} = time() - $this->{_engine_time};

      if (!$this->{_synchronized}) {
         $this->{_synchronized} = TRUE;
         if (!defined($this->_security_params)) {
            return ($this->{_synchronized} = FALSE);
         } 
      }

      TRUE; 

   } else {

      DEBUG_INFO(
         'no update: engineBoots = %d, msgBoots = %d; ' .
         'latestTime = %d, msgTime = %d',
         $this->_engine_boots, $msg_boots, 
         $this->{_latest_engine_time}, $msg_time
      );

      TRUE;

   } 
}

sub _timeliness
{
   my ($this, $msg_boots, $msg_time) = @_;

   return TRUE if ($this->{_security_level} < SECURITY_LEVEL_AUTHNOPRIV);

   # Retrieve a local copy of our snmpEngineBoots and snmpEngineTime 
   # to avoid the possibilty of using different values in each of 
   # the comparisons.  
 
   my $engine_time  = $this->_engine_time;
   my $engine_boots = $this->_engine_boots;

   if ($engine_boots == 2147483647) {
      $this->{_synchronized} = FALSE;
      return $this->_error('Not in time window');
   }

   if (!$this->{_authoritative}) {

      if ($msg_boots < $engine_boots) {
         return $this->_error('Incoming message not in time window');
      }
      if (($msg_boots == $engine_boots) && ($msg_time < ($engine_time - 150))) {
         return $this->_error('Incoming message not in time window');
      }

   } else {

      if ($msg_boots != $engine_boots) {
         return $this->_error('Incoming message not in time window');
      }
      if (($msg_time < ($engine_time - 150)) ||
          ($msg_time > ($engine_time + 150)))
      {
         return $this->_error('Incoming message not in time window');
      }

   }

   TRUE;
}

sub _authenticate_outgoing_msg
{
   my ($this, $msg, $auth_location) = @_;

   if (!$auth_location) {
      return $this->_error(
         'Authentication failure (Unable to set msgAuthenticationParameters)'
      );
   }

   # Set the msgAuthenticationParameters
   substr(${$msg->reference}, -$auth_location, 12) = $this->_auth_hmac($msg); 
}

sub _authenticate_incoming_msg
{
   my ($this, $msg, $auth_params) = @_;

   # Authenticate the message
   if ($auth_params ne $this->_auth_hmac($msg)) {
      return $this->_error('Authentication failure');
   }
   DEBUG_INFO('authentication passed');

   TRUE;
}

sub _auth_hmac
{
#  my ($this, $msg) = @_;

   return '' unless defined($_[0]->{_auth_data}) && defined($_[1]);

   substr($_[0]->{_auth_data}->reset->add(${$_[1]->reference})->digest, 0, 12);
}

sub _auth_data_init
{
   my ($this) = @_;

   if (!defined($this->{_auth_key})) {
      return $this->_error('Required authKey not defined');
   }

   return $this->{_auth_data} if defined($this->{_auth_data});

   if ($this->{_auth_protocol} eq AUTH_PROTOCOL_HMACMD5) {

      $this->{_auth_data} =
         Digest::HMAC->new($this->{_auth_key}, 'Digest::MD5');

   } elsif ($this->{_auth_protocol} eq AUTH_PROTOCOL_HMACSHA) {

      $this->{_auth_data} = 
         Digest::HMAC->new($this->{_auth_key}, 'Digest::SHA1');

   } else {

      return $this->_error(
         'Unknown authProtocol [%s]', $this->{_auth_protocol}
      );

   }
} 

{
   my $encrypt = 
   {
      PRIV_PROTOCOL_DES,              \&_priv_encrypt_des,          
      PRIV_PROTOCOL_DRAFT_3DESEDE,    \&_priv_encrypt_3desede,
      PRIV_PROTOCOL_AESCFB128,        \&_priv_encrypt_aescfbxxx,
      PRIV_PROTOCOL_DRAFT_AESCFB192,  \&_priv_encrypt_aescfbxxx,
      PRIV_PROTOCOL_DRAFT_AESCFB256,  \&_priv_encrypt_aescfbxxx
   };

   sub _encrypt_data
   {
   #  my ($this, $msg, $priv_params, $plain) = @_;

      if (!exists($encrypt->{$_[0]->{_priv_protocol}})) {
         return $_[0]->_error('Encryption error (Unknown protocol)');
      }

      if (!defined(
            $_[1]->prepare(
               OCTET_STRING, 
               $_[0]->${\$encrypt->{$_[0]->{_priv_protocol}}}($_[2], $_[3])
            )
         ))
      {
         return $_[0]->_error('Encryption error');
      }

      # Set the PDU buffer equal to the encryptedPDU
      $_[3] = $_[1]->clear;
   }
}

{
   my $decrypt =
   {
      PRIV_PROTOCOL_DES,              \&_priv_decrypt_des,
      PRIV_PROTOCOL_DRAFT_3DESEDE,    \&_priv_decrypt_3desede,
      PRIV_PROTOCOL_AESCFB128,        \&_priv_decrypt_aescfbxxx,
      PRIV_PROTOCOL_DRAFT_AESCFB192,  \&_priv_decrypt_aescfbxxx,
      PRIV_PROTOCOL_DRAFT_AESCFB256,  \&_priv_decrypt_aescfbxxx
   };

   sub _decrypt_data
   {
   #  my ($this, $msg, $priv_params, $cipher) = @_;

      # Make sure there is data to decrypt.
      if (!defined($_[3])) {
         return $_[0]->_error($_[1]->error || 'Decryption error (No data)');
      }

      if (!exists($decrypt->{$_[0]->{_priv_protocol}})) {
         return $_[0]->_error('Decryption error (Unknown protocol)');
      }

      # Clear the Message buffer
      $_[1]->clear;

      # Put the decrypted data back into the Message buffer
      if (!defined(
            $_[1]->prepend(
               $_[0]->${\$decrypt->{$_[0]->{_priv_protocol}}}($_[2], $_[3])
            )
         )) 
      {
         return $_[0]->_error($_[1]->error);
      }
      return $_[0]->_error($_[1]->error) unless ($_[1]->length);

      # See if the decrypted data starts with a SEQUENCE 
      # and has a reasonable length.

      my $msglen = $_[1]->process(SEQUENCE);
      if ((!defined($msglen)) || ($msglen > $_[1]->length)) {
         return $_[0]->_error('Decryption error');
      }
      $_[1]->index(0); # Reset the index
      
      DEBUG_INFO('privacy passed');

      TRUE;
   }
}

sub _priv_data_init
{
   my ($this) = @_;

   if (!defined($this->{_priv_key})) {
      return $this->_error('Required privKey not defined');
   }

   return TRUE if defined($this->{_priv_data}); 

   my $init =
   {
      PRIV_PROTOCOL_DES,              \&_priv_data_init_des,
      PRIV_PROTOCOL_DRAFT_3DESEDE,    \&_priv_data_init_3desede,
      PRIV_PROTOCOL_AESCFB128,        \&_priv_data_init_aescfbxxx,
      PRIV_PROTOCOL_DRAFT_AESCFB192,  \&_priv_data_init_aescfbxxx,
      PRIV_PROTOCOL_DRAFT_AESCFB256,  \&_priv_data_init_aescfbxxx
   };

   if (!exists($init->{$this->{_priv_protocol}})) {
      return $this->_error(
         'Unknown privProtocol [%s]', $this->{_priv_protocol}
      );
   }

   $this->${\$init->{$this->{_priv_protocol}}}();
}

sub _priv_data_init_des
{
   my ($this) = @_;

   if (!defined($this->{_priv_key})) {
      return $this->_error('Required privKey not defined');
   }

   # Create the DES object
   $this->{_priv_data}->{des} = 
      Crypt::DES->new(substr($this->{_priv_key}, 0, 8));

   # Extract the pre-IV
   $this->{_priv_data}->{pre_iv} = substr($this->{_priv_key}, 8, 8);

   # Initialize the salt
   $this->{_priv_data}->{salt} = int(rand(~0));
 
   TRUE;
}

sub _priv_encrypt_des
{
#  my ($this, $priv_params, $plain) = @_;

   if (!defined($_[0]->{_priv_data})) {
      return $_[0]->_error('Required privacy data not defined');
   }

   # Always pad the plain text data.  "The actual pad value is 
   # irrelevant..." according RFC 3414 Section 8.1.1.2.  However,
   # there are some agents out there that expect "standard block
   # padding" where each of the padding byte(s) are set to the size 
   # of the padding (even for data that is a multiple of block size).

   my $pad = 8 - (length($_[2]) % 8);
   $_[2] .= pack('C', $pad) x $pad;

   # Create and set the salt
   if (++$_[0]->{_priv_data}->{salt} == ~0) {
      $_[0]->{_priv_data}->{salt} = 0;
   }
   $_[1] = pack('NN', $_[0]->{_engine_boots}, $_[0]->{_priv_data}->{salt});

   # Create the initial vector (IV)
   my $iv = $_[0]->{_priv_data}->{pre_iv} ^ $_[1];

   my $cipher = '';

   # Perform Cipher Block Chaining (CBC) 
   while($_[2] =~ /(.{8})/gs) {
      $cipher .= $iv = $_[0]->{_priv_data}->{des}->encrypt($1 ^ $iv);
   }

   $cipher;
}

sub _priv_decrypt_des
{
#  my ($this, $priv_params, $cipher) = @_;

   if (!defined($_[0]->{_priv_data})) {
      return $_[0]->_error('Required privacy data not defined');
   }

   if (length($_[1]) != 8) {
      return $_[0]->_error(
        'Invalid msgPrivParameters length [%d octet%s]', 
        length($_[1]), length($_[1]) != 1 ? 's' : ''
      );
   }

   if (length($_[2]) % 8) {
      return $_[0]->_error('DES cipher length not multiple of block size');
   }

   # Create the initial vector (IV)
   my $iv = $_[0]->{_priv_data}->{pre_iv} ^ $_[1];

   my $plain = '';

   # Perform Cipher Block Chaining (CBC) 
   while ($_[2] =~ /(.{8})/gs) {
      $plain .= $iv ^ $_[0]->{_priv_data}->{des}->decrypt($1);
      $iv = $1;
   }

   $plain;
}

sub _priv_data_init_3desede
{
   my ($this) = @_;

   if (!defined($this->{_priv_key})) {
      return $this->_error('Required privKey not defined');
   }

   # Create the 3 DES objects

   $this->{_priv_data}->{des1} =
      Crypt::DES->new(substr($this->{_priv_key}, 0, 8));
   $this->{_priv_data}->{des2} =
      Crypt::DES->new(substr($this->{_priv_key}, 8, 8));
   $this->{_priv_data}->{des3} =
      Crypt::DES->new(substr($this->{_priv_key}, 16, 8));

   # Extract the pre-IV
   $this->{_priv_data}->{pre_iv} = substr($this->{_priv_key}, 24, 8);

   # Initialize the salt
   $this->{_priv_data}->{salt} = int(rand(~0));

   # Assign a hash algorithm to "bit spread" the salt

   if ($this->{_auth_protocol} eq AUTH_PROTOCOL_HMACMD5) {
      $this->{_priv_data}->{hash} = Digest::MD5->new;
   } elsif ($this->{_auth_protocol} eq AUTH_PROTOCOL_HMACSHA) {
      $this->{_priv_data}->{hash} = Digest::SHA1->new;
   }

   TRUE;
}

sub _priv_encrypt_3desede
{
#  my ($this, $priv_params, $plain) = @_;

   if (!defined($_[0]->{_priv_data})) {
      return $_[0]->_error('Required privacy data not defined');
   }

   # Pad the plain text data using "standard block padding". 
   my $pad = 8 - (length($_[2]) % 8);
   $_[2] .= pack('C', $pad) x $pad;

   # Create and set the salt
   if (++$_[0]->{_priv_data}->{salt} == ~0) {
      $_[0]->{_priv_data}->{salt} = 0;
   }
   $_[1] = pack('NN', $_[0]->{_engine_boots}, $_[0]->{_priv_data}->{salt});

   # Draft 3DES-EDE for USM Section 5.1.1.1.2 - "To achieve effective 
   # bit spreading, the complete 8-octet 'salt' value SHOULD be 
   # hashed using the usmUserAuthProtocol."

   if (exists($_[0]->{_priv_data}->{hash})) {
      $_[1] = substr($_[0]->{_priv_data}->{hash}->add($_[1])->digest, 0, 8);
   }

   # Create the initial vector (IV)
   my $iv = $_[0]->{_priv_data}->{pre_iv} ^ $_[1];

   my $cipher = '';

   # Perform Cipher Block Chaining (CBC)
   while($_[2] =~ /(.{8})/gs) {
      $cipher .= $iv =
         $_[0]->{_priv_data}->{des3}->encrypt(
            $_[0]->{_priv_data}->{des2}->decrypt(
               $_[0]->{_priv_data}->{des1}->encrypt($1 ^ $iv)
            )
         );
   }

   $cipher;
}

sub _priv_decrypt_3desede
{
#  my ($this, $priv_params, $cipher) = @_;

   if (!defined($_[0]->{_priv_data})) {
      return $_[0]->_error('Required privacy data not defined');
   }

   if (length($_[1]) != 8) {
      return $_[0]->_error(
        'Invalid msgPrivParameters length [%d octet%s]',
        length($_[1]), length($_[1]) != 1 ? 's' : ''
      );
   }

   if (length($_[2]) % 8) {
      return $_[0]->_error(
         'CBC-3DES-EDE cipher length not multiple of block size'
      );
   }

   # Create the initial vector (IV)
   my $iv = $_[0]->{_priv_data}->{pre_iv} ^ $_[1];

   my $plain = '';

   # Perform Cipher Block Chaining (CBC) 
   while ($_[2] =~ /(.{8})/gs) {
      $plain .= 
         $iv ^ $_[0]->{_priv_data}->{des1}->decrypt(
                  $_[0]->{_priv_data}->{des2}->encrypt(
                     $_[0]->{_priv_data}->{des3}->decrypt($1)
                  )
               );
      $iv = $1;
   }

   $plain;
}

sub _priv_data_init_aescfbxxx
{
   my ($this) = @_;

   if (!defined($this->{_priv_key})) {
      return $this->_error('Required privKey not defined');
   }

   # Avoid a "strict subs" error if Crypt::Rijndael is not loaded.
   no strict 'subs';

   # Create the AES (Rijndael) object with a 128, 192, or 256 bit key.

   $this->{_priv_data}->{aes} = 
      Crypt::Rijndael->new($this->{_priv_key}, Crypt::Rijndael::MODE_CFB());

   # Initialize the salt
   $this->{_priv_data}->{salt1} = int(rand(~0));
   $this->{_priv_data}->{salt2} = int(rand(~0));

   TRUE;
}

sub _priv_encrypt_aescfbxxx
{
#  my ($this, $priv_params, $plain) = @_;

   if (!defined($_[0]->{_priv_data})) {
      return $_[0]->_error('Required privacy data not defined');
   }

   # Validate the plain text length
   my $length = length($_[2]);
   if ($length <= 16) {
      return $_[0]->_error('AES plain text length not greater than block size');
   }

   # Create and set the salt
   if (++$_[0]->{_priv_data}->{salt1} == ~0) {
      $_[0]->{_priv_data}->{salt1} = 0;
      if (++$_[0]->{_priv_data}->{salt2} == ~0) {
         $_[0]->{_priv_data}->{salt2} = 0;
      }
   }
   $_[1] = pack(
      'NN', $_[0]->{_priv_data}->{salt2}, $_[0]->{_priv_data}->{salt1}
   );

   # AES in the USM Section - Section 3.1.3 "The last ciphertext 
   # block is produced by exclusive-ORing the last plaintext segment 
   # of r bits (r is less or equal to 128) with the segment of the r 
   # most significant bits of the last output block."  
   
   # This operation is identical to those performed on the previous 
   # blocks except for the fact that the block can be less than the 
   # block size.  We can just pad the last block and operate on it as 
   # usual and then ignore the padding after encrypting.

   $_[2] .= "\000" x (16 - ($length % 16));

   # Create the IV by concatenating "...the generating SNMP engine's 
   # 32-bit snmpEngineBoots, the SNMP engine's 32-bit  snmpEngineTime, 
   # and a local 64-bit integer..." 

   $_[0]->{_priv_data}->{aes}->set_iv(
      pack('NN', $_[0]->{_engine_boots}, $_[0]->{_engine_time}) . $_[1]
   );

   # Let the Crypt::Rijndael module perform 128 bit Cipher Feedback 
   # (CFB) and return the result minus the "internal" padding.

   substr($_[0]->{_priv_data}->{aes}->encrypt($_[2]), 0, $length);
}

sub _priv_decrypt_aescfbxxx
{
#  my ($this, $priv_params, $cipher) = @_;

   if (!defined($_[0]->{_priv_data})) {
      return $_[0]->_error('Required privacy data not defined');
   }

   # Validate the msgPrivParameters length.  We assume that the
   # msgAuthoritativeEngineBoots and msgAuthoritativeEngineTime
   # have been prepended to the msgPrivParameters to create the
   # required 128 bit IV.

   if (length($_[1]) != 16) {
       return $_[0]->_error(
          'Invalid AES IV length [%d octet%s]', 
          length($_[1]), (length($_[1]) != 1) ? 's' : ''
       );
   }

   # Validate the cipher length
   my $length = length($_[2]);
   if ($length <= 16) {
      return $_[0]->_error('AES cipher length not greater than block size');
   }

   # AES in the USM Section - Section 3.1.4 "The last ciphertext 
   # block (whose size r is less or equal to 128) is less or equal 
   # to 128) is exclusive-ORed with the segment of the r most 
   # significant bits of the last output block to recover the last 
   # plaintext block of r bits."

   # This operation is identical to those performed on the previous
   # blocks except for the fact that the block can be less than the
   # block size.  We can just pad the last block and operate on it as
   # usual and then ignore the padding after decrypting.

   $_[2] .= "\000" x (16 - ($length % 16));

   # Use the msgPrivParameters as the IV.
   $_[0]->{_priv_data}->{aes}->set_iv($_[1]); 

   # Let the Crypt::Rijndael module perform 128 bit Cipher Feedback
   # (CFB) and return the result minus the "internal" padding.

   substr($_[0]->{_priv_data}->{aes}->decrypt($_[2]), 0, $length);
}

sub _auth_key_generate
{
   my ($this) = @_;

   if (!defined($this->{_engine_id}) || !defined($this->{_auth_password})) {
      return $this->_error('Unable to generate authKey');
   }

   $this->{_auth_key} = $this->_password_localize($this->{_auth_password});
}

sub _auth_key_validate
{
   my ($this) = @_;

   my $key_len =
   {
      AUTH_PROTOCOL_HMACMD5,    [ 16, 'HMAC-MD5'  ],
      AUTH_PROTOCOL_HMACSHA,    [ 20, 'HMAC-SHA1' ],
   };

   if (!exists($key_len->{$this->{_auth_protocol}})) {
      return $this->_error(
         'Unknown authProtocol [%s]', $this->{_auth_protocol}
      );
   }

   if (length($this->{_auth_key}) != $key_len->{$this->{_auth_protocol}}->[0])
   {
      return $this->_error(
         'Invalid %s authKey length [%d octet%s]',
         $key_len->{$this->{_auth_protocol}}->[1], 
         length($this->{_auth_key}), length($this->{_auth_key}) != 1 ? 's' : '' 
      );
   }

   TRUE;
}

sub _priv_key_generate
{
   my ($this) = @_;

   if (!defined($this->{_engine_id}) || !defined($this->{_priv_password})) {
      return $this->_error('Unable to generate privKey');
   }

   $this->{_priv_key} = $this->_password_localize($this->{_priv_password});

   return $this->_error unless defined($this->{_priv_key});

   if ($this->{_priv_protocol} eq PRIV_PROTOCOL_DRAFT_3DESEDE) {

      # Draft 3DES-EDE for USM Section 2.1 - "To acquire the necessary 
      # number of key bits, the password-to-key algorithm may be chained
      # using its output as further input in order to generate an
      # appropriate number of key bits."

      $this->{_priv_key} .= $this->_password_localize($this->{_priv_key}); 

   } elsif (($this->{_priv_protocol} eq PRIV_PROTOCOL_DRAFT_AESCFB192) ||
            ($this->{_priv_protocol} eq PRIV_PROTOCOL_DRAFT_AESCFB256))
   {
      # Draft AES in the USM Section 3.1.2.1 - "...if the size of the 
      # localized key is not large enough to generate an encryption 
      # key... ...set Kul = Kul || Hnnn(Kul) where Hnnn is the hash 
      # function for the authentication protocol..."

      my $hnnn;

      if ($this->{_auth_protocol} eq AUTH_PROTOCOL_HMACMD5) {
         $hnnn = Digest::MD5->new;
      } elsif ($this->{_auth_protocol} eq AUTH_PROTOCOL_HMACSHA) {
         $hnnn = Digest::SHA1->new;
      } else {
         return $this->_error(
            'Unknown authProtocol [%s]', $this->{_auth_protocol}
         );
      }

      $this->{_priv_key} .= $hnnn->add($this->{_priv_key})->digest;

   }

   # Truncate the privKey to the appropriate length.

   my $key_len =
   {
      PRIV_PROTOCOL_DES,              16,  # RFC 3414 Section 8.2.1
      PRIV_PROTOCOL_DRAFT_3DESEDE,    32,  # Draft 3DES for USM Section 5.2.1
      PRIV_PROTOCOL_AESCFB128,        16,  # AES in the USM Section 3.2.1
      PRIV_PROTOCOL_DRAFT_AESCFB192,  24,  # Draft AES in the USM Section 3.2.1
      PRIV_PROTOCOL_DRAFT_AESCFB256,  32   # Draft AES in the USM Section 3.2.1
   };

   if (!exists($key_len->{$this->{_priv_protocol}})) {
      return $this->_error(
         'Unknown privProtocol [%s]', $this->{_priv_protocol}
      );
   }

   $this->{_priv_key} = 
      substr($this->{_priv_key}, 0, $key_len->{$this->{_priv_protocol}}); 
}

sub _priv_key_validate
{
   my ($this) = @_;

   my $key_len =
   {
      PRIV_PROTOCOL_DES,              [ 16, 'CBC-DES'        ],  
      PRIV_PROTOCOL_DRAFT_3DESEDE,    [ 32, 'CBC-3DES-EDE'   ], 
      PRIV_PROTOCOL_AESCFB128,        [ 16, 'CFB128-AES-128' ],
      PRIV_PROTOCOL_DRAFT_AESCFB192,  [ 24, 'CFB128-AES-192' ],
      PRIV_PROTOCOL_DRAFT_AESCFB256,  [ 32, 'CFB128-AES-256' ]
   };

   if (!exists($key_len->{$this->{_priv_protocol}})) {
      return $this->_error(
         'Unknown privProtocol [%s]', $this->{_priv_protocol}
      );
   }

   if (length($this->{_priv_key}) != $key_len->{$this->{_priv_protocol}}->[0])
   {
      return $this->_error(
         'Invalid %s privKey length [%d octet%s]',
         $key_len->{$this->{_priv_protocol}}->[1], 
         length($this->{_priv_key}), length($this->{_priv_key}) != 1 ? 's' : ''
      );
   }
  
   if ($this->{_priv_protocol} eq PRIV_PROTOCOL_DRAFT_3DESEDE) {

      # Draft 3DES-EDE for USM Section 5.1.1.1.1 "The checks for difference 
      # and weakness... ...should be performed when the key is assigned.
      # If any of the mandated tests fail, then the whole key MUST be 
      # discarded and an appropriate exception noted."

      if (substr($this->{_priv_key}, 0, 8) eq substr($this->{_priv_key}, 8, 8)) 
      {
         return $this->_error('Invalid CBC-3DES-EDE privKey [K1 equals K2]');
      }

      if (substr($this->{_priv_key}, 8, 8) eq substr($this->{_priv_key}, 16, 8))
      {
         return $this->_error('Invalid CBC-3DES-EDE privKey [K2 equals K3]');
      }

      if (substr($this->{_priv_key}, 0, 8) eq substr($this->{_priv_key}, 16, 8))
      {
         return $this->_error('Invalid CBC-3DES-EDE privKey [K1 equals K3]');
      }

   }

   TRUE;
}

sub _password_localize
{
   my ($this, $password) = @_;

   my $digests =
   {
      AUTH_PROTOCOL_HMACMD5,  'Digest::MD5',
      AUTH_PROTOCOL_HMACSHA,  'Digest::SHA1',
   };

   if (!exists($digests->{$this->{_auth_protocol}})) {
      return $this->_error(
         'Unknown authProtocol [%s]', $this->{_auth_protocol}
      );
   }

   my $digest = $digests->{$this->{_auth_protocol}}->new;

   # Create the initial digest using the password

   my $d = my $pad = $password x ((2048 / length($password)) + 1);

   for (my $count = 0; $count < 2**20; $count += 2048) {
      $digest->add(substr($d, 0, 2048, ''));
      $d .= $pad;
   }
   $d = $digest->digest;

   # Localize the key with the authoritativeEngineID

   $digest->add($d . $this->{_engine_id} . $d)->digest;
}

{
   my %modules;

   sub load_module
   {
      my ($module) = @_;

      # We attempt to load the required module under the protection of an
      # eval statement.  If there is a failure, typically it is due to a
      # missing module required by the requested module and we attempt to
      # simplify the error message by just listing that module.  We also
      # need to track failures since require() only produces an error on
      # the first attempt to load the module.

      # NOTE: Contrary to our typical convention, a return value of "undef"
      # actually means success and a defined value means error.

      return $modules{$module} if (exists($modules{$module}));

      if (!eval("require $module")) {
         if ($@ =~ /locate (\S+\.pm)/) {
            $modules{$module} = sprintf('(Required module %s not found)', $1);
         } else {
            $modules{$module} = sprintf('(%s)', $@);
         }
      } else {
         $modules{$module} = undef;
      }
   }
}

sub DEBUG_INFO
{
   return unless $Net::SNMP::Security::DEBUG;

   printf(
      sprintf('debug: [%d] %s(): ', (caller(0))[2], (caller(1))[3]) .
      ((@_ > 1) ? shift(@_) : '%s') .
      "\n",
      @_
   );

   $Net::SNMP::Security::DEBUG;
}

# ============================================================================
1; # [end Net::SNMP::Security::USM]
