%% 
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1999-2024. All Rights Reserved.
%% 
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% 
%% %CopyrightEnd%
%% 
-module(snmp_user_based_sm_mib).
-moduledoc """
Instrumentation Functions for SNMP-USER-BASED-SM-MIB

The module `snmp_user_based_sm_mib` implements the instrumentation functions for
the SNMP-USER-BASED-SM-MIB, and functions for configuring the database.

Note that authentication has been extended according to RFC 7860
(SNMP-USM-HMAC-SHA2-MIB).

The configuration files are described in the SNMP User's Manual.
""".

%% Avoid warning for local function error/1 clashing with autoimported BIF.
-compile({no_auto_import,[error/1]}).
-export([configure/1, reconfigure/1,
	 usmUserSpinLock/1, usmUserSpinLock/2,
	 usmUserTable/1, usmUserTable/3,
	 table_next/2,
	 is_engine_id_known/1, get_user/2, get_user_from_security_name/2,
	 mk_key_change/3, mk_key_change/5, extract_new_key/3, mk_random/1]).
-export([usmStatsUnsupportedSecLevels/1,
	 usmStatsNotInTimeWindows/1,
	 usmStatsUnknownUserNames/1,
	 usmStatsUnknownEngineIDs/1,
	 usmStatsWrongDigests/1,
	 usmStatsDecryptionErrors/1]).
-export([add_user/1, add_user/13, delete_user/1]).

-export_type([
              name/0,
              clone_from/0,
              auth_protocol/0,
              key_change/0,
              priv_protocol/0,
              public/0,
              auth_key/0,
              priv_key/0,
              usm_entry/0
             ]).

%% Internal
-export([check_usm/1]).

-include("SNMP-USER-BASED-SM-MIB.hrl").
-include("SNMP-USM-HMAC-SHA2-MIB.hrl").
-include("SNMP-USM-AES-MIB.hrl").
-include("SNMPv2-TC.hrl").
-include("snmpa_internal.hrl").
-include("snmp_types.hrl").
-include("snmp_usm.hrl").

-define(VMODULE,"USM_MIB").
-include("snmp_verbosity.hrl").

-ifndef(default_verbosity).
-define(default_verbosity,silence).
-endif.


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


%% *** name ***
-doc """
> #### Note {: .info }
>
> "A human readable string representing the name of the user. This is the
> (User-based Security) Model dependent security ID."

`SnmpAdminString (SIZE(1..32))`
""".
-type name()             :: snmp_framework_mib:admin_string().

%% *** clone_from ***
-doc """
> #### Note {: .info }
>
> "A pointer to another conceptual row in this usmUserTable. The user in this
> other conceptual row is called the clone-from user."

`RowPointer`
""".
-type clone_from()       :: zeroDotZero | snmp:row_pointer().

%% *** auth_protocol ***
-doc """
> #### Note {: .info }
>
> "An indication of whether messages sent on behalf of this user to/from the
> SNMP engine identified by usmUserEngineID, can be authenticated, and if so,
> the type of authentication protocol which is used."

> #### Note {: .info }
>
> Some of the entries of this type are actually defined by the
> SNMP-USM-HMAC-SHA2-MIB mib.

`AutonomousType`
""".
-type auth_protocol()    :: usmNoAuthProtocol             |
                            usmHMACMD5AuthProtocol        |
                            usmHMACSHAAuthProtocol        |
                            usmHMAC128SHA224AuthProtocol  |
                            usmHMAC192SH256AuthProtocol   |
                            usmHMAC256SHA384AuthProtocol  |
                            usmHMAC384SHA512AuthProtocol.

%% *** key_change ***
-doc """
> #### Note {: .info }
>
> "Every definition of an object with this syntax must identify a protocol P, a
> secret key K, and a hash algorithm H that produces output of L octets."

`OCTET STRING`
""".
-type key_change()       :: snmp:octet_string().

%% *** priv_protocol ***
-doc """
> #### Note {: .info }
>
> "An indication of whether messages sent on behalf of this user to/from the
> SNMP engine identified by usmUserEngineID, can be protected from disclosure,
> and if so, the type of privacy protocol which is used."

> #### Note {: .info }
>
> Some of the entries of this tyype are actually defined by the SNMP-USM-AES-MIB
> mib.

`AutonomousType`
""".
-type priv_protocol()    :: usmNoPrivProtocol    |
                            usmDESPrivProtocol   |
                            usmAesCfb128Protocol.

%% *** public ***
-doc "`OCTET STRING (SIZE(0..32))`".
-type public()           :: string().

%% *** auth_key ***
-doc """
The size/length of the list depends on auth protocol:

```text
               Size any for usmNoAuthProtocol
               Size 16  for usmHMACMD5AuthProtocol
               Size 20  for usmHMACSHAAuthProtocol
               Size 28  for usmHMAC128SHA224AuthProtocol
               Size 32  for usmHMAC192SHA256AuthProtocol
               Size 48  for usmHMAC256SHA384AuthProtocol
	       Size 64  for usmHMAC384SHA512AuthProtocol
```
""".
-type auth_key()         :: snmp:octet_string().

%% *** priv_key ***
-doc """
The size/length of the list depends on priv protocol:

```text
	       Size any for usmNoPrivProtocol
               Size 16  for usmDESPrivProtocol
               Size 16  for usmAesCfb128Protocol
```
""".
-type priv_key()         :: snmp:octet_string().

-type usm_entry() :: {
                      EngineID    :: snmp_framework_mib:engine_id(),
                      UserName    :: name(),
                      SecName     :: snmp_framework_mib:admin_string(),
                      Clone       :: clone_from(),
                      AuthP       :: auth_protocol(),
                      AuthKeyC    :: key_change(),
                      OwnAuthKeyC :: key_change(),
                      PrivP       :: priv_protocol(),
                      PrivKeyC    :: key_change(),
                      OwnPrivKeyC :: key_change(),
                      Public      :: public(),
                      AuthKey     :: auth_key(),
                      PrivKey     :: priv_key()
                     }.


%%%-----------------------------------------------------------------
%%% Utility functions
%%%-----------------------------------------------------------------

%%%-----------------------------------------------------------------
%%% Implements the instrumentation functions and additional 
%%% functions for the SNMP-USER-BASED-SM-MIB.
%%%-----------------------------------------------------------------

%%-----------------------------------------------------------------
%% Func: configure/1
%% Args: Dir is the directory where the configuration files are found.
%% Purpose: If the tables doesn't exist, this function reads
%%          the config-files for the USM tables, and
%%          inserts the data.  This means that the data in the tables
%%          survive a reboot.  However, the StorageType column is
%%          checked for each row.  If volatile, the row is deleted.
%% Returns: ok
%% Fails: exit(configuration_error)
%%-----------------------------------------------------------------

-doc """
This function is called from the supervisor at system start-up.

Inserts all data in the configuration files into the database and destroys all
old rows with StorageType `volatile`. The rows created from the configuration
file will have StorageType `nonVolatile`.

All `snmp` counters are set to zero.

If an error is found in the configuration file, it is reported using the
function `config_err/2` of the error report module, and the function fails with
the reason `configuration_error`.

`ConfDir` is a string which points to the directory where the configuration
files are found.

The configuration file read is: `usm.conf`.
""".
-spec configure(ConfDir) -> snmp:void() when
      ConfDir :: string().

configure(ConfDir) ->
    set_sname(),
    case db(usmUserTable) of
        {_, mnesia} ->
            ?vdebug("usm user table in mnesia: init vars & cleanup",[]),
            init_vars(),
            gc_tabs();
	TabDb ->
	    case snmpa_local_db:table_exists(TabDb) of
		true ->
		    ?vdebug("usm user table exists: init vars & cleanup",[]),
		    init_vars(),
		    gc_tabs();
		false ->
		    ?vdebug("usm user table does not exist: reconfigure",[]),
		    reconfigure(ConfDir)
	    end
    end.

%%-----------------------------------------------------------------
%% Func: reconfigure/1
%% Args: Dir is the directory where the configuration files are found.
%% Purpose: Reads the config-files for the USM tables, and
%%          inserts the data.  Makes sure that all old data in
%%          the tables are deleted, and the new data inserted.
%%          This function makes sure that all (and only) 
%%          config-file-data are in the tables. 
%% Returns: ok
%% Fails: exit(configuration_error) |
%%        exit({unsupported_crypto, Function})
%%-----------------------------------------------------------------

-doc """
Inserts all data in the configuration files into the database and destroys all
old data, including the rows with StorageType `nonVolatile`. The rows created
from the configuration file will have StorageType `nonVolatile`.

Thus, the data in the SNMP-USER-BASED-SM-MIB, after this function has been
called, is the data from the configuration files.

All `snmp` counters are set to zero.

If an error is found in the configuration file, it is reported using the
function `config_err/2` of the error report module, and the function fails with
the reason `configuration_error`.

`ConfDir` is a string which points to the directory where the configuration
files are found.

The configuration file read is: `usm.conf`.
""".
-spec reconfigure(ConfDir) -> snmp:void() when
      ConfDir :: string().

reconfigure(ConfDir) ->
    set_sname(),
    case (catch do_reconfigure(ConfDir)) of
	ok ->
	    ok;
	{error, Reason} ->
	    ?vinfo("reconfigure error: ~p", [Reason]),
	    config_err("reconfigure failed: ~p", [Reason]),
	    exit(configuration_error);
	Error ->
	    ?vinfo("reconfigure failed: ~p", [Error]),
	    config_err("reconfigure failed: ~p", [Error]),
	    exit(configuration_error)
    end.

do_reconfigure(Dir) ->
    ?vdebug("read usm configuration files",[]),
    Users = read_usm_config_files(Dir),
    ?vdebug("check users",[]),
    check_users(Users),
    ?vdebug("initiate tables",[]),
    init_tabs(Users),
    ?vdebug("initiate vars",[]),
    init_vars(),
    ok.


read_usm_config_files(Dir) ->
    ?vdebug("read usm config file",[]),
    Gen    = fun (D, Reason) -> generate_usm(D, Reason) end,
    Order  = fun snmp_conf:no_order/2,
    Check  = fun (Entry, State) -> {check_usm(Entry), State} end,
    Filter = fun snmp_conf:no_filter/1,
    [Usms] = 
	snmp_conf:read_files(Dir, [{"usm.conf", Gen, Order, Check, Filter}]),
    Usms.


generate_usm(Dir, _Reason) ->
    info_msg("Incomplete configuration. Generating empty usm.conf.", []),
    USMFile = filename:join(Dir, "usm.conf"),
    ok = file:write_file(USMFile, list_to_binary([])),
    [].


-doc false.
check_usm({EngineID, Name, SecName, Clone, AuthP, AuthKeyC, OwnAuthKeyC,
           PrivP, PrivKeyC, OwnPrivKeyC, Public, AuthKey, PrivKey}) ->
    snmp_conf:check_string(EngineID),
    snmp_conf:check_string(Name),
    snmp_conf:check_string(SecName),
    CloneVal =
        case catch snmp_conf:check_atom(Clone,[{zeroDotZero,?zeroDotZero}]) of
	    {error, _} ->
                snmp_conf:check_oid(Clone),
                Clone;
	    {ok, X} ->
                X
        end,
    AuthProtoAlt = [{usmNoAuthProtocol,            ?usmNoAuthProtocol},
		    {usmHMACMD5AuthProtocol,       ?usmHMACMD5AuthProtocol},
		    {usmHMACSHAAuthProtocol,       ?usmHMACSHAAuthProtocol},
		    {usmHMAC128SHA224AuthProtocol, ?usmHMAC128SHA224AuthProtocol},
		    {usmHMAC192SHA256AuthProtocol, ?usmHMAC192SHA256AuthProtocol},
		    {usmHMAC256SHA384AuthProtocol, ?usmHMAC256SHA384AuthProtocol},
		    {usmHMAC384SHA512AuthProtocol, ?usmHMAC384SHA512AuthProtocol}],
    {ok, AuthProto} = snmp_conf:check_atom(AuthP, AuthProtoAlt),
    snmp_conf:check_string(AuthKeyC),
    snmp_conf:check_string(OwnAuthKeyC),
    PrivProtoAlt = [{usmNoPrivProtocol,    ?usmNoPrivProtocol},
		    {usmDESPrivProtocol,   ?usmDESPrivProtocol},
		    {usmAesCfb128Protocol, ?usmAesCfb128Protocol}],
    {ok, PrivProto} = snmp_conf:check_atom(PrivP, PrivProtoAlt),
    snmp_conf:check_string(PrivKeyC),
    snmp_conf:check_string(OwnPrivKeyC),
    snmp_conf:check_string(Public),
    snmp_conf:check_string(AuthKey, alen(AuthP)),
    snmp_conf:check_string(PrivKey, plen(PrivP)),

    User = {EngineID, Name, SecName, 
	    CloneVal, AuthProto, AuthKeyC, OwnAuthKeyC,
	    PrivProto, PrivKeyC, OwnPrivKeyC, Public,
	    ?'StorageType_nonVolatile', ?'RowStatus_active', AuthKey, PrivKey},
    {ok, User};
check_usm(X) ->
    error({invalid_user, X}).

alen(usmNoAuthProtocol)            -> any;
alen(usmHMACMD5AuthProtocol)       -> 16;
alen(usmHMACSHAAuthProtocol)       -> 20;
alen(usmHMAC128SHA224AuthProtocol) -> ?usmHMAC128SHA224AuthProtocol_secret_key_length;
alen(usmHMAC192SHA256AuthProtocol) -> ?usmHMAC192SHA256AuthProtocol_secret_key_length;
alen(usmHMAC256SHA384AuthProtocol) -> ?usmHMAC256SHA384AuthProtocol_secret_key_length;
alen(usmHMAC384SHA512AuthProtocol) -> ?usmHMAC384SHA512AuthProtocol_secret_key_length.

plen(usmNoPrivProtocol)    -> any;
plen(usmDESPrivProtocol)   -> 16;
plen(usmAesCfb128Protocol) -> 16.


%%-----------------------------------------------------------------
%% This function loops through all users, and check that the
%% definition is constistent with the support for crypto on
%% the system.  Thus, it is not possible to define a user that
%% uses authentication if 'crypto' is not started, or a user that
%% uses DES if 'crypto' doesn't support DES.
%%-----------------------------------------------------------------
check_users([User | Users]) ->
    check_user(User),
    check_users(Users);
check_users([]) ->
    ok.

check_user(User) ->
    case element(?usmUserAuthProtocol, User) of
	?usmNoAuthProtocol -> ok;
	?usmHMACMD5AuthProtocol ->
	    case is_crypto_supported(md5) of
		true -> ok;
		false -> exit({unsupported_crypto, md5_mac_96})
	    end;
	?usmHMACSHAAuthProtocol ->
	    case is_crypto_supported(sha) of
		true -> ok;
		false -> exit({unsupported_crypto, sha_mac_96})
	    end;
	?usmHMAC128SHA224AuthProtocol ->
	    case is_crypto_supported(sha224) of
		true -> ok;
		false -> exit({unsupported_crypto, sha224_hmac_sha_2})
	    end;
	?usmHMAC192SHA256AuthProtocol ->
	    case is_crypto_supported(sha256) of
		true -> ok;
		false -> exit({unsupported_crypto, sha256_hmac_sha_2})
	    end;
	?usmHMAC256SHA384AuthProtocol ->
	    case is_crypto_supported(sha384) of
		true -> ok;
		false -> exit({unsupported_crypto, sha384_hmac_sha_2})
	    end;
	?usmHMAC384SHA512AuthProtocol ->
	    case is_crypto_supported(sha512) of
		true -> ok;
		false -> exit({unsupported_crypto, sha512_hmac_sha_2})
	    end
    end,
    case element(?usmUserPrivProtocol, User) of
	?usmNoPrivProtocol -> ok;
	?usmDESPrivProtocol ->
	    case is_crypto_supported(des_cbc) of
		true -> ok;
		false -> exit({unsupported_crypto, des_cbc})
	    end;
	?usmAesCfb128Protocol ->
	    case is_crypto_supported(aes_cfb128) of
		true -> ok;
		false -> exit({unsupported_crypto, aes_cfb128})
	    end
    end.
    

init_tabs(Users) ->
    ?vdebug("create usm user table",[]),
    snmpa_local_db:table_delete(db(usmUserTable)),
    snmpa_local_db:table_create(db(usmUserTable)),
    init_user_table(Users).
    
init_user_table([Row | T]) ->
    Key = mk_user_table_key(Row),
    snmpa_local_db:table_create_row(db(usmUserTable), Key, Row),
    init_user_table(T);
init_user_table([]) -> true.

mk_user_table_key(Row) ->
    Key1 = element(1, Row),
    Key2 = element(2, Row),
    [length(Key1) | Key1] ++ [length(Key2) | Key2].

table_cre_row(Tab, Key, Row) ->
    snmpa_mib_lib:table_cre_row(db(Tab), Key, Row).

table_del_row(Tab, Key) ->
    snmpa_mib_lib:table_del_row(db(Tab), Key).


-doc """
Adds a USM security data (user) to the agent config. Equivalent to one line in
the `usm.conf` file.
""".
-spec add_user(EngineID, Name, SecName, Clone, AuthP, AuthKeyC, OwnAuthKeyC,
               PrivP, PrivKeyC, OwnPrivKeyC, Public, AuthKey, PrivKey) ->
          {ok, Key} | {error, Reason} when
      EngineID    :: snmp_framework_mib:engine_id(),
      Name        :: name(),
      SecName     :: snmp_framework_mib:admin_string(),
      Clone       :: clone_from(),
      AuthP       :: auth_protocol(),
      AuthKeyC    :: key_change(),
      OwnAuthKeyC :: key_change(),
      PrivP       :: priv_protocol(),
      PrivKeyC    :: key_change(),
      OwnPrivKeyC :: key_change(),
      Public      :: public(),
      AuthKey     :: auth_key(),
      PrivKey     :: priv_key(),
      Key         :: term(),
      Reason      :: term().

add_user(EngineID, Name, SecName, Clone, AuthP, AuthKeyC, OwnAuthKeyC,
	 PrivP, PrivKeyC, OwnPrivKeyC, Public, AuthKey, PrivKey) ->
    User = {EngineID, Name, SecName, Clone, AuthP, AuthKeyC, OwnAuthKeyC,
	    PrivP, PrivKeyC, OwnPrivKeyC, Public, AuthKey, PrivKey},
    add_user(User).

-doc false.
-spec add_user(UsmEntry) -> {ok, Key} | {error, Reason} when
      UsmEntry :: usm_entry(),
      Key      :: term(),
      Reason   :: term().
      
add_user(UsmEntry) ->
    case (catch check_usm(UsmEntry)) of
	{ok, Row} ->
	    case (catch check_user(Row)) of
		{'EXIT', Reason} ->
		    {error, Reason};
		_ ->
		    Key = mk_user_table_key(Row),
		    case table_cre_row(usmUserTable, Key, Row) of
			true ->
			    {ok, Key};
			false ->
			    {error, create_failed}
		    end
		end;
	{error, Reason} ->
	    {error, Reason};
	Error ->
	    {error, Error}
    end.


-doc "Delete a USM security data (user) from the agent config.".
-spec delete_user(Key) -> ok | {error, Reason} when
      Key    :: term(),
      Reason :: term().

delete_user(Key) ->
    case table_del_row(usmUserTable, Key) of
	true ->
	    ok;
	false ->
	    {error, delete_failed}
    end.


gc_tabs() ->
    DB  = db(usmUserTable),
    STC = stc(usmUserTable),
    FOI = foi(usmUserTable),
    snmpa_mib_lib:gc_tab(DB, STC, FOI),
    ok.


%%-----------------------------------------------------------------
%% Counter functions
%%-----------------------------------------------------------------

-doc false.
usmStatsUnsupportedSecLevels(print) ->
    VarAndValue = [{usmStatsUnsupportedSecLevels, 
		    usmStatsUnsupportedSecLevels(get)}],
    snmpa_mib_lib:print_variables(VarAndValue);
usmStatsUnsupportedSecLevels(get) ->
    get_counter(usmStatsUnsupportedSecLevels).

-doc false.
usmStatsNotInTimeWindows(print) ->
    VarAndValue = [{usmStatsNotInTimeWindows, usmStatsNotInTimeWindows(get)}],
    snmpa_mib_lib:print_variables(VarAndValue);
usmStatsNotInTimeWindows(get) ->
    get_counter(usmStatsNotInTimeWindows).

-doc false.
usmStatsUnknownUserNames(print) ->
    VarAndValue = [{usmStatsUnknownUserNames, usmStatsUnknownUserNames(get)}],
    snmpa_mib_lib:print_variables(VarAndValue);
usmStatsUnknownUserNames(get) ->
    get_counter(usmStatsUnknownUserNames).

-doc false.
usmStatsUnknownEngineIDs(print) ->
    VarAndValue = [{usmStatsUnknownEngineIDs, usmStatsUnknownEngineIDs(get)}],
    snmpa_mib_lib:print_variables(VarAndValue);
usmStatsUnknownEngineIDs(get) ->
    get_counter(usmStatsUnknownEngineIDs).

-doc false.
usmStatsWrongDigests(print) ->
    VarAndValue = [{usmStatsWrongDigests, usmStatsWrongDigests(get)}],
    snmpa_mib_lib:print_variables(VarAndValue);
usmStatsWrongDigests(get) ->
    get_counter(usmStatsWrongDigests).

-doc false.
usmStatsDecryptionErrors(print) ->
    VarAndValue = [{usmStatsDecryptionErrors, usmStatsDecryptionErrors(get)}],
    snmpa_mib_lib:print_variables(VarAndValue);
usmStatsDecryptionErrors(get) ->
    get_counter(usmStatsDecryptionErrors).


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


init_vars() -> lists:map(fun maybe_create_var/1, vars()).

maybe_create_var(Var) ->
    case ets:lookup(snmp_agent_table, Var) of
	[_] -> ok;
	_ -> init_var(Var)
    end.

init_var(Var) -> ets:insert(snmp_agent_table, {Var, 0}).

vars() ->
    [
     usmStatsUnsupportedSecLevels,
     usmStatsNotInTimeWindows,
     usmStatsUnknownUserNames,
     usmStatsUnknownEngineIDs,
     usmStatsWrongDigests,
     usmStatsDecryptionErrors
    ].


%%-----------------------------------------------------------------
%% API functions
%%-----------------------------------------------------------------
-doc false.
is_engine_id_known(EngineID) ->
    EngineKey = [length(EngineID) | EngineID],
    ?vtrace("is_engine_id_known -> EngineKey: ~w", [EngineKey]),
    case table_next(usmUserTable, EngineKey) of
	endOfTable -> false;
	Key -> 
	    ?vtrace("is_engine_id_known -> Key: ~w", [Key]),
	    lists:prefix(EngineKey, Key)
    end.

-doc false.
get_user(EngineID, UserName) ->
    Key = [length(EngineID) | EngineID] ++ [length(UserName) | UserName],
    snmp_generic:table_get_row(db(usmUserTable), Key, foi(usmUserTable)).

-doc false.
get_user_from_security_name(EngineID, SecName) ->
    %% Since the normal mapping between UserName and SecName is the
    %% identityfunction, we first try to use the SecName as UserName,
    %% and check the resulting row.  If it doesn't match, we'll have to
    %% loop through the entire table.
    Key = [length(EngineID) | EngineID] ++ [length(SecName) | SecName],
    case snmp_generic:table_get_row(db(usmUserTable), Key, 
				    foi(usmUserTable)) of
	Row when is_tuple(Row) ->
	    ?vtrace("get_user_from_security_name -> "
		    "found user using the identity function", []),
	    Row;
	undefined ->
	    ?vtrace("get_user_from_security_name -> "
		    "user *not* found using the identity function", []),
	    F = fun(_, Row) when (
			      (element(?usmUserEngineID,Row) =:= EngineID) andalso 
			      (element(?usmUserSecurityName,Row) =:= SecName)) ->
			throw({ok, Row});
		   (_, _) ->
			ok
		end,
	    case catch snmp_generic:table_foreach(db(usmUserTable), F) of
		{ok, Row} ->
		    Row;
		_Else ->
		    undefined
	    end
    end.
		

%%-----------------------------------------------------------------
%% Instrumentation Functions
%%-----------------------------------------------------------------

-doc false.
usmUserSpinLock(print) ->
    VarAndValue = [{usmUserSpinLock, usmUserSpinLock(get)}],
    snmpa_mib_lib:print_variables(VarAndValue);
    
usmUserSpinLock(new) ->
    snmp_generic:variable_func(new, {usmUserSpinLock, volatile}),
    ?SNMP_RAND_SEED(),
    %% rand:seed(exrop,
    %%           {erlang:phash2([node()]),
    %%            erlang:monotonic_time(),
    %%            erlang:unique_integer()}),
    Val = rand:uniform(2147483648) - 1,
    snmp_generic:variable_func(set, Val, {usmUserSpinLock, volatile});

usmUserSpinLock(delete) ->
    ok;

usmUserSpinLock(get) ->
    snmp_generic:variable_func(get, {usmUserSpinLock, volatile}).

-doc false.
usmUserSpinLock(is_set_ok, NewVal) ->
    case snmp_generic:variable_func(get, {usmUserSpinLock, volatile}) of
	{value, NewVal} -> noError;
	_ -> inconsistentValue
    end;
usmUserSpinLock(set, NewVal) ->
    snmp_generic:variable_func(set, (NewVal + 1) rem 2147483648,
			       {usmUserSpinLock, volatile}).


%% Op = print - Used for debugging purposes
-doc false.
usmUserTable(print) ->
    Table = usmUserTable, 
    DB    = db(Table),
    FOI   = foi(Table),
    %% TableInfo = snmpa_mib_lib:get_table(db(Table), foi(Table)),
    PrintRow = 
	fun(Prefix, Row) ->
		lists:flatten(
		  io_lib:format("~sEngineID:         ~p"
				"~n~sName:             ~p"
				"~n~sSecurityName:     ~p"
				"~n~sCloneFrom:        ~p"
				"~n~sAuthProtocol:     ~p (~w)"
				"~n~sAuthKeyChange:    ~p"
				"~n~sOwnAuthKeyChange: ~p"
				"~n~sPrivProtocol:     ~p (~w)"
				"~n~sPrivKeyChange:    ~p"
				"~n~sOwnPrivKeyChange: ~p"
				"~n~sPublic:           ~p"
				"~n~sStorageType:      ~p (~w)"
				"~n~sStatus:           ~p (~w)", 
				[Prefix, element(?usmUserEngineID, Row),
				 Prefix, element(?usmUserName, Row),
				 Prefix, element(?usmUserSecurityName, Row),
				 Prefix, element(?usmUserCloneFrom, Row),
				 Prefix, element(?usmUserAuthProtocol, Row),
				 case element(?usmUserAuthProtocol, Row) of
				     ?usmNoAuthProtocol            -> none;
				     ?usmHMACMD5AuthProtocol       -> md5;
				     ?usmHMACSHAAuthProtocol       -> sha;
				     ?usmHMAC128SHA224AuthProtocol -> sha224;
				     ?usmHMAC192SHA256AuthProtocol -> sha256;
				     ?usmHMAC256SHA384AuthProtocol -> sha384;
				     ?usmHMAC384SHA512AuthProtocol -> sha512;
				     md5    -> md5;
				     sha    -> sha;
				     sha224 -> sha224;
				     sha256 -> sha256;
				     sha384 -> sha384;
				     sha512 -> sha512;
				     _ -> undefined
				 end,
				 Prefix, element(?usmUserAuthKeyChange, Row),
				 Prefix, element(?usmUserOwnAuthKeyChange, Row),
				 Prefix, element(?usmUserPrivProtocol, Row),
				 case element(?usmUserPrivProtocol, Row) of
				     ?usmNoPrivProtocol    -> none;
				     ?usmDESPrivProtocol   -> des;
				     ?usmAesCfb128Protocol -> aes;
				     des -> des;
				     aes -> aes;
				     _ -> undefined
				 end,
				 Prefix, element(?usmUserPrivKeyChange, Row),
				 Prefix, element(?usmUserOwnPrivKeyChange, Row),
				 Prefix, element(?usmUserPublic, Row),
				 Prefix, element(?usmUserStorageType, Row),
				 case element(?usmUserStorageType, Row) of
				     ?'usmUserStorageType_readOnly' -> readOnly;
				     ?'usmUserStorageType_permanent' -> permanent;
				     ?'usmUserStorageType_nonVolatile' -> nonVolatile;
				     ?'usmUserStorageType_volatile' -> volatile;
				     ?'usmUserStorageType_other' -> other;
				     _ -> undefined
				 end,
				 Prefix, element(?usmUserStatus, Row),
				 case element(?usmUserStatus, Row) of
				     ?'usmUserStatus_destroy' -> destroy;
				     ?'usmUserStatus_createAndWait' -> createAndWait;
				     ?'usmUserStatus_createAndGo' -> createAndGo;
				     ?'usmUserStatus_notReady' -> notReady;
				     ?'usmUserStatus_notInService' -> notInService;
				     ?'usmUserStatus_active' -> active;
				     _ -> undefined
				 end]))
	end,
    snmpa_mib_lib:print_table(Table, DB, FOI, PrintRow);
%% Op == new | delete
usmUserTable(Op) ->
    snmp_generic:table_func(Op, db(usmUserTable)).

%% Op == get | is_set_ok | set | get_next
-doc false.
usmUserTable(get, RowIndex, Cols) ->
    get_patch(Cols, get(usmUserTable, RowIndex, Cols));
usmUserTable(get_next, RowIndex, Cols) ->
    next_patch(next(usmUserTable, RowIndex, Cols));
usmUserTable(is_set_ok, RowIndex, Cols0) ->
    ?vtrace("usmUserTable(is_set_ok) -> entry with"
	    "~n   RowIndex: ~p"
	    "~n   Cols0:    ~p", [RowIndex, Cols0]),
    case (catch verify_usmUserTable_cols(Cols0, [])) of
	{ok, Cols} ->
	    ?vtrace("usmUserTable(is_set_ok) -> verified: "
		    "~n   Cols: ~p", [Cols]),
	    %% Add a dummy value for securityName; otherwise snmp_generic will
	    %% think that a value is missing, so the row can't be created.
	    %% Note: this value is only added for is_set_ok, not for set!
	    NCols = [{?usmUserSecurityName, ""} | Cols],
	    IsSetOkRes = snmp_generic:table_func(is_set_ok, RowIndex,
						 NCols, db(usmUserTable)),
	    ?vtrace("usmUserTable(is_set_ok) -> tested: "
		    "~n   IsSetOkRes: ~p", [IsSetOkRes]),
	    validate_is_set_ok(IsSetOkRes, RowIndex, Cols);
	Error ->
	    Error
    end;
usmUserTable(set, RowIndex, Cols0) ->
    ?vtrace("usmUserTable(set) -> entry with"
	    "~n   RowIndex: ~p"
	    "~n   Cols0:    ~p", [RowIndex, Cols0]),
    case (catch verify_usmUserTable_cols(Cols0, [])) of
	{ok, Cols} ->
	    ?vtrace("usmUserTable(set) -> verified"
		    "~n   Cols: ~p", [Cols]),
        % check whether we're cloning. if so, get cloned params and add a few
        % defaults that might be needed.
	    NCols = pre_set(RowIndex, validate_clone_from(RowIndex, Cols)),
	    ?vtrace("usmUserTable(set) -> pre-set: "
		    "~n   NCols: ~p", [NCols]),
	    %% NOTE: The NCols parameter is sent to snmp_generic, but not to
	    %% validate_set!  The reason is that the columns from pre_set are
	    %% set in snmp_generic, but not used by validate_set.
	    validate_set(snmp_generic:table_func(set, RowIndex,
						 NCols, db(usmUserTable)),
			 RowIndex, Cols);
	Error ->
	    Error
    end;
usmUserTable(Op, Arg1, Arg2) ->
    snmp_generic:table_func(Op, Arg1, Arg2, db(usmUserTable)).


verify_usmUserTable_cols([], Cols) ->
    ?vtrace("verify_usmUserTable_cols -> entry when done with"
	    "~n   Cols: ~p", [Cols]),
    {ok, lists:reverse(Cols)};
verify_usmUserTable_cols([{Col, Val0}|Cols], Acc) ->
    ?vtrace("verify_usmUserTable_cols -> entry with"
	    "~n   Col:  ~p"
	    "~n   Val0: ~p", [Col, Val0]),
    Val = verify_usmUserTable_col(Col, Val0),
    ?vtrace("verify_usmUserTable_cols -> verified: "
	    "~n   Val: ~p", [Val]),
    verify_usmUserTable_cols(Cols, [{Col, Val}|Acc]).

verify_usmUserTable_col(?usmUserEngineID, EngineID) ->
    case (catch snmp_conf:check_string(EngineID)) of
	ok ->
	    EngineID;
	_ ->
	    wrongValue(?usmUserEngineID)
    end;
verify_usmUserTable_col(?usmUserName, Name) ->
    case (catch snmp_conf:check_string(Name)) of
	ok ->
	    Name;
	_ ->
	    wrongValue(?usmUserName)
    end;
verify_usmUserTable_col(?usmUserSecurityName, Name) ->
    case (catch snmp_conf:check_string(Name)) of
	ok ->
	    Name;
	_ ->
	    wrongValue(?usmUserSecurityName)
    end;
verify_usmUserTable_col(?usmUserCloneFrom, Clone) ->
    case Clone of
	zeroDotZero  -> ?zeroDotZero;
	?zeroDotZero -> ?zeroDotZero;
	_ ->
	    case (catch snmp_conf:check_oid(Clone)) of
		ok ->
		    Clone;
		_ ->
		    wrongValue(?usmUserCloneFrom)
	    end
    end;
verify_usmUserTable_col(?usmUserAuthProtocol, AuthP) ->
    case AuthP of
	usmNoAuthProtocol             -> ?usmNoAuthProtocol;
	usmHMACSHAAuthProtocol        -> ?usmHMACSHAAuthProtocol;
	usmHMACMD5AuthProtocol        -> ?usmHMACMD5AuthProtocol;
        usmHMAC128SHA224AuthProtocol  -> ?usmHMAC128SHA224AuthProtocol;
        usmHMAC192SHA256AuthProtocol  -> ?usmHMAC192SHA256AuthProtocol;
        usmHMAC256SHA384AuthProtocol  -> ?usmHMAC256SHA384AuthProtocol;
        usmHMAC384SHA512AuthProtocol  -> ?usmHMAC384SHA512AuthProtocol;
	?usmNoAuthProtocol            -> ?usmNoAuthProtocol;
	?usmHMACSHAAuthProtocol       -> ?usmHMACSHAAuthProtocol;
	?usmHMACMD5AuthProtocol       -> ?usmHMACMD5AuthProtocol;
	?usmHMAC128SHA224AuthProtocol -> ?usmHMAC128SHA224AuthProtocol;
	?usmHMAC192SHA256AuthProtocol -> ?usmHMAC192SHA256AuthProtocol;
	?usmHMAC256SHA384AuthProtocol -> ?usmHMAC256SHA384AuthProtocol;
	?usmHMAC384SHA512AuthProtocol -> ?usmHMAC384SHA512AuthProtocol;
	_ ->
	    wrongValue(?usmUserAuthProtocol)
    end;
verify_usmUserTable_col(?usmUserAuthKeyChange, AKC) ->
    case (catch snmp_conf:check_string(AKC)) of
	ok ->
	    AKC;
	_ ->
	    wrongValue(?usmUserAuthKeyChange)
    end;
verify_usmUserTable_col(?usmUserOwnAuthKeyChange, OAKC) ->
    case (catch snmp_conf:check_string(OAKC)) of
	ok ->
	    OAKC;
	_ ->
	    wrongValue(?usmUserOwnAuthKeyChange)
    end;
verify_usmUserTable_col(?usmUserPrivProtocol, PrivP) ->
    case PrivP of
	usmNoPrivProtocol     -> ?usmNoPrivProtocol;
	usmDESPrivProtocol    -> ?usmDESPrivProtocol;
	usmAesCfb128Protocol  -> ?usmAesCfb128Protocol;
	?usmNoPrivProtocol    -> ?usmNoPrivProtocol;
	?usmDESPrivProtocol   -> ?usmDESPrivProtocol;
	?usmAesCfb128Protocol -> ?usmAesCfb128Protocol;
	_ ->
	    wrongValue(?usmUserPrivProtocol)
    end;
verify_usmUserTable_col(?usmUserPrivKeyChange, PKC) ->
    case (catch snmp_conf:check_string(PKC)) of
	ok ->
	    PKC;
	_ ->
	    wrongValue(?usmUserPrivKeyChange)
    end;
verify_usmUserTable_col(?usmUserOwnPrivKeyChange, OPKC) ->
    case (catch snmp_conf:check_string(OPKC)) of
	ok ->
	    OPKC;
	_ ->
	    wrongValue(?usmUserOwnPrivKeyChange)
    end;
verify_usmUserTable_col(?usmUserPublic, Public) ->
    case (catch snmp_conf:check_string(Public)) of
	ok ->
	    Public;
	_ ->
	    wrongValue(?usmUserPublic)
    end;
verify_usmUserTable_col(_, Val) ->
    Val.
				      

%% Patch the values stored in the DB with other values for some
%% objects.
get_patch([?usmUserCloneFrom | Cols], [{value, _Val} | Vals]) ->
    [{value, ?zeroDotZero} | get_patch(Cols, Vals)];
get_patch([?usmUserAuthKeyChange | Cols], [{value, _Val} | Vals]) ->
    [{value, ""} | get_patch(Cols, Vals)];
get_patch([?usmUserOwnAuthKeyChange | Cols], [{value, _Val} | Vals]) ->
    [{value, ""} | get_patch(Cols, Vals)];
get_patch([?usmUserPrivKeyChange | Cols], [{value, _Val} | Vals]) ->
    [{value, ""} | get_patch(Cols, Vals)];
get_patch([?usmUserOwnPrivKeyChange | Cols], [{value, _Val} | Vals]) ->
    [{value, ""} | get_patch(Cols, Vals)];
get_patch([_Col | Cols], [Val | Vals]) ->
    [Val | get_patch(Cols, Vals)];
get_patch(_Cols, Result) ->
    Result.

next_patch([{[?usmUserCloneFrom | Idx], _Val} | Vals]) ->
    [{[?usmUserCloneFrom | Idx], ?zeroDotZero} | next_patch(Vals)];
next_patch([{[?usmUserAuthKeyChange | Idx], _Val} | Vals]) ->
    [{[?usmUserAuthKeyChange | Idx], ""} | next_patch(Vals)];
next_patch([{[?usmUserOwnAuthKeyChange | Idx], _Val} | Vals]) ->
    [{[?usmUserOwnAuthKeyChange | Idx], ""} | next_patch(Vals)];
next_patch([{[?usmUserPrivKeyChange | Idx], _Val} | Vals]) ->
    [{[?usmUserPrivKeyChange | Idx], ""} | next_patch(Vals)];
next_patch([{[?usmUserOwnPrivKeyChange | Idx], _Val} | Vals]) ->
    [{[?usmUserOwnPrivKeyChange | Idx], ""} | next_patch(Vals)];
next_patch([Val | Vals]) ->
    [Val | next_patch(Vals)];
next_patch(Result) -> Result.


validate_is_set_ok({noError, 0}, RowIndex, Cols) ->
    case (catch do_validate_is_set_ok(RowIndex, Cols)) of
	ok ->
	    {noError, 0};
	Error ->
	    Error
    end;
validate_is_set_ok(Error, _RowIndex, _Cols) ->
    Error.

do_validate_is_set_ok(RowIndex, Cols) ->
    NCols = validate_clone_from(RowIndex, Cols),
    validate_auth_protocol(RowIndex, NCols),
    validate_auth_key_change(RowIndex, NCols),
    validate_own_auth_key_change(RowIndex, NCols),
    validate_priv_protocol(RowIndex, NCols),
    validate_priv_key_change(RowIndex, NCols),
    validate_own_priv_key_change(RowIndex, NCols),
    ok.
    
pre_set(RowIndex, Cols) ->
    %% Remove the ?is_cloning member again; it must no longer be
    %% present.
    Cols0 = key1delete(?is_cloning, Cols),
    %% Possibly initialize the usmUserSecurityName and privacy keys
    case snmp_generic:table_row_exists(db(usmUserTable), RowIndex) of
	true -> Cols0;
	false ->
	    SecName = get_user_name(RowIndex),
	    Cols1 = [{?usmUserSecurityName, SecName} | Cols0],
        case proplists:get_value(?is_cloning, Cols) of
            true ->
                % the row is just being cloned. the cloned user's
                % passwords are already present in Cols and must
                % not be overwritten.
                Cols1;
            _ ->
                Cols1 ++ [{?usmUserAuthKey, ""},
                    {?usmUserPrivKey, ""}]
        end
    end.

validate_set({noError, 0}, RowIndex, Cols) ->
    %% Now, all is_set_ok validation steps have been executed.  So
    %% everything is ready for the set.
    set_auth_key_change(RowIndex, Cols),
    set_own_auth_key_change(RowIndex, Cols),
    set_priv_key_change(RowIndex, Cols),
    set_own_priv_key_change(RowIndex, Cols),
    {noError, 0};
validate_set(Error, _RowIndex, _Cols) ->
    Error.

%%-----------------------------------------------------------------
%% Here's the alg: If this is the first time the CloneFrom is written,
%% we must check that the CloneFrom row exists, so we can invoke the
%% clone process in the set phase.  Otherwise, the set succeed, with
%% no further checks.
%%-----------------------------------------------------------------
validate_clone_from(RowIndex, Cols) ->
    case key1search(?usmUserCloneFrom, Cols) of
	{value, {_Col, RowPointer}} ->
	    RowIndex2 = extract_row(RowPointer),
	    OldCloneFrom = snmp_generic:table_get_element(db(usmUserTable),
							  RowIndex,
							  ?usmUserCloneFrom),
	    case OldCloneFrom of
		{value, Val} when Val /= noinit ->
		    %% This means that the cloning is already done...
		    no_cloning(Cols);
		_ ->
		    %% Otherwise, we must check the CloneFrom value. It
            %% must relate to a usmUserEntry that exists and is active.
            case snmp_generic:table_get_row(db(usmUserTable), RowIndex2) of
                CloneFromRow when is_tuple(CloneFromRow) ->
                    case element(?usmUserStatus, CloneFromRow) of
                        ?'RowStatus_active' ->
                            get_cloned_cols(CloneFromRow, Cols);
                        _ ->
                            inconsistentName(?usmUserCloneFrom)
                    end;
                undefined ->
                    inconsistentName(?usmUserCloneFrom)
            end
	    end;
	false ->
        % no ?usmUserCloneFrom specified, don't modify columns
        no_cloning(Cols)
    end.

get_cloned_cols(CloneFromRow, Cols) ->
    % initialize cloned columns with data from CloneFromRow
    % and overwrite that again with data found in Cols
    AuthP = element(?usmUserAuthProtocol, CloneFromRow),
    PrivP = element(?usmUserPrivProtocol, CloneFromRow),
    AuthK = element(?usmUserAuthKey, CloneFromRow),
    PrivK = element(?usmUserPrivKey, CloneFromRow),
    ClonedCols = [{?usmUserAuthProtocol, AuthP},
        {?usmUserPrivProtocol, PrivP},
        {?usmUserAuthKey, AuthK},
        {?usmUserPrivKey, PrivK},
        {?is_cloning, true}
    ],
    Func = fun({Col, _} = Item, NCols) ->
            key1store(Col, NCols, Item)
    end,
    Cols1 = lists:foldl(Func, ClonedCols, Cols),
    key1sort(Cols1).

no_cloning(Cols0) ->
    Cols1 = key1delete(?usmUserCloneFrom, Cols0),
    key1delete(?is_cloning, Cols1).


validate_auth_protocol(RowIndex, Cols) ->
    case key1search(?usmUserAuthProtocol, Cols) of
	{value, {_Col, AuthProtocol}} ->
	    %% Check if the row is being cloned; we can't check the
	    %% old value of authProtocol, because if the row was
	    %% createAndWaited, the default value would have been
	    %% written (usmNoAuthProtocol).
	    IsCloning = proplists:get_value(?is_cloning, Cols, false),
	    if
		not IsCloning ->
		    %% This means that the row is not being cloned right
            %% now; set is ok if new protocol is usmNoAuthProtocol
		    case AuthProtocol of
			?usmNoAuthProtocol ->
			    %% Check that the Priv protocol is noPriv
			    case get_priv_proto(RowIndex, Cols) of
				?usmNoPrivProtocol -> ok;
				_ -> inconsistentValue(?usmUserAuthProtocol)
			    end;
			?usmHMACMD5AuthProtocol ->
			    inconsistentValue(?usmUserAuthProtocol);
			?usmHMACSHAAuthProtocol ->
			    inconsistentValue(?usmUserAuthProtocol);
			?usmHMAC128SHA224AuthProtocol ->
			    inconsistentValue(?usmUserAuthProtocol);
			?usmHMAC192SHA256AuthProtocol ->
			    inconsistentValue(?usmUserAuthProtocol);
			?usmHMAC256SHA384AuthProtocol ->
			    inconsistentValue(?usmUserAuthProtocol);
			?usmHMAC384SHA512AuthProtocol ->
			    inconsistentValue(?usmUserAuthProtocol);
			_ ->
			    wrongValue(?usmUserAuthProtocol)
		    end;
		true ->
		    %% Otherwise, check that the new protocol is known,
		    %% and that the system we're running supports the
		    %% hash function.
		    case AuthProtocol of
			?usmNoAuthProtocol ->
			    %% Check that the Priv protocol is noPriv
			    case get_priv_proto(RowIndex, Cols) of
				?usmNoPrivProtocol -> ok;
				_ -> inconsistentValue(?usmUserAuthProtocol)
			    end;
			?usmHMACMD5AuthProtocol ->
			    case is_crypto_supported(md5) of
				true -> ok;
				false ->
				    wrongValue(?usmUserAuthProtocol)
			    end;
			?usmHMACSHAAuthProtocol ->
			    case is_crypto_supported(sha) of
				true -> ok;
				false ->
				    wrongValue(?usmUserAuthProtocol)
			    end;
			?usmHMAC128SHA224AuthProtocol ->
			    case is_crypto_supported(sha224) of
				true -> ok;
				false ->
				    wrongValue(?usmUserAuthProtocol)
			    end;
			?usmHMAC192SHA256AuthProtocol ->
			    case is_crypto_supported(sha256) of
				true -> ok;
				false ->
				    wrongValue(?usmUserAuthProtocol)
			    end;
			?usmHMAC256SHA384AuthProtocol ->
			    case is_crypto_supported(sha384) of
				true -> ok;
				false ->
				    wrongValue(?usmUserAuthProtocol)
			    end;
			?usmHMAC384SHA512AuthProtocol ->
			    case is_crypto_supported(sha512) of
				true -> ok;
				false ->
				    wrongValue(?usmUserAuthProtocol)
			    end;
			_ -> wrongValue(?usmUserAuthProtocol)
		    end
	    end;
	false ->
	    ok
    end.

validate_auth_key_change(RowIndex, Cols) ->
    validate_key_change(RowIndex, Cols, ?usmUserAuthKeyChange, auth).

validate_own_auth_key_change(RowIndex, Cols) ->
    validate_requester(RowIndex, Cols, ?usmUserOwnAuthKeyChange),
    validate_key_change(RowIndex, Cols, ?usmUserOwnAuthKeyChange, auth).

validate_priv_key_change(RowIndex, Cols) ->
    validate_key_change(RowIndex, Cols, ?usmUserPrivKeyChange, priv).

validate_own_priv_key_change(RowIndex, Cols) ->
    validate_requester(RowIndex, Cols, ?usmUserOwnPrivKeyChange),
    validate_key_change(RowIndex, Cols, ?usmUserOwnPrivKeyChange, priv).

%% Check that the requesting user is the same as the modified user
validate_requester(RowIndex, Cols, KeyChangeCol) ->
    case key1search(KeyChangeCol, Cols) of
	{value, _} ->
	    case get(sec_model) of % Check the securityModel in the request
		?SEC_USM -> ok;
		_ -> noAccess(KeyChangeCol)
	    end,
	    %% The SecurityName may not be set yet.  First, check if it is set.
	    SecNameForUser = 
		case snmp_generic:table_get_element(db(usmUserTable),
						    RowIndex,
						    ?usmUserSecurityName) of
		    {value, Val} when Val /= noinit -> Val;
		    _ -> get_user_name(RowIndex)
		end,
	    case get(sec_name) of % Check the securityName in the request
		SecNameForUser -> ok;
		_ -> noAccess(KeyChangeCol)
	    end;
	false ->
	    ok
    end.

validate_key_change(RowIndex, Cols, KeyChangeCol, Type) ->
    case key1search(KeyChangeCol, Cols) of
	{value, {_Col, KeyC}} ->
	    %% Check if the row has been cloned; or if it is cloned in
	    %% this set-operation.
	    OldCloneFrom = snmp_generic:table_get_element(db(usmUserTable),
							  RowIndex,
							  ?usmUserCloneFrom),
	    IsClonePresent = proplists:get_value(?is_cloning, Cols, false),
	    %% Set is ok if 1) the user already is created, 2) this is
	    %% a new user, which has been cloned, or is about to be
	    %% cloned.
	    case {OldCloneFrom, IsClonePresent} of
		{{value, Val}, _} when Val /= noinit ->
		    %% The user exists, or has been cloned
		    ok;
		{_, true} ->
		    %% The user is cloned in this operation
		    ok;
		_ ->
		    %% The user doesn't exist, or hasn't been cloned,
		    %% and is not cloned in this operation.
		    inconsistentName(KeyChangeCol)
	    end,
	    %% Check that the length makes sense
	    Len = length(KeyC),
	    case Type of
		auth ->
		    case get_auth_proto(RowIndex, Cols) of
			?usmNoAuthProtocol -> ok;
			?usmHMACMD5AuthProtocol       when Len =:= 32 -> ok;
			?usmHMACSHAAuthProtocol       when Len =:= 40 -> ok;

			?usmHMAC128SHA224AuthProtocol when Len =:= (2*?usmHMAC128SHA224AuthProtocol_secret_key_length) -> ok;
			?usmHMAC192SHA256AuthProtocol when Len =:= (2*?usmHMAC192SHA256AuthProtocol_secret_key_length) -> ok;
			?usmHMAC256SHA384AuthProtocol when Len =:= (2*?usmHMAC256SHA384AuthProtocol_secret_key_length) -> ok;
			?usmHMAC384SHA512AuthProtocol when Len =:= (2*?usmHMAC384SHA512AuthProtocol_secret_key_length) -> ok;

			_ -> wrongValue(KeyChangeCol)
		    end;
		priv ->
		    case get_priv_proto(RowIndex, Cols) of
			?usmNoPrivProtocol -> ok;
			?usmDESPrivProtocol when Len == 32 -> ok;
			?usmAesCfb128Protocol when Len == 32 -> ok;
			_ -> wrongValue(KeyChangeCol)
		    end
	    end;
	false ->
	    ok
    end.

validate_priv_protocol(RowIndex, Cols) ->
    case key1search(?usmUserPrivProtocol, Cols) of
	{value, {_Col, PrivProtocol}} ->
	    %% Check if the row has been cloned; we can't check the
	    %% old value of privhProtocol, because if the row was
	    %% createAndWaited, the default value would have been
	    %% written (usmNoPrivProtocol).
        IsCloning = proplists:get_value(?is_cloning, Cols, false),
	    if
        not IsCloning ->
		    %% This means that the cloning is already done; set is ok
		    %% if new protocol is usmNoPrivProtocol
		    case PrivProtocol of
			?usmNoPrivProtocol ->
			    ok;
			?usmDESPrivProtocol ->
			    inconsistentValue(?usmUserPrivProtocol);
			?usmAesCfb128Protocol ->
			    inconsistentValue(?usmUserPrivProtocol);
			_ ->
			    wrongValue(?usmUserPrivProtocol)
		    end;
		true ->
		    %% Otherwise, check that the new protocol is known,
		    %% and that the system we're running supports the
		    %% crypto function.
		    case PrivProtocol of
			?usmNoPrivProtocol ->
			    ok;
			?usmDESPrivProtocol ->
			    %% The 'catch' handles the case when 'crypto' is
			    %% not present in the system.
			    case is_crypto_supported(des_cbc) of
				true ->
				    case get_auth_proto(RowIndex, Cols) of
					?usmNoAuthProtocol ->
					    inconsistentValue(?usmUserPrivProtocol);
					_ ->
					    ok
				    end;
				false -> 
				    wrongValue(?usmUserPrivProtocol)
			    end;
			?usmAesCfb128Protocol ->
			    %% The 'catch' handles the case when 'crypto' is
			    %% not present in the system.
			    case is_crypto_supported(aes_cfb128) of
				true ->
				    case get_auth_proto(RowIndex, Cols) of
					?usmNoAuthProtocol ->
					    inconsistentValue(?usmUserPrivProtocol);
					_ ->
					    ok
				    end;
				false -> 
				    wrongValue(?usmUserPrivProtocol)
			    end;
			_ -> wrongValue(?usmUserPrivProtocol)
		    end
	    end;
	false ->
	    ok
    end.


set_auth_key_change(RowIndex, Cols) ->
    set_key_change(RowIndex, Cols, ?usmUserAuthKeyChange, auth).

set_own_auth_key_change(RowIndex, Cols) ->
    set_key_change(RowIndex, Cols, ?usmUserOwnAuthKeyChange, auth).

set_priv_key_change(RowIndex, Cols) ->
    set_key_change(RowIndex, Cols, ?usmUserPrivKeyChange, priv).

set_own_priv_key_change(RowIndex, Cols) ->
    set_key_change(RowIndex, Cols, ?usmUserOwnPrivKeyChange, priv).

set_key_change(RowIndex, Cols, KeyChangeCol, Type) ->
    case key1search(KeyChangeCol, Cols) of
	{value, {_Col, KeyChange}} ->
	    KeyCol = case Type of
			 auth -> ?usmUserAuthKey;
			 priv -> ?usmUserPrivKey
		     end,
	    [AuthP, Key] =
		snmp_generic:table_get_elements(db(usmUserTable),
						RowIndex,
						[?usmUserAuthProtocol,
						 KeyCol]),
	    NewKey = extract_new_key(AuthP, Key, KeyChange),
	    snmp_generic:table_set_element(db(usmUserTable), RowIndex,
					   KeyCol, NewKey);
	false ->
	    ok
    end.

%% Extract the UserName part from a RowIndex.
get_user_name([L1 | Rest])         -> get_user_name(L1, Rest).
get_user_name(0, [_L2 | UserName]) -> UserName;
get_user_name(N, [_H | T])          -> get_user_name(N-1, T).

extract_row(RowPtr)                     -> extract_row(?usmUserEntry, RowPtr).
extract_row([H | T], [H | T2])          -> extract_row(T, T2);
extract_row([], [?usmUserSecurityName | T]) -> T;
extract_row(_, _) -> wrongValue(?usmUserCloneFrom).

%% Pre: the user exists or is being cloned in this operation
get_auth_proto(RowIndex, Cols) ->
    %% The protocol can be changed by the request too, otherwise,
    %% check the stored protocol.
    case key1search(?usmUserAuthProtocol, Cols) of
	{value, {_, Protocol}} ->
	    Protocol;
	false ->
	    %% OTP-3596
	    case snmp_generic:table_get_element(db(usmUserTable),
						RowIndex,
						?usmUserAuthProtocol) of
		{value, Protocol} ->
		    Protocol;
		_ ->
		    undefined
	    end
    end.

%% Pre: the user exists or is being cloned in this operation
get_priv_proto(RowIndex, Cols) ->
    %% The protocol can be changed by the request too, otherwise,
    %% check the stored protocol.
    case key1search(?usmUserPrivProtocol, Cols) of
	{value, {_, Protocol}} ->
	    Protocol;
	false ->
	    %% OTP-3596
	    case snmp_generic:table_get_element(db(usmUserTable),
						RowIndex,
						?usmUserPrivProtocol) of
		{value, Protocol} ->
		    Protocol;
		_ ->
		    undefined
	    end
    end.


db(X) -> snmpa_agent:db(X).

fa(usmUserTable) -> ?usmUserSecurityName.
 
foi(usmUserTable) -> ?usmUserEngineID.
 
noc(usmUserTable) -> 13.

stc(usmUserTable) -> ?usmUserStorageType.
 
next(Name, RowIndex, Cols) ->
    snmp_generic:handle_table_next(db(Name), RowIndex, Cols,
                                   fa(Name), foi(Name), noc(Name)).

-doc false.
table_next(Name, RestOid) ->
    snmp_generic:table_next(db(Name), RestOid).

 
get(Name, RowIndex, Cols) ->
    snmp_generic:handle_table_get(db(Name), RowIndex, Cols, foi(Name)).

%%-----------------------------------------------------------------
%% Key change functions.  The KeyChange Texual-Convention is
%% defined in the SNMP-USER-BASED-SM-MIB.
%% Note that this implementation supports md5 and sha, which
%% both have fixed length requirements on the length of the key;
%% thus the implementation can be (and is) simplified.
%%-----------------------------------------------------------------
-doc false.
mk_key_change(Hash, OldKey, NewKey) ->
    KeyLen = length(NewKey),
    Alg = case Hash of
	      ?usmHMACMD5AuthProtocol       -> md5;
	      ?usmHMACSHAAuthProtocol       -> sha;
	      ?usmHMAC128SHA224AuthProtocol -> sha224;
	      ?usmHMAC192SHA256AuthProtocol -> sha256;
	      ?usmHMAC256SHA384AuthProtocol -> sha384;
	      ?usmHMAC384SHA512AuthProtocol -> sha512;
	      md5    -> md5;
	      sha    -> sha;
	      sha224 -> sha224;
	      sha256 -> sha256;
	      sha384 -> sha384;
	      sha512 -> sha512
	  end,
    Random = mk_random(KeyLen),
    mk_key_change(Alg, OldKey, NewKey, KeyLen, Random).

%% This function is only exported for test purposes.  There is a test
%% case in the standard where Random is pre-defined.
-doc false.
mk_key_change(Alg, OldKey, NewKey, KeyLen, Random) ->
    %% OldKey and Random is of length KeyLen...
    Digest = lists:sublist(binary_to_list(crypto:hash(Alg, OldKey++Random)), KeyLen),
    %% ... and so is Digest
    Delta = snmp_misc:str_xor(Digest, NewKey),
    Random ++ Delta.

%% Extracts a new Key from a KeyChange value, sent by a manager.
-doc false.
extract_new_key(?usmNoAuthProtocol, OldKey, _KeyChange) ->
    OldKey;
extract_new_key(Hash, OldKey, KeyChange) ->
    KeyLen = length(OldKey),
    Alg = case Hash of
	      ?usmHMACMD5AuthProtocol       -> md5;
	      ?usmHMACSHAAuthProtocol       -> sha;
	      ?usmHMAC128SHA224AuthProtocol -> sha224;
	      ?usmHMAC192SHA256AuthProtocol -> sha256;
	      ?usmHMAC256SHA384AuthProtocol -> sha384;
	      ?usmHMAC384SHA512AuthProtocol -> sha512;
	      md5    -> md5;
	      sha    -> sha;
	      sha224 -> sha224;
	      sha256 -> sha256;
	      sha384 -> sha384;
	      sha512 -> sha512
	  end,
    {Random, Delta} = split(KeyLen, KeyChange, []),
    Digest = lists:sublist(binary_to_list(crypto:hash(Alg, OldKey++Random)), KeyLen),
    NewKey = snmp_misc:str_xor(Digest, Delta),
    NewKey.

-define(i16(Int), (Int bsr 8) band 255, Int band 255).
-define(i8(Int), Int band 255).

-doc false.
mk_random(Len) when Len =< 20 ->
    binary_to_list(crypto:strong_rand_bytes(Len)).
    
split(0, Rest, FirstRev) ->
    {lists:reverse(FirstRev), Rest};
split(N, [H | T], FirstRev) when N > 0 ->
    split(N-1, T, [H | FirstRev]).


-compile({inline, [{is_crypto_supported,1}]}).
is_crypto_supported(Func) ->
    snmp_misc:is_crypto_supported(Func). 


inconsistentValue(V) -> throw({inconsistentValue, V}).
inconsistentName(N)  -> throw({inconsistentName,  N}).
wrongValue(V)        -> throw({wrongValue,        V}).
noAccess(C)          -> throw({noAccess,          C}).
     
set_sname() ->
    set_sname(get(sname)).

set_sname(undefined) ->
    put(sname,conf);
set_sname(_) -> %% Keep it, if already set.
    ok.

error(Reason) ->
    throw({error, Reason}).


%%-----------------------------------------------------------------
%%     lists key-function(s) wrappers 

-compile({inline,key1delete/2}).
key1delete(Key, List) ->
    lists:keydelete(Key, 1, List).

-compile({inline,key1search/2}).
key1search(Key, List) ->
    lists:keysearch(Key, 1, List).

-compile({inline,key1store/3}).
key1store(Key, List, Elem) ->
    lists:keystore(Key, 1, List, Elem).

-compile({inline,key1sort/1}).
key1sort(List) ->
    lists:keysort(1, List).


%%-----------------------------------------------------------------

info_msg(F, A) ->
    ?snmpa_info("USM: " ++ F, A).

%% --- 

config_err(F, A) ->
    snmpa_error:config_err("[USER-BASED-SM-MIB]: " ++ F, A).

