%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1999-2011. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%
-module(snmpa_usm).

%% Avoid warning for local function error/1 clashing with autoimported BIF.
-compile({no_auto_import,[error/1]}).
%% Avoid warning for local function error/2 clashing with autoimported BIF.
-compile({no_auto_import,[error/2]}).
-export([
	 process_incoming_msg/4, process_incoming_msg/5, 
	 generate_outgoing_msg/5, generate_outgoing_msg/6,
	 generate_discovery_msg/4, generate_discovery_msg/5,
	 current_statsNotInTimeWindows_vb/0
	]).

-define(SNMP_USE_V3, true).
-include("snmp_types.hrl").
-include("SNMP-USER-BASED-SM-MIB.hrl").
-include("SNMP-USM-AES-MIB.hrl").
-include("SNMPv2-TC.hrl").

-define(VMODULE,"A-USM").
-include("snmp_verbosity.hrl").
-include("snmpa_internal.hrl").


%%-----------------------------------------------------------------
%% This module implements the User Based Security Model for SNMP,
%% as defined in rfc2274.
%%-----------------------------------------------------------------

%% Columns not accessible via SNMP
-define(usmUserAuthKey, 14).
-define(usmUserPrivKey, 15).

-define(i32(Int), (Int bsr 24) band 255, (Int bsr 16) band 255, (Int bsr 8) band 255, Int band 255).
-define(i64(Int), (Int bsr 56) band 255, (Int bsr 48) band 255, (Int bsr 40) band 255, (Int bsr 32) band 255, (Int bsr 24) band 255, (Int bsr 16) band 255, (Int bsr 8) band 255, Int band 255).


%%-----------------------------------------------------------------
%% Func: process_incoming_msg(Packet, Data, SecParams, SecLevel) ->
%%       {ok, {SecEngineID, SecName, ScopedPDUBytes, SecData}} |
%%       {error, Reason} | {error, Reason, ErrorInfo}
%%       Return value may be throwed.
%% Types: Reason -> term()
%% Purpose: 
%%-----------------------------------------------------------------

process_incoming_msg(Packet, Data, SecParams, SecLevel) ->
    LocalEngineID = ?DEFAULT_LOCAL_ENGINE_ID, 
    process_incoming_msg(Packet, Data, SecParams, SecLevel, LocalEngineID).

process_incoming_msg(Packet, Data, SecParams, SecLevel, LocalEngineID) ->
    TermDiscoEnabled    = is_terminating_discovery_enabled(), 
    TermTriggerUsername = terminating_trigger_username(), 
    %% 3.2.1
    ?vtrace("process_incoming_msg -> check security parms: 3.2.1",[]),
    UsmSecParams =
	case catch snmp_pdus:dec_usm_security_parameters(SecParams) of
	    {'EXIT', Reason} ->
		inc(snmpInASNParseErrs),
		error({parseError, Reason}, []);
	    Res ->
		Res
	end,
    case UsmSecParams of
	#usmSecurityParameters{msgAuthoritativeEngineID = MsgAuthEngineID,
			       msgUserName              = TermTriggerUsername} when TermDiscoEnabled =:= true ->
	    %% Step 1 discovery message
	    ?vtrace("process_incoming_msg -> [~p] discovery step 1", 
		    [TermTriggerUsername]),
	    process_discovery_msg(MsgAuthEngineID, Data, SecLevel);
	
	#usmSecurityParameters{msgAuthoritativeEngineID = MsgAuthEngineID,
			       msgUserName = MsgUserName} ->
	    ?vlog("process_incoming_msg -> USM security parms: "
		  "~n   msgAuthEngineID: ~w"
		  "~n   userName:        ~p", [MsgAuthEngineID, MsgUserName]),
	    %% 3.2.3
	    ?vtrace("process_incoming_msg -> check engine id: 3.2.3",[]),
	    case snmp_user_based_sm_mib:is_engine_id_known(MsgAuthEngineID) of
		true ->
		    ok;
		false ->
		    SecData1 = [MsgUserName],
		    error(usmStatsUnknownEngineIDs, 
			  ?usmStatsUnknownEngineIDs_instance, %% OTP-3542
			  undefined, [{sec_data, SecData1}])
	    end,
	    %% 3.2.4
	    ?vtrace("process_incoming_msg -> retrieve usm user: 3.2.4",[]),
	    UsmUser =
		case snmp_user_based_sm_mib:get_user(MsgAuthEngineID, 
						     MsgUserName) of
		    User when element(?usmUserStatus, User) =:= ?'RowStatus_active' ->
			User;
		    {_, Name,_,_,_,_,_,_,_,_,_,_,_, RowStatus,_,_} ->
			?vdebug("process_incoming_msg -> "
				"found user ~p with wrong row status: ~p", 
				[Name, RowStatus]),
			SecData2 = [MsgUserName],
			error(usmStatsUnknownUserNames, 
			      ?usmStatsUnknownUserNames_instance, %% OTP-3542
			      undefined, [{sec_data, SecData2}]);
		    _ -> % undefined or not active user
			SecData2 = [MsgUserName],
			error(usmStatsUnknownUserNames, 
			      ?usmStatsUnknownUserNames_instance, %% OTP-3542
			      undefined, [{sec_data, SecData2}])
		end,
	    SecName = element(?usmUserSecurityName, UsmUser),
	    ?vtrace("process_incoming_msg -> securityName: ~p",[SecName]),
	    %% 3.2.5 - implicit in following checks
	    %% 3.2.6 - 3.2.7
	    ?vtrace("process_incoming_msg -> "
		    "authenticate incoming: 3.2.5 - 3.2.7"
		    "~n   ~p",[UsmUser]),
	    DiscoOrPlain = authenticate_incoming(Packet, 
						 UsmSecParams, UsmUser, 
						 SecLevel, LocalEngineID), 
	    %% 3.2.8
	    ?vtrace("process_incoming_msg -> "
		    "decrypt scoped data: 3.2.8",[]),
	    ScopedPDUBytes = 
		decrypt(Data, UsmUser, UsmSecParams, SecLevel),
	    %% 3.2.9
	    %% Means that if AuthKey/PrivKey are changed; 
	    %% the old values will be used.
	    ?vtrace("process_incoming_msg -> "
		    "AuthKey/PrivKey are changed - "
		    "use old values: 3.2.9",[]),
	    CachedSecData = {MsgUserName,
			     element(?usmUserAuthProtocol, UsmUser),
			     element(?usmUserPrivProtocol, UsmUser),
			     element(?usmUserAuthKey, UsmUser),
			     element(?usmUserPrivKey, UsmUser)},
	    {ok, {MsgAuthEngineID, SecName, ScopedPDUBytes, 
		  CachedSecData, DiscoOrPlain}}
    end.
    
%% Process a step 1 discovery message
process_discovery_msg(MsgAuthEngineID, Data, SecLevel) ->
    ?vtrace("process_discovery_msg -> entry with"
	    "~n   Data:     ~p"
	    "~n   SecLevel: ~p", [Data, SecLevel]),
    case (not snmp_misc:is_priv(SecLevel)) of
	true -> % noAuthNoPriv
	    ?vtrace("process_discovery_msg -> noAuthNoPriv", []),
	    ScopedPDUBytes = Data,
	    SecData = {"", usmNoAuthProtocol, "", usmNoPrivProtocol, ""},
	    NewData = {SecData,
		       ?usmStatsUnknownEngineIDs_instance, 
		       get_counter(usmStatsUnknownEngineIDs)}, 
	    {ok, {MsgAuthEngineID, "", ScopedPDUBytes, NewData, discovery}};
	false ->
	    error(usmStatsUnknownEngineIDs, 
		  ?usmStatsUnknownEngineIDs_instance, 
		  undefined, [{sec_data, ""}])
    end.
	    

authenticate_incoming(Packet, UsmSecParams, UsmUser, SecLevel, 
		      LocalEngineID) ->
    %% 3.2.6
    ?vtrace("authenticate_incoming -> 3.2.6", []),
    AuthProtocol = element(?usmUserAuthProtocol, UsmUser),
    #usmSecurityParameters{msgAuthoritativeEngineID    = MsgAuthEngineID,
			   msgAuthoritativeEngineBoots = MsgAuthEngineBoots,
			   msgAuthoritativeEngineTime  = MsgAuthEngineTime,
			   msgAuthenticationParameters = MsgAuthParams} = 
	UsmSecParams,
    ?vtrace("authenticate_incoming -> Sec params: "
	    "~n   MsgAuthEngineID:    ~w"
	    "~n   MsgAuthEngineBoots: ~p"
	    "~n   MsgAuthEngineTime:  ~p",
	    [MsgAuthEngineID, MsgAuthEngineBoots, MsgAuthEngineTime]),
    case snmp_misc:is_auth(SecLevel) of
	true ->
	    SecName = element(?usmUserSecurityName, UsmUser),
	    case is_auth(AuthProtocol,
			 element(?usmUserAuthKey, UsmUser),
			 MsgAuthParams,
			 Packet,
			 SecName,
			 MsgAuthEngineID,
			 MsgAuthEngineBoots, 
			 MsgAuthEngineTime,
			 LocalEngineID) of
		discovery ->
		    discovery;
		true -> 
		    plain;
		false -> 
		    error(usmStatsWrongDigests,
			  ?usmStatsWrongDigests_instance, % OTP-5464
			  SecName) 
	    end;

	false ->  % noAuth
	    plain
    end.
	    
authoritative(SecName, MsgAuthEngineBoots, MsgAuthEngineTime, LocalEngineID) ->
    ?vtrace("authoritative -> entry with"
	    "~n   SecName:            ~p"
	    "~n   MsgAuthEngineBoots: ~p"
	    "~n   MsgAuthEngineTime:  ~p", 
	    [SecName, MsgAuthEngineBoots, MsgAuthEngineTime]),
    SnmpEngineBoots = get_local_engine_boots(LocalEngineID),
    ?vtrace("authoritative -> SnmpEngineBoots: ~p", [SnmpEngineBoots]),
    SnmpEngineTime = get_local_engine_time(LocalEngineID),
    ?vtrace("authoritative -> SnmpEngineTime: ~p", [SnmpEngineTime]),
    InTimeWindow =
	if
	    SnmpEngineBoots =:= 2147483647 -> false;
	    MsgAuthEngineBoots =/= SnmpEngineBoots -> false;
	    MsgAuthEngineTime + 150 < SnmpEngineTime -> false;
	    MsgAuthEngineTime - 150 > SnmpEngineTime -> false;
	    true -> true
	end,
    case InTimeWindow of
	true -> 
	    true;
	false -> 
	    %% OTP-4090 (OTP-3542)
	    ?vinfo("NOT in time window: "
		   "~n   SecName:            ~p"
		   "~n   SnmpEngineBoots:    ~p"
		   "~n   MsgAuthEngineBoots: ~p"
		   "~n   SnmpEngineTime:     ~p"
		   "~n   MsgAuthEngineTime:  ~p",
		   [SecName,
		    SnmpEngineBoots, MsgAuthEngineBoots,
		    SnmpEngineTime, MsgAuthEngineTime]),
	    error(usmStatsNotInTimeWindows,
		  ?usmStatsNotInTimeWindows_instance,
		  SecName,
		  [{securityLevel, 1}]) % authNoPriv
    end.

non_authoritative(SecName, 
		  MsgAuthEngineID, MsgAuthEngineBoots, MsgAuthEngineTime) ->
    ?vtrace("non_authoritative -> entry with"
	    "~n   SecName:            ~p"
	    "~n   MsgAuthEngineID:    ~p"
	    "~n   MsgAuthEngineBoots: ~p"
	    "~n   MsgAuthEngineTime:  ~p", 
	    [SecName, 
	     MsgAuthEngineID, MsgAuthEngineBoots, MsgAuthEngineTime]),
    SnmpEngineBoots = get_engine_boots(MsgAuthEngineID),
    SnmpEngineTime  = get_engine_time(MsgAuthEngineID),
    LatestRecvTime  = get_engine_latest_time(MsgAuthEngineID),
    ?vtrace("non_authoritative -> "
	    "~n   SnmpEngineBoots: ~p"
	    "~n   SnmpEngineTime:  ~p"
	    "~n   LatestRecvTime:  ~p", 
	    [SnmpEngineBoots, SnmpEngineTime, LatestRecvTime]),
    UpdateLCD =
	if
	    MsgAuthEngineBoots > SnmpEngineBoots -> true;
	    ((MsgAuthEngineBoots =:= SnmpEngineBoots) andalso 
	     (MsgAuthEngineTime > LatestRecvTime)) -> true;
	    true -> false
	end,
    case UpdateLCD of
	true -> %% 3.2.7b1
	    ?vtrace("non_authoritative -> "
		    "update msgAuthoritativeEngineID: 3.2.7b1",
		    []),
	    set_engine_boots(MsgAuthEngineID, MsgAuthEngineBoots),
	    set_engine_time(MsgAuthEngineID, MsgAuthEngineTime),
	    set_engine_latest_time(MsgAuthEngineID, MsgAuthEngineTime);
	false ->
	    ok
    end,
    %% 3.2.7.b2
    ?vtrace("non_authoritative -> "
	    "check if message is outside time window: 3.2.7b2", []),
    InTimeWindow =
	if
	    SnmpEngineBoots =:= 2147483647 ->
		false;
	    MsgAuthEngineBoots < SnmpEngineBoots ->
		false;
	    ((MsgAuthEngineBoots =:= SnmpEngineBoots) andalso 
	     (MsgAuthEngineTime < (SnmpEngineTime - 150))) ->
		false;
	    true -> true
	end,
    case InTimeWindow of
	false ->
	    ?vinfo("NOT in time window: "
		   "~n   SecName:            ~p"
		   "~n   SnmpEngineBoots:    ~p"
		   "~n   MsgAuthEngineBoots: ~p"
		   "~n   SnmpEngineTime:     ~p"
		   "~n   MsgAuthEngineTime:  ~p",
		   [SecName,
		    SnmpEngineBoots, MsgAuthEngineBoots,
		    SnmpEngineTime, MsgAuthEngineTime]),
	    error(notInTimeWindow, []);
	true ->
	    %% If the previous values where all zero's this is the 
	    %% second stage discovery message
	    if
		((SnmpEngineBoots =:= 0) andalso 
		 (SnmpEngineTime  =:= 0) andalso 
		 (LatestRecvTime  =:= 0)) ->
		    ?vtrace("non_authoritative -> "
			    "[maybe] originating discovery stage 2", []),
		    discovery;
		true ->
		    true
	    end
    end.

is_auth(?usmNoAuthProtocol, _, _, _, SecName, _, _, _, _) -> % 3.2.5
    error(usmStatsUnsupportedSecLevels,
	  ?usmStatsUnsupportedSecLevels_instance, SecName); % OTP-5464
is_auth(AuthProtocol, AuthKey, AuthParams, Packet, SecName,
	MsgAuthEngineID, MsgAuthEngineBoots, MsgAuthEngineTime, 
	LocalEngineID) ->
    TermDiscoEnabled = is_terminating_discovery_enabled(), 
    TermDiscoStage2  = terminating_discovery_stage2(), 
    IsAuth = auth_in(AuthProtocol, AuthKey, AuthParams, Packet),
    ?vtrace("is_auth -> IsAuth: ~p", [IsAuth]),
    case IsAuth of
	true ->
	    %% 3.2.7
	    ?vtrace("is_auth -> "
		    "retrieve EngineBoots and EngineTime: 3.2.7",[]),
	    SnmpEngineID = LocalEngineID,
	    ?vtrace("is_auth -> SnmpEngineID: ~p", [SnmpEngineID]),
	    case MsgAuthEngineID of
		SnmpEngineID when ((MsgAuthEngineBoots =:= 0) andalso 
				   (MsgAuthEngineTime =:= 0) andalso 
				   (TermDiscoEnabled =:= true) andalso 
				   (TermDiscoStage2 =:= discovery)) -> %% 3.2.7a
		    ?vtrace("is_auth -> terminating discovery stage 2 - discovery",[]),
		    discovery;
		SnmpEngineID when ((MsgAuthEngineBoots =:= 0) andalso 
				   (MsgAuthEngineTime =:= 0) andalso 
				   (TermDiscoEnabled =:= true) andalso 
				   (TermDiscoStage2 =:= plain)) -> %% 3.2.7a
		    ?vtrace("is_auth -> terminating discovery stage 2 - plain",[]),
		    %% This will *always* result in the manager *not* 
		    %% beeing in timewindow
		    authoritative(SecName, 
				  MsgAuthEngineBoots, MsgAuthEngineTime, 
				  LocalEngineID);

		SnmpEngineID -> %% 3.2.7a
		    ?vtrace("is_auth -> we are authoritative: 3.2.7a", []),
		    authoritative(SecName, 
				  MsgAuthEngineBoots, MsgAuthEngineTime, 
				  LocalEngineID);

		_ -> %% 3.2.7b - we're non-authoritative
		    ?vtrace("is_auth -> we are non-authoritative: 3.2.7b",[]),
		    non_authoritative(SecName, 
				      MsgAuthEngineID, 
				      MsgAuthEngineBoots, MsgAuthEngineTime)
	    end;

	false -> 
	    false
    end.
				
			    
decrypt(Data, UsmUser, UsmSecParams, SecLevel) ->
    case snmp_misc:is_priv(SecLevel) of
	true ->
	    do_decrypt(Data, UsmUser, UsmSecParams);
	false ->
	    Data
    end.

do_decrypt(Data, UsmUser, UsmSecParams) ->
	    EncryptedPDU = snmp_pdus:dec_scoped_pdu_data(Data),
	    SecName      = element(?usmUserSecurityName, UsmUser),
	    PrivP        = element(?usmUserPrivProtocol, UsmUser),
	    PrivKey      = element(?usmUserPrivKey,      UsmUser), 
    ?vtrace("do_decrypt -> try decrypt with: "
	    "~n   SecName: ~p"
	    "~n   PrivP:   ~p", [SecName, PrivP]),
    try_decrypt(PrivP, PrivKey, UsmSecParams, EncryptedPDU, SecName).

try_decrypt(?usmNoPrivProtocol, _, _, _, SecName) -> % 3.2.5
    error(usmStatsUnsupportedSecLevels, 
	  ?usmStatsUnsupportedSecLevels_instance, SecName); % OTP-5464
try_decrypt(?usmDESPrivProtocol, 
	    PrivKey, UsmSecParams, EncryptedPDU, SecName) ->
    case (catch des_decrypt(PrivKey, UsmSecParams, EncryptedPDU)) of
	{ok, DecryptedData} ->
	    DecryptedData;
	Error ->
	    ?vlog("try_decrypt -> failed DES decrypt"
		  "~n   Error: ~p", [Error]),
	    error(usmStatsDecryptionErrors, 
		  ?usmStatsDecryptionErrors_instance, % OTP-5464
		  SecName)
    end;
try_decrypt(?usmAesCfb128Protocol, 
	    PrivKey, UsmSecParams,  EncryptedPDU, SecName) ->
    case (catch aes_decrypt(PrivKey, UsmSecParams, EncryptedPDU)) of
	{ok, DecryptedData} ->
	    DecryptedData;
	Error ->
	    ?vlog("try_decrypt -> failed AES decrypt"
		  "~n   Error: ~p", [Error]),
	    error(usmStatsDecryptionErrors, 
		  ?usmStatsDecryptionErrors_instance, % OTP-5464
		  SecName)
    end.


generate_outgoing_msg(Message, SecEngineID, SecName, SecData, SecLevel) ->
    LocalEngineID = ?DEFAULT_LOCAL_ENGINE_ID, 
    generate_outgoing_msg(Message, SecEngineID, SecName, SecData, SecLevel, 
			  LocalEngineID).

generate_outgoing_msg(Message, SecEngineID, SecName, SecData, SecLevel, 
		      LocalEngineID) ->
    %% 3.1.1
    ?vtrace("generate_outgoing_msg -> [3.1.1] entry with"
	    "~n   SecEngineID:   ~p"
	    "~n   SecName:       ~p"
	    "~n   SecLevel:      ~w" 
	    "~n   LocalEngineID: ~p", 
	    [SecEngineID, SecName, SecLevel, LocalEngineID]),
    {UserName, AuthProtocol, PrivProtocol, AuthKey, PrivKey} =
	case SecData of
	    [] -> % 3.1.1b
		%% Not a response - read from LCD
		case snmp_user_based_sm_mib:get_user_from_security_name(
		       SecEngineID, SecName) of
		    User when element(?usmUserStatus, User) =:=
			      ?'RowStatus_active' ->
			{element(?usmUserName, User),
			 element(?usmUserAuthProtocol, User),
			 element(?usmUserPrivProtocol, User),
			 element(?usmUserAuthKey, User),
			 element(?usmUserPrivKey, User)};
		    {_, Name,_,_,_,_,_,_,_,_,_,_,_, RowStatus,_,_} ->
			?vdebug("generate_outgoing_msg -> "
				"found not active user ~p: ~p", 
				[Name, RowStatus]),
			error(unknownSecurityName);
		    _ ->
			error(unknownSecurityName)
		end;
	    [MsgUserName] ->
		%% This means the user at the engine is unknown
		{MsgUserName, ?usmNoAuthProtocol, ?usmNoPrivProtocol, "", ""};
	    _ -> % 3.1.1a
		SecData
	end,
    %% 3.1.4
    ?vtrace("generate_outgoing_msg -> [3.1.4]"
	    "~n   UserName:     ~p"
	    "~n   AuthProtocol: ~p"
	    "~n   PrivProtocol: ~p", 
	    [UserName, AuthProtocol, PrivProtocol]),
    ScopedPduBytes = Message#message.data,
    {ScopedPduData, MsgPrivParams} =
	encrypt(ScopedPduBytes, PrivProtocol, PrivKey, SecLevel),
    SnmpEngineID = LocalEngineID, 
    ?vtrace("generate_outgoing_msg -> SnmpEngineID: ~p [3.1.6]",
	    [SnmpEngineID]),
    %% 3.1.6
    {MsgAuthEngineBoots, MsgAuthEngineTime} =
	case snmp_misc:is_auth(SecLevel) of
	    false when SecData =:= [] -> % not a response
		{0, 0}; 
	    false when UserName =:= "" -> % reply (report) to discovery step 1
		{0, 0}; 
	    true when SecEngineID =/= SnmpEngineID ->
		{get_engine_boots(SecEngineID),
		 get_engine_time(SecEngineID)};
	    _ ->
		{get_local_engine_boots(SnmpEngineID),
		 get_local_engine_time(SnmpEngineID)}
	end,
    %% 3.1.5 - 3.1.7
    ?vtrace("generate_outgoing_msg -> [3.1.5 - 3.1.7]",[]),
    UsmSecParams =
	#usmSecurityParameters{msgAuthoritativeEngineID    = SecEngineID,
			       msgAuthoritativeEngineBoots = MsgAuthEngineBoots,
			       msgAuthoritativeEngineTime  = MsgAuthEngineTime,
			       msgUserName                 = UserName,
			       msgPrivacyParameters        = MsgPrivParams},
    Message2 = Message#message{data = ScopedPduData},
    %% 3.1.8
    ?vtrace("generate_outgoing_msg -> [3.1.8]",[]),
    authenticate_outgoing(Message2, UsmSecParams,
			  AuthKey, AuthProtocol, SecLevel).


generate_discovery_msg(Message, SecEngineID, SecName, SecLevel) ->
    generate_discovery_msg(Message, SecEngineID, SecName, SecLevel, "").

generate_discovery_msg(Message, 
		       SecEngineID, SecName, SecLevel, 
		       InitialUserName) ->
   ?vtrace("generate_discovery_msg -> entry with"
	    "~n   SecEngineID:     ~p"
	    "~n   SecName:         ~p"
	    "~n   SecLevel:        ~p"
	    "~n   InitialUserName: ~p", 
	    [SecEngineID, SecName, SecLevel, InitialUserName]),
    {UserName, AuthProtocol, AuthKey, PrivProtocol, PrivKey} = 
	case SecEngineID of
	    "" ->
		%% Discovery step 1
		%% Nothing except the user name will be used in this
		%% tuple in this step, but since we need some values,
		%% we fill in proper ones just in case
		%% {"initial", usmNoAuthProtocol, "", usmNoPrivProtocol, ""}; 
		%% {"", usmNoAuthProtocol, "", usmNoPrivProtocol, ""}; 
		{InitialUserName, 
		 usmNoAuthProtocol, "", usmNoPrivProtocol, ""}; 

	    _ ->
		%% Discovery step 2
		case snmp_user_based_sm_mib:get_user_from_security_name(
		       SecEngineID, SecName) of
		    User when element(?usmUserStatus, User) =:=
			      ?'RowStatus_active' ->
			{element(?usmUserName, User),
			 element(?usmUserAuthProtocol, User),
			 element(?usmUserAuthKey, User),
			 element(?usmUserPrivProtocol, User),
			 element(?usmUserPrivKey, User)};
		    {_, Name,_,_,_,_,_,_,_,_,_,_,_, RowStatus,_,_} ->
			?vdebug("generate_discovery_msg -> "
				"found user ~p with wrong row status: ~p", 
				[Name, RowStatus]),
			error(unknownSecurityName);
		    _ ->
			error(unknownSecurityName)
		end
	end,
    ScopedPduBytes = Message#message.data,
    {ScopedPduData, MsgPrivParams} =
	encrypt(ScopedPduBytes, PrivProtocol, PrivKey, SecLevel),
    UsmSecParams =
	#usmSecurityParameters{msgAuthoritativeEngineID    = SecEngineID, 
			       msgAuthoritativeEngineBoots = 0, % Boots, 
			       msgAuthoritativeEngineTime  = 0, % Time, 
			       msgUserName                 = UserName,
			       msgPrivacyParameters        = MsgPrivParams},
    Message2 = Message#message{data = ScopedPduData},
    authenticate_outgoing(Message2, UsmSecParams,
			  AuthKey, AuthProtocol, SecLevel).

    
%% Ret: {ScopedPDU, MsgPrivParams} - both are already encoded as OCTET STRINGs
encrypt(Data, PrivProtocol, PrivKey, SecLevel) ->
    case snmp_misc:is_priv(SecLevel) of
	false -> % 3.1.4b
	    ?vtrace("encrypt -> 3.1.4b",[]),
	    {Data, []};
	true -> % 3.1.4a
	    ?vtrace("encrypt -> 3.1.4a",[]),
	    case (catch try_encrypt(PrivProtocol, PrivKey, Data)) of
		{ok, ScopedPduData, MsgPrivParams} ->
		    ?vtrace("encrypt -> encrypted - now encode tag",[]),
		    {snmp_pdus:enc_oct_str_tag(ScopedPduData), MsgPrivParams};
                {error, Reason} ->
		    ?vtrace("encrypt -> error: "
			    "~n   Reason: ~p", [Reason]),
                    error(Reason);
 		Error ->
		    ?vtrace("encrypt -> other: "
			    "~n   Error: ~p", [Error]),
		    error(encryptionError)
	    end
    end.

try_encrypt(?usmNoPrivProtocol, _PrivKey, _Data) -> % 3.1.2
    error(unsupportedSecurityLevel);
try_encrypt(?usmDESPrivProtocol, PrivKey, Data) ->
    des_encrypt(PrivKey, Data);
try_encrypt(?usmAesCfb128Protocol, PrivKey, Data) ->
    aes_encrypt(PrivKey, Data).


authenticate_outgoing(Message, UsmSecParams, 
		      AuthKey, AuthProtocol, SecLevel) ->
    Message2 = 
	case snmp_misc:is_auth(SecLevel) of
	    true ->
		auth_out(AuthProtocol, AuthKey, Message, UsmSecParams);
	    false ->
		set_msg_auth_params(Message, UsmSecParams)
	end,
    ?vtrace("authenticate_outgoing -> encode message only",[]),
    snmp_pdus:enc_message_only(Message2).


%%-----------------------------------------------------------------
%% Auth and priv algorithms
%%-----------------------------------------------------------------
auth_in(AuthProtocol, AuthKey, AuthParams, Packet) ->
    snmp_usm:auth_in(AuthProtocol, AuthKey, AuthParams, Packet).

auth_out(AuthProtocol, AuthKey, Message, UsmSecParams) ->
    snmp_usm:auth_out(AuthProtocol, AuthKey, Message, UsmSecParams).

set_msg_auth_params(Message, UsmSecParams) ->
    snmp_usm:set_msg_auth_params(Message, UsmSecParams, []).

des_encrypt(PrivKey, Data) ->
    snmp_usm:des_encrypt(PrivKey, Data, fun get_des_salt/0).

des_decrypt(PrivKey, UsmSecParams, EncData) ->
    #usmSecurityParameters{msgPrivacyParameters = PrivParms} = UsmSecParams,
    snmp_usm:des_decrypt(PrivKey, PrivParms, EncData).

get_des_salt() ->
    SaltInt = 
	case catch ets:update_counter(snmp_agent_table, usm_des_salt, 1) of
	    N when N =< 4294967295 ->
		N;
	    N when is_integer(N) -> % wrap
		ets:insert(snmp_agent_table, {usm_des_salt, 0}),
		0;
	    _ -> % it doesn't exist, initialize
		{A1,A2,A3} = erlang:now(),
		random:seed(A1,A2,A3),
		R = random:uniform(4294967295),
		ets:insert(snmp_agent_table, {usm_des_salt, R}),
		R
	end,
    EngineBoots = snmp_framework_mib:get_engine_boots(),
    [?i32(EngineBoots), ?i32(SaltInt)].

aes_encrypt(PrivKey, Data) ->
    snmp_usm:aes_encrypt(PrivKey, Data, fun get_aes_salt/0).

aes_decrypt(PrivKey, UsmSecParams, EncData) ->
    #usmSecurityParameters{msgPrivacyParameters        = PrivParams,
			   msgAuthoritativeEngineTime  = EngineTime,
			   msgAuthoritativeEngineBoots = EngineBoots} =
	UsmSecParams,
    snmp_usm:aes_decrypt(PrivKey, PrivParams, EncData, 
			 EngineBoots, EngineTime).

get_aes_salt() ->
    SaltInt = 
	case catch ets:update_counter(snmp_agent_table, usm_aes_salt, 1) of
	    N when N =< 36893488147419103231  ->
		N;
	    N when is_integer(N) -> % wrap
		ets:insert(snmp_agent_table, {usm_aes_salt, 0}),
		0;
	    _ -> % it doesn't exist, initialize
		{A1,A2,A3} = erlang:now(),
		random:seed(A1,A2,A3),
		R = random:uniform(36893488147419103231),
		ets:insert(snmp_agent_table, {usm_aes_salt, R}),
		R
	end,
    [?i64(SaltInt)].



%%-----------------------------------------------------------------
%% Discovery wrapper functions
%%-----------------------------------------------------------------

is_terminating_discovery_enabled() ->
    snmpa_agent:is_terminating_discovery_enabled().

terminating_discovery_stage2() ->
    snmpa_agent:terminating_discovery_stage2().

terminating_trigger_username() ->
    snmpa_agent:terminating_trigger_username().

current_statsNotInTimeWindows_vb() ->
    #varbind{oid          = ?usmStatsNotInTimeWindows_instance,
	     variabletype = 'Counter32',
	     value        = get_counter(usmStatsNotInTimeWindows)}.



%%-----------------------------------------------------------------
%% Future profing...
%%-----------------------------------------------------------------

get_local_engine_boots(_LocalEngineID) ->
    snmp_framework_mib:get_engine_boots().

get_local_engine_time(_LocalEngineID) ->
    snmp_framework_mib:get_engine_time().



%%-----------------------------------------------------------------
%% We cache the local values of all non-auth engines we know.
%% Keep the values in the snmp_agent_table.
%% See section 2.3 of the RFC.
%%-----------------------------------------------------------------
get_engine_boots(SnmpEngineID) ->
    case ets:lookup(snmp_agent_table, {usm_eboots, SnmpEngineID}) of
	[{_Key, Boots}] -> Boots;
	_ -> 0
    end.

get_engine_time(SnmpEngineID) ->
    case ets:lookup(snmp_agent_table, {usm_etime, SnmpEngineID}) of
	[{_Key, Diff}] -> snmp_misc:now(sec) - Diff;
	_ -> 0
    end.
	    
get_engine_latest_time(SnmpEngineID) ->
    case ets:lookup(snmp_agent_table, {usm_eltime, SnmpEngineID}) of
	[{_Key, Time}] -> Time;
	_ -> 0
    end.
	    

set_engine_boots(SnmpEngineID, EngineBoots) ->
    ets:insert(snmp_agent_table, {{usm_eboots, SnmpEngineID}, EngineBoots}).

set_engine_time(SnmpEngineID, EngineTime) ->
    Diff = snmp_misc:now(sec) - EngineTime,
    ets:insert(snmp_agent_table, {{usm_etime, SnmpEngineID}, Diff}).

set_engine_latest_time(SnmpEngineID, EngineTime) ->
    ets:insert(snmp_agent_table, {{usm_eltime, SnmpEngineID}, EngineTime}).


%%-----------------------------------------------------------------
%% Utility functions
%%-----------------------------------------------------------------
-spec error(term()) -> no_return().
error(Reason) ->
    throw({error, Reason}).

-spec error(term(), term()) -> no_return().
error(Reason, ErrorInfo) ->
    throw({error, Reason, ErrorInfo}).

-spec error(term(), term(), term()) -> no_return().
error(Variable, Oid, SecName) ->
    error(Variable, Oid, SecName, []).

-spec error(term(), term(), term(), [term()]) -> no_return().
error(Variable, Oid, SecName, Opts) ->
    Val = inc(Variable),
    ErrorInfo = {#varbind{oid = Oid,
			  variabletype = 'Counter32',
			  value = Val},
		 SecName,
		 Opts},
    throw({error, Variable, ErrorInfo}).

inc(Name) -> ets:update_counter(snmp_agent_table, Name, 1).

get_counter(Name) ->
    case (catch ets:lookup(snmp_agent_table, Name)) of
	[{_, Val}] ->
	    Val;
	_ ->
	    0
    end.
