# SecurityOps.java

    /*
    ** Module   : SecurityOps.java
    ** Abstract : Progress 4GL compatible security interface
    **
    ** Copyright (c) 2006-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 GES 20060331   @25287 First version implementing the USERID builtin function.
    ** 002 GES 20060331   @25294 Added ENCODE support (not Progress 4GL compatible).
    ** 003 NVS 20060418   @25585 Added changePassword() convenience method to be called from
    **                           converted code.
    ** 004 GES 20060831   @29063 Make sure encode output only includes chars from the set A-Z and
    **                           a-z using a simple table driven base 52 approach.
    ** 005 GES 20101005          Added stub (non-functional) methods for the USERID(dbname) variant.
    ** 006 CA  20130129          Added stubs for misc security-related builtins. Implemented
    **                           base64-encode/decode.
    ** 007 EVL 20130225          Adding conversion support for SETUSERID built-in.
    ** 008 GES 20130523          Minor fixes for the base64-encode/decode.
    ** 009 VIG 20130123          Implemented base64Decode and base64Encode methods using the own old
    **                           Base64.java class. Added new method checkFormat to show an error
    **                           while decoding in specific cases like the Progress does.
    ** 010 OM  20130917          Added _User metadata handling (configured in directory).
    ** 011 GES 20140117          Added HEX-DECODE and HEX-ENCODE functions.
    ** 012 GES 20140130          Added missing radix in hex-decode implementation and properly
    **                           calculated the byte array size.
    ** 013 GES 20141031          Added a compatible version of ENCODE, left old version that is used
    **                           only when a configuration flag is set.
    ** 014 EVL 20160217          Clean up comments from symbols invalid for Solaris to compile.
    ** 015 ECF 20160315          Stubbed out generateUUID.
    ** 016 OM  20160706          Disabled condition 12118.
    ** 017 OM  20180305          USERID function returns unknown value if unknown arg is passed.
    ** 018 GES 20180426          Implemented the body (basic support) of generateUUID(), added
    **                           methods for GUID.
    **     GES 20180502          MESSAGE-DIGEST (and MD5-DIGEST, SHA1-DIGEST) basic implementations.
    ** 019 CA  20181128          Initial support for GET-DB-CLIENT and SET-DB-CLIENT.
    **                           Initial support for GENERATE-RANDOM-KEY, ENCRYPT and DECRYPT (only
    **                           AES_CBC algorithm is supported at this time).
    ** 020 CA  20181220          Added getDbClient(String).
    ** 021 CA  20190516          Added missing encrypt(...) signatures.
    ** 022 IAS 20190517          Added full support for ENCRYPT/DECRYPT, GENERATE-PBE-KEY
    **                           GENERATE-PBE-SALT.
    ** 023 IAS 20190626          Changed SET-DB-CLIENT support
    ** 024 IAS 20190703          Changed support for SET-DB-CLIENT for non-default domains
    ** 025 OM  20190709          Optimize imports. Dropped unused statements.
    ** 026 EVL 20190719          Adding temporary workaround for validateCP() in certain conditions.
    ** 027 IAS 20190820          Fixed temporary workaround for validateCP().
    ** 028 IAS 20191015          Rework SET-CLIENT/SET-DB-CLIENT
    ** 029 IAS 20200112          Added DOMAIN-ACCESS-CODE validation with for sealed CP
    ** 030 IAS 20200428          Check if SEC_AUTHETICATION_* tables exist before trying to access them.
    ** 031 GES 20201125          Added OS-USERID as a FWD-specific 4GL extension.
    **     CA  20210621          ENCRYPT must return unknown if the text data is empty or unknown.
    **     OM  20210916          Cached information on existence of a table in a database to avoid subsequent
    **                           identical SQL statements.
    **     CA  20211228          The default domain access code is empty string, and not 'fwd-access-code'.
    **     IAS 20220111          Changed message level about missing SEC_AUTHENTICATION_* tables to
    **                           INFO.
    **     AL2 20220328          Added methods for BDT and proxy checks.
    **     IAS 20220404          Fixed 'tableExists' method to not depend on the table name case
    **     IAS 20220408          Check if a cipher requires IV.
    **     IAS 20220415          Misc. fixes for error reporting.
    **     IAS 20220713          Do not lock database on SET-CLIENT.
    **     TJD 20220504          Java 11 compatibility minor changes
    **     CA  20220520          Fixed generateRandomKey, to consider the current algorithm in
    **                           SECURITY-POLICY:SYMMETRIC-ENCRYPTION-ALGORITHM.
    **     IAS 20220415          Misc. fixes for error reporting.
    ** 032 TT  20230215          GENERATE-RANDOM-KEY now works correctly with the DES and DES3 ciphers
    ** 033 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 034 DDF 20230705          Replace static field initialization with a method called at server bootstrap.
    ** 035 SB  20240516          Change messageDigest to return a raw with the unknown value if the data
    **                           is unknown. Refs #8778.
    ** 036 RAA 20240513          Before validating the client-principal, check that the context has been reset.
    **     RAA 20240710          Added validateClientPrincipalHandle function.
    **     RAA 20240715          Clarified the type of database name occurrences.
    **     RAA 20240717          When reading domains in multi-tenant mode, read them from TenantManager.
    **     RAA 20240808          Added multi-tenancy check.
    ** 037 CA  20240809          Improved hexEncode, to avoid StringBuilder.
    ** 038 OM  20240912          Added no-active transaction check when tenant is changed.
    ** 039 OM  20241001          getUserIdFromDB() returns the user@domain when tenant is active.
    ** 040 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
    **                           based on [sharedDb] parameter.
    ** 041 OM  20241204          The [sec_authentication_domain] and [sec_authentication_system] meta tables are
    **                           accessed from default persistent database, not from [landlord] database.
    ** 042 OM  20250227          Local optimizations: avoided multiple context lookups, etc.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.util;
    import java.nio.charset.*;
    import java.security.*;
    import java.sql.*;
    import java.util.*;
    import java.util.logging.*;
    import java.util.regex.*;
    import java.util.stream.*;
    import javax.crypto.*;
    import com.goldencode.p2j.*;
    import com.goldencode.p2j.directory.*;
    // explicitly import Base64 class, as it conflicts with other versions
    import com.goldencode.p2j.directory.Base64;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.security.SecurityManager;
    import com.goldencode.p2j.util.ClientPrincipal.*;
    import com.goldencode.p2j.util.ClientPrincipalResource.*;
    import com.goldencode.p2j.util.CryptoUtils.*;
    import com.goldencode.p2j.util.logging.*;

    /**
     * Provides static utility methods for Progress 4GL compatible security
     * functions.  At this time, the following Progress 4GL functions are handled:
     * <p>
     * <pre>
     * 4GL Function                   Java Equivalent
     * ---------------------------    ---------------------------
     * BASE64-DECODE                  {@link #base64Decode}
     * BASE64-ENCODE                  {@link #base64Encode}
     * ENCODE                         {@link #encode}
     * ENCRYPT                        {@link #encrypt}
     * GENERATE-PBE-KEY               {@link #generatePBEKey}
     * GENERATE-PBE-SALT              {@link #generatePBESalt}
     * GENERATE-UUID                  {@link #generateUUID}
     * GUID                           {@link #convertUniversalUIDToGlobalUID}
     * MD5-DIGEST                     {@link #md5Digest}
     * MESSAGE-DIGEST                 {@link #messageDigest}
     * OS-USERID                      {@link #getOSUserId}
     * SHA1-DIGEST                    {@link #sha1Digest}
     * USERID                         {@link #getUserId}
     * USERID(dbname)                 {@link #getUserIdFromDB}
     * </pre>
     * <p>
     */
    public class SecurityOps
    {
       /** Constant for blank user. In 4GL the blank user is equivalent of guest user. */
       public static final String BLANK_USER = "";

       /** _User table of the database is empty, no access privilege needed. */
       public static final int FREE_ACCESS = 0;

       /** Database has userids stored in _user table, but do not enforce them. */
       public static final int BLANK_ALLOWED = 1;

       /** Only with valid user/password are allowed to access a database. */
       public static final int STRICT = 2;

       /** _User table based authentication domain type */
       public static final String _USER_TABLE_AUTH_SYSTEM = "_oeusertable";

       /** Random for the salt generation */
       private static final Random RND = new SecureRandom();

       /** Context-local instance. */
       private static final ContextLocal<WorkArea> local = new ContextLocal<WorkArea>()
       {
          protected WorkArea initialValue()
          {
             return new WorkArea();
          }
       };

       /** The default domain access code. */
       private static final String DEFAULT_DOMAIN_ACCESS_CODE = ""; // the default access code is empty string

       /** The default classname for _User security handler */
       private static final String DEFAULT_SECURITY_OPS_HANDLER =
          "com.goldencode.p2j.util.SecurityManagerSecurityOps";

       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(SecurityOps.class);

       /** Table of all alphabetic characters used to limit the encode output. */
       private static final String ALPHAS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
                                            "abcdefghijklmnopqrstuvwxyz";

       /** Progress error msg, can be shown while base64-encode method error. */
       private static final String ERR12118 = "Unable to allocate memory for result from BASE64-ENCODE function";

       /** Progress error msg, can be shown while base64-decode method error. */
       private static final String ERR12119 = "Error converting Base64 to RAW";

       /** Base64 PAD service symbol */
       private static final char PAD = '=';

       /** Initial CRC value. */
       private static final int INITIAL_CRC_VALUE = 0x11;

       /** Number of bytes in the encoded output. */
       private static final int OUTPUT_ARRAY_SIZE = 16;

       /** Number of iterations in the core encoding loop. */
       private static final int CORE_LOOP_PASSES = 5;

       /** Reversed polynomial used with the CRC-16 algorithm. */
       private static final int REVERSED_POLYNOMIAL = 0xA001;

       /** Special authentication domain types */
       private static final Set<String> SPECIAL_AUTH_SYSTEMS = Collections.unmodifiableSet(
             new HashSet<String>()
             {{
               add("_extsso");
               add("_oslocal");
             }}
       );

       /** The current _User security handler for emulation of USERID and SETUSERID P4GL functions. */
       private static final CustomSecurityOps customSecurityOps = instantiateUserHandler();

       /** Random number generator. */
       private static final Random RAND = new Random(System.currentTimeMillis());

       /** Collects information of the existence of some tables for each physical database. */
       private static final Map<String, Boolean> checkedTables = new HashMap<>();

       /** Regexp for extracting key kength */
       private static final Pattern KEY_LEN = Pattern.compile("^.*_([0-9]*)$");

       /** Regexp for detecting DES/DES3 cipher */
       private static final Pattern DES = Pattern.compile("^DES3?_.*");

       /** The SQL name of the {@code _sec-authentication-domain} meta table.*/
       public static final String META_SEC_AUTHENTICATION_DOMAIN = "meta_sec_authentication_domain";

       /** The SQL name of the {@code _sec-authentication-system} meta table.*/
       public static final String META_SEC_AUTHENTICATION_SYSTEM = "meta_sec_authentication_system";

       /** Flag to enable the old incompatible ENCODE. This is 4GL compatible by default. */
       private static boolean compatible = true;

       /** The timestamps when the _sec-auth-system were invalidated by a write operation on that table. */
       private static final Map<String, Long> authSystemInvalidationTime =
             Collections.synchronizedMap(new HashMap<>());

       /** The timestamps when the _sec-auth-domain were invalidated by a write operation on that table. */
       private static final Map<String, Long> authDomainInvalidationTime =
             Collections.synchronizedMap(new HashMap<>());

       /** The timestamps when the _users were invalidated by a write operation on that table. */
       private static final Map<String, Long> usersInvalidationTime =
             Collections.synchronizedMap(new HashMap<>());

       /**
        * Instantiates and returns a SecurityOps handler for emulation of USERID and SETUSERID P4GL
        * functions as configured in the directory.
        * <p>
        * At this moment, only two of implementation are available: SecurityManagerSecurityOps and
        * DatabaseTableSecurityOps. If the directory does not contain an entry for
        * 'userSecurityOpsClass' then the default SecurityManagerSecurityOps will be used.
        *
        * @return  an instance of SecurityOps handler as configured in directory
        */
       private static CustomSecurityOps instantiateUserHandler()
       {
          Directory dir = DirectoryManager.getInstance();
          String handlerName = dir.getString(Directory.ID_RELATIVE,
                                             "userSecurityOpsClass",
                                             DEFAULT_SECURITY_OPS_HANDLER);

          // use the class-loader to get the class into memory
          Class<?> cl = null;
          try
          {
             cl = Class.forName(handlerName);
          }
          catch (Exception exc)
          {
             if (LOG.isLoggable(Level.WARNING))
             {
                LOG.log(Level.WARNING,
                        "Can't load user security handler '" + handlerName + "'. Loading default.",
                        exc);
             }
             return getDefaultUserHandler();
          }

          // attempt to instantiate an object
          Object o = null;
          try
          {
             o = cl.getDeclaredConstructor().newInstance();
          }
          catch (Exception exc)
          {
             if (LOG.isLoggable(Level.WARNING))
             {
                LOG.log(Level.WARNING,
                        "User security handler '" + handlerName + "'cannot be instantiated. Loading default.",
                        exc);
             }
             return getDefaultUserHandler();
          }

          // make sure the object is of the desired type
          if (!(o instanceof CustomSecurityOps))
          {
             if (LOG.isLoggable(Level.WARNING))
             {
                LOG.log(Level.WARNING,
                        "Class '" + handlerName + "'cannot be used as security handler. Loading default.");
             }
             return getDefaultUserHandler();
          }

          return (CustomSecurityOps) o;
       }

       /**
        * Instantiate the default _User handler and returns it. This method must not be called
        * multiple times as only one _User handler should exist.
        *
        * @return  An instance of CustomSecurityOps
        */
       private static CustomSecurityOps getDefaultUserHandler()
       {
          return new SecurityManagerSecurityOps();
       }

       /**
        * Method called at server bootstrap that initializes values from the directory
        * configuration. Until this method is called, default values are used.
        */
       public static void bootstrap()
       {
          compatible = Utils.getDirectoryNodeBoolean(null, "encode-compatibility", true, false);
       }

       /**
        * Set the timestamp of the last invalidation.
        *
        * @param   database
        *          The database logical name.
        * @param   table
        *          A specific table (legacy OR converted SQL name) to be invalidated.
        */
       public static void invalidate(String database, String table)
       {
          switch (table.toLowerCase())
          {
             case META_SEC_AUTHENTICATION_DOMAIN:          // SQL name
             case TenantManager.SEC_AUTHENTICATION_DOMAIN: // legacy name
                authDomainInvalidationTime.put(database.toLowerCase(), System.currentTimeMillis());
                break;
             case META_SEC_AUTHENTICATION_SYSTEM:          // SQL name
             case TenantManager.SEC_AUTHENTICATION_SYSTEM: // legacy name
                authSystemInvalidationTime.put(database.toLowerCase(), System.currentTimeMillis());
                break;
             case MetadataSecurityOps.META_TABLE_USER: // SQL name
             case "_user":                             // legacy name
                usersInvalidationTime.put(database.toLowerCase(), System.currentTimeMillis());
                break;
          }
       }

       /**
        * Returns the userid associated with the current user.
        * <p>
        * Note that in Progress 4GL, this takes an optional string parameter with
        * the name of the logical database in which the userid is set.  There is
        * no corresponding database level authentication concept in P2J so this
        * parameter is not supported.
        *
        * @return   The current user context's userid or the empty string in the
        *           case of any problem.
        */
       public static character getUserId()
       {
          return new character(customSecurityOps.getUserId());
       }

       /**
        * Returns the userid associated with the current user of the given logical database.
        * <p>
        * This will call the current configured implementation of CustomSecurityOps. If the database
        * _User metadata is enabled then the authenticated user is returned, otherwise the default
        * SecurityManager user is returned.
        *
        * @param    dbname
        *           Logical database name.
        *
        * @return  The current user context's userid for specified database. If database is not known
        *          or not connected, the empty string is returned. If an {@code unknown} parameter is
        *          provided the method returns {@code unknown} value.
        */
       public static character getUserIdFromDB(character dbname)
       {
          if (dbname == null || dbname.isUnknown())
          {
             return new character();
          }
          // call the method with unwrapped-argument signature:
          return getUserIdFromDB(dbname.toStringMessage());
       }

       /**
        * Returns the userid associated with the current user of the given logical database.
        * <p>
        * This will call the current configured implementation of CustomSecurityOps. If the database
        * _User metadata is enabled then the authenticated user is returned, otherwise the default
        * SecurityManager user is returned.
        *
        * @param    dbname
        *           Logical database name.
        *
        * @return   The current user context's userid or the empty string in the
        *           case of any problem.
        */
       public static character getUserIdFromDB(String dbname)
       {
          ConnectionManager cm = ConnectionManager.get();
          String ldbName = cm.getLDBName(dbname);
          if (ldbName != null && cm.getTenantName(dbname) != null)
          {
             ClientPrincipal cp = ConnectionManager.getDbClient(dbname.toLowerCase());
             if (cp != null)
             {
                return new character(cp.getUserId().toJavaType() + "@" + cp.getDomainName().toJavaType());
             }
          }

          // no active tenant call the method with unwrapped-argument signature:
          return new character(customSecurityOps.getUserIdFromDB(dbname));
       }

       /**
        * Returns the os-userid associated with the current user.  This is a FWD-specific 4GL extension which
        * uses native APIs to read the operating-system userid/username (e.g. GetUserNameA in Windows).
        *
        * @return   The user's operating system userid/username or the empty string in the case of any problem.
        */
       public static character getOSUserId()
       {
          ClientParameters params = StandardServer.getClientParameters();
          return new character(params.osUserName == null ? "" : params.osUserName);
       }

       /**
        * Returns the default userid used for authentication to databases.
        * If configured with metadata security and the database does not have users defined in its
        * metadata, we will use the <code>getDefaultUserid()</code> user id.
        *
        * @param   ldbname
        *          The logical name of the database.
        *
        * @return  the default userid used for authentication to databases.
        */
       public static String getDefaultUserid(String ldbname)
       {
          if (customSecurityOps.getAuthLevel(ldbname) == SecurityOps.FREE_ACCESS)
          {
             return getDefaultUserid();
          }
          else
          {
             return BLANK_USER;
          }
       }

       /**
        * Returns the default userid used for authentication to databases that do not have _User
        * meta-table populated. Usually this is the username from the underlying OS, or empty string
        * if it cannot be detected for various causes.
        *
        * @return  the default userid used for authentication to databases.
        */
       public static String getDefaultUserid()
       {
          try
          {
             // extract the user name from the OS properties
             String defUserid = System.getProperty("user.name");

             // make sure is empty string instead of null
             return defUserid == null ? BLANK_USER : defUserid;
          }
          catch (SecurityException e)
          {
             // the security manager does not allow extracting the information return empty string, too
             return "";
          }
       }

       /**
        * Authenticates the user for specified DB connection. Checks if the user login account match
        * the corresponding record of the _User table of the database.
        *
        * @param    userid
        *           The name of the user to set as UserID.
        * @param    password
        *           The user's password.
        * @param    dbname
        *           Logical database name.
        *
        * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
        */
       public static logical setUserId(String userid, character password, character dbname)
       {
          return logical.of(customSecurityOps.setUserId(userid,
                                                        password.toStringMessage(),
                                                        dbname.toStringMessage()));
       }

       /**
        * Authenticates the user for specified DB connection. Checks if the user login account match
        * the corresponding record of the _User table of the database.
        *
        * @param    userid
        *           The name of the user to set as UserID.
        * @param    password
        *           The user's password.
        * @param    dbname
        *           Logical database name.
        *
        * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
        */
       public static logical setUserId(character userid, String password, character dbname)
       {
          return logical.of(customSecurityOps.setUserId(userid.toStringMessage(),
                                                        password,
                                                        dbname.toStringMessage()));
       }

       /**
        * Authenticates the user for specified DB connection. Checks if the user login account match
        * the corresponding record of the _User table of the database.
        *
        * @param    userid
        *           The name of the user to set as UserID.
        * @param    password
        *           The user's password.
        * @param    dbname
        *           Logical database name.
        *
        * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
        */
       public static logical setUserId(String userid, String password, character dbname)
       {
          return logical.of(customSecurityOps.setUserId(userid, password, dbname.toStringMessage()));
       }

       /**
        * Authenticates the user for specified DB connection. Checks if the user login account match
        * the corresponding record of the _User table of the database.
        *
        * @param    userid
        *           The name of the user to set as UserID.
        * @param    password
        *           The user's password.
        * @param    dbname
        *           Logical database name.
        *
        * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
        */
       public static logical setUserId(character userid, character password, String dbname)
       {
          return logical.of(customSecurityOps.setUserId(userid.toStringMessage(),
                                                        password.toStringMessage(),
                                                        dbname));
       }

       /**
        * Authenticates the user for specified DB connection. Checks if the user login account match
        * the corresponding record of the _User table of the database.
        *
        * @param    userid
        *           The name of the user to set as UserID.
        * @param    password
        *           The user's password.
        * @param    dbname
        *           Logical database name.
        *
        * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
        */
       public static logical setUserId(String userid, character password, String dbname)
       {
          return logical.of(customSecurityOps.setUserId(userid, password.toStringMessage(), dbname));
       }

       /**
        * Authenticates the user for specified DB connection. Checks if the user login account match
        * the corresponding record of the _User table of the database.
        *
        * @param    userid
        *           The name of the user to set as UserID.
        * @param    password
        *           The user's password.
        * @param    dbname
        *           Logical database name.
        *
        * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
        */
       public static logical setUserId(character userid, String password, String dbname)
       {
          return logical.of(customSecurityOps.setUserId(userid.toStringMessage(), password, dbname));
       }

       /**
        * Authenticates the user for specified DB connection. Checks if the user login account match
        * the corresponding record of the _User table of the database.
        *
        * @param    userid
        *           The name of the user to set as UserID.
        * @param    password
        *           The user's password.
        * @param    dbname
        *           Logical database name.
        *
        * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
        */
       public static logical setUserId(String userid, String password, String dbname)
       {
          return logical.of(customSecurityOps.setUserId(userid, password, dbname));
       }

       /**
        * Authenticates the user for specified DB connection. Checks if the user login account match
        * the corresponding record of the _User table of the database.
        *
        * @param    userid
        *           The name of the user to set as UserID.
        * @param    password
        *           The user's password.
        *
        * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
        */
       public static logical setUserId(String userid, character password)
       {
          return logical.of(customSecurityOps.setUserId(userid, password.toStringMessage()));
       }

       /**
        * Authenticates the user for specified DB connection. Checks if the user login account match
        * the corresponding record of the _User table of the database.
        *
        * @param    userid
        *           The name of the user to set as UserID.
        * @param    password
        *           The user's password.
        *
        * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
        */
       public static logical setUserId(character userid, String password)
       {
          return logical.of(customSecurityOps.setUserId(userid.toStringMessage(), password));
       }

       /**
        * Authenticates the user for specified DB connection. Checks if the user login account match
        * the corresponding record of the _User table of the database.
        *
        * @param    userid
        *           The name of the user to set as UserID.
        * @param    password
        *           The user's password.
        *
        * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
        */
       public static logical setUserId(String userid, String password)
       {
          return logical.of(customSecurityOps.setUserId(userid, password));
       }

       /**
        * Authenticates the user for specified DB connection. Checks if the user login account match
        * the corresponding record of the _User table of the database.
        *
        * @param    userid
        *           The name of the user to set as UserID.
        * @param    password
        *           The user's password.
        *
        * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
        */
       public static logical setUserId(character userid, character password)
       {
          return logical.of(customSecurityOps.setUserId(userid.toStringMessage(),
                                                         password.toStringMessage()));
       }

       /**
        * Authenticates the user for specified DB connection. Checks if the user login account match
        * the corresponding record of the _User table of the database.
        *
        * @param    userid
        *           The name of the user to set as UserID.
        * @param    password
        *           The user's password.
        * @param    dbname
        *           Logical database name.
        *
        * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
        */
       public static logical setUserId(character userid, character password, character dbname)
       {
          return logical.of(customSecurityOps.setUserId(userid.toStringMessage(),
                                                         password.toStringMessage(),
                                                         dbname.toStringMessage()));
       }

       /**
        * Assign the specified CLIENT-PRINCIPAL to all the connected databases.
        *
        * @param    h
        *           The handle with the CLIENT-PRINCIPAL reference.
        *
        * @return   <code>true</code> if the USERID was properly set.
        */
       public static logical setDbClient(handle h)
       {
          List<String> ldbNameList = IntStream.range(1, ConnectionManager.getConnectedDbCount())
                                              .mapToObj(i -> ConnectionManager.ldbName(i).getValue())
                                              .collect(Collectors.toList());
          Boolean result = validateClientPrincipalHandle(h, ldbNameList);
          if (result != null)
          {
             return logical.of(result);
          }

          ClientPrincipal client = (ClientPrincipal) h.getResource();
          return logical.of(_setDbClient(client, ldbNameList, SealCallee.SET_DB_CLIENT));
       }

       /**
        * Assign the specified CLIENT-PRINCIPAL to the specified connected database.
        *
        * @param    h
        *           The handle with the CLIENT-PRINCIPAL reference.
        * @param    dbNum
        *           The connected database index.
        *
        * @return   <code>true</code> if the USERID was properly set.
        */
       public static logical setDbClient(handle h, int64 dbNum)
       {
          return dbNum == null || dbNum.isUnknown() ? setDbClient(h, (character) null)
                                                    : setDbClient(h, dbNum.intValue());
       }

       /**
        * Assign the specified CLIENT-PRINCIPAL to the specified connected database.
        *
        * @param    h
        *           The handle with the CLIENT-PRINCIPAL reference.
        * @param    dbNum
        *           The connected database index.
        *
        * @return   <code>true</code> if the USERID was properly set.
        */
       public static logical setDbClient(handle h, int dbNum)
       {
          String dbName = ConnectionManager.ldbName(dbNum).getValue();
          return setDbClient(h, dbName);
       }

       /**
        * Assign the specified CLIENT-PRINCIPAL to the specified connected database.
        *
        * @param    h
        *           The handle with the CLIENT-PRINCIPAL reference.
        * @param    ldbName
        *           The logical database name or alias.
        *
        * @return   <code>true</code> if the USERID was properly set.
        */
       public static logical setDbClient(handle h, character ldbName)
       {
          return setDbClient(h,
                ldbName == null || ldbName.isUnknown() ? null : ldbName.toStringMessage(),
                SealCallee.SET_DB_CLIENT);
       }

       /**
        * Assign the specified CLIENT-PRINCIPAL to the specified connected database.
        *
        * @param    h
        *           The handle with the CLIENT-PRINCIPAL reference.
        * @param    ldbName
        *           The logical database name or alias.
        * @return   <code>true</code> if the USERID was properly set.
        */
       public static logical setDbClient(handle h, String ldbName)
       {
          return setDbClient(h, ldbName, SealCallee.APP);
       }

       /**
        * Assign the specified CLIENT-PRINCIPAL to the specified connected database.
        *
        * @param    h
        *           The handle with the CLIENT-PRINCIPAL reference.
        * @param    ldbName
        *           The logical database name or alias.
        * @param    callee
        *           Where the method is called from.
        *
        * @return   <code>true</code> if the USERID was properly set.
        */
       public static logical setDbClient(handle h, String ldbName, SealCallee callee)
       {
          List<String> ldbNameList = Collections.singletonList(ldbName);
          Boolean result = validateClientPrincipalHandle(h, ldbNameList);
          if (result != null)
          {
             return logical.of(result);
          }

          ClientPrincipal client = (ClientPrincipal) h.getResource();
          if (TenantConfig.isEnabled())
          {
             if (BufferManager.get().isTransaction())
             {
                int[] errCodes = new int[] {16402, 16367, 13691, 12710};
                String[] errMsgs = new String[] {
                      "Cannot change tenant identity from within a transaction",
                      "CLIENT-PRINCIPAL validation failed in Test_DB because - " + "Insufficient privileges",
                      "Failed to set " + ldbName + "user id.",
                      "Error setting client principal"
                };
                ErrorManager.recordOrShowError(errCodes, errMsgs, false, false, false, false, false);
                return logical.FALSE;
             }

             ConnectionManager.putTenantDatabaseConnected(client, ldbName); // uses the 'old' _meta database
          }

          ldbName = validateDbName(ldbName);
          return ldbName == null ? logical.FALSE : logical.of(_setDbClient(client, ldbNameList, callee));
       }

       /**
        * Validate database name.
        *
        * @param ldbName database name.
        *
        * @return  The logical name, or null if <code>name</code> is invalid.
        */
       private static String validateDbName(String ldbName)
       {
          if (ldbName == null)
          {
             ErrorManager.recordOrShowError(new int[] {12709},
                                            new String[] {"Invalid database identifier in SET-DB-CLIENT"},
                                            false, false, false, false, false);
             return null;
          }

          ldbName = ConnectionManager.ldbName(ldbName).getValue();
          if (ldbName == null)
          {
             int[] nums = { 12704, 12710 };
             String[] texts =
             {
                "Invalid parameter for SET-DB-CLIENT",
                "Error setting client principal"
             };
             ErrorManager.recordOrShowError(nums, texts, false, false, false, false, false);
             return null;
          }

          return ldbName;
       }

       /**
        * Fully read the content of {@code _User} table from the database and store it in local data structure.
        *
        * @param   wa
        *          The contextual target data structure.
        * @param   db
        *          The database name.
        * @param   persistence
        *          The Persistence instance for the database.
        */
       private static void refreshUsers(WorkArea wa, String db, Persistence persistence)
       {
          if (!wa.dbUsers.isEmpty())
          {
             Long lastWrite = usersInvalidationTime.get(db);
             if (lastWrite == null)
             {
                // no invalidation, then we have the up-to-date revision of _user list
                return;
             }

             long lastRead = wa.usersLastRead.get(db); // should not be null since wa.dbUsers is NOT empty
             if (lastRead >= lastWrite)
             {
                // we have the last copy, no need to update
                return;
             }

             // reaching this point means the list of users is deprecated
             wa.dbUsers.clear();
          }

          wa.usersLastRead.put(db, System.currentTimeMillis());
          ConnectionManager.readUserTable(persistence, a -> wa.addUser(db, new QUserId(a[0], a[1]), a[2]));
       }

       /**
        * Read domains data from {@code _sec-authentication-domain} and {@code _sec-authentication-system tables}.
        *
        * @param   wa
        *          The contextual target data structure.
        * @param   db
        *          The database name.
        * @param   persistence
        *          The Persistence instance for the database.
        */
       private static void refreshDomains(WorkArea wa, String db, Persistence persistence)
       {
          boolean doRefresh = false;

          if (!wa.dbDomains.isEmpty())
          {
             Long lastWrite = authDomainInvalidationTime.get(db);
             if (lastWrite != null)
             {
                long lastRead = wa.domainsLastRead.get(db); // should not be null since wa.dbDomains is NOT empty
                if (lastRead < lastWrite)
                {
                   // we do NOT have the last copy, update needed
                   doRefresh = true;
                }
             }
          }
          else
          {
             doRefresh = true;
          }

          if (!doRefresh)
          {
             if (!wa.dbDomainTypes.isEmpty())
             {
                Long lastWrite = authSystemInvalidationTime.get(db);
                if (lastWrite != null)
                {
                   long lastRead = wa.domainTypesLastRead.get(db); // should not be null since wa.dbDomainTypes is NOT empty
                   if (lastRead < lastWrite)
                   {
                      // we do NOT have the last copy, update needed
                      doRefresh = true;
                   }
                }
             }
             else
             {
                doRefresh = true;
             }
          }

          // prevent next attempts to read from the same db
          if (!doRefresh)
          {
             return;
          }
          wa.dbDomains.clear();
          wa.dbDomainTypes.clear();
          wa.domainsLastRead.put(db, System.currentTimeMillis());
          wa.domainTypesLastRead.put(db, System.currentTimeMillis());

          try
          {
             // If multi-tenancy is enabled, then read the domains persistence-less
             if (TenantConfig.isEnabled())
             {
                TenantManager.readDomains(db, wa);
                return;
             }

             // Otherwise, in single-tenant mode, read them as usual
             if (!tableExists(persistence, META_SEC_AUTHENTICATION_SYSTEM, Persistence.SHARED_CTX) ||
                 !tableExists(persistence, META_SEC_AUTHENTICATION_DOMAIN, Persistence.SHARED_CTX))
             {
                LOG.log(Level.INFO,
                        "One or both of SEC_AUTHENTICATION_* tables do not exist in the '" + db +
                        "' database. Directory domains will be used");
                return;
             }

             // TODO read authentication callback name and related fields
             String queryDomainTypes =
                   "SELECT domain_type, domain_type_description " +
                   "FROM " + META_SEC_AUTHENTICATION_SYSTEM;
             ConnectionManager.readQueryResults(queryDomainTypes, persistence, rs ->
             {
                Object[] row = rs.get();
                wa.addDomainType(db, (String) row[0], (String) row[1], true);
             });

             String queryDomains =
                   "SELECT domain_name, domain_type, domain_description, domain_access_code, domain_enabled, tenant_name " +
                   "FROM " + META_SEC_AUTHENTICATION_DOMAIN;
             ConnectionManager.readQueryResults(queryDomains, persistence, rs ->
             {
                Object[] row = rs.get();
                wa.addDomain(db, (String) row[0], (String) row[1], (String) row[2],
                             SymmetricEncryption.decrypt((String) row[3]), (Boolean) row[4], (String) row[5]);
             });
          }
          catch (Exception e)
          {
             LOG.log(Level.INFO,"Failed to load domains from _SEC_AUTHENTICATION_*", e);
          }
       }

       /**
        * Check is table exists in the database. The check is done at very low level, in database driver. FWD is
        * not responsible for the construction and only manages the result set response locally.
        * <p>
        * In order to improve performance, the result is cached so a subsequent call will be faster for same
        * combination of physical database/table.
        *
        * @param   persistence
        *          The {@code Persistence} instance for the database.
        * @param   tableName
        *          The table name to be checked.
        * @param   sharedDb
        *          In case of multi-tenant environment: is the buffer shared or private?
        *
        * @return  {@code true} if table exists.
        */
       public static boolean tableExists(Persistence persistence, String tableName, boolean sharedDb)
       {
          String key = (persistence.getDatabase(sharedDb).getName() + " " + tableName).toLowerCase();

          Boolean exists = checkedTables.get(key);
          if (exists != null)
          {
             // cache hit. We encountered the call for this database/table combination
             return exists;
          }

          ResultSet rs = null;
          try
          {
             Connection con = persistence.getSession(sharedDb).getConnection();
             rs = con.getMetaData().getTables(null, null, null, new String[]{"TABLE"});
             while (rs.next())
             {
                String tName = rs.getString("TABLE_NAME");
                if (tName != null && tName.equalsIgnoreCase(tableName))
                {
                   checkedTables.put(key, true);
                   return true;
                }
             }
          }
          catch (SQLException | PersistenceException e)
          {
             LOG.log(Level.WARNING, "Failed to check the existence of " + tableName, e);
          }
          finally
          {
             if (rs != null)
             {
                try
                {
                   rs.close();
                }
                catch (SQLException sqle)
                {
                   //ignore me
                }
             }
          }

          // the table was not found defined in database meta structures or some error occurred
          checkedTables.put(key, false);
          return false;
       }

       /**
        * Assign the specified CLIENT-PRINCIPAL to the specified connected database.
        *
        * @param    client
        *           The CLIENT-PRINCIPAL instance.
        * @param    ldbNames
        *           The list of logical databases names or aliases.
        * @param    callee
        *           where the method is called from
        *
        * @return   <code>true</code> if the USERID was properly set.
        */
       private static boolean _setDbClient(ClientPrincipal client, List<String> ldbNames, SealCallee callee)
       {
          List<String> fails = new ArrayList<>();
          for (String db : ldbNames)
          {
             if (validateCP(client, db, callee))
             {
                customSecurityOps.setUserIdDirect(client.getUserId().getValue(), db,
                                                  callee != SealCallee.SET_CLIENT);
                ConnectionManager.setDbClient(client, db, callee);
             }
             else
             {
                fails.add(db);
             }
          }

          if (!fails.isEmpty())
          {
             if (callee != SealCallee.SET_CLIENT)
             {
                ErrorManager.recordOrShowError(new int[] {12710},
                      new String[] {"Error setting client principal"},
                      false, false, false, false, false);
             }
             return false;
          }

          return true;
       }

       /**
        * Perform the validation of a client principal handle.
        *
        * @param   h
        *          The handle of the {@code ClientPrincipal} reference.
        * @param   ldbNames
        *          The list of logical databases names or aliases.
        *
        * @return  {@code null} means that the process calling this one can continue,
        *          {@code true} means that the handle is unknown,
        *          {@code false} if an error appears while validating.
        */
       private static Boolean validateClientPrincipalHandle(handle h, List<String> ldbNames)
       {
          if (h.isUnknown())
          {
             ldbNames.forEach(ConnectionManager::unlock);
             return true;
          }

          if (!(h.getResource() instanceof ClientPrincipal))
          {
             ErrorManager.recordOrShowError(new int[] {12710},
                                            new String[] {"Error setting client principal"},
                                            false, false, false, false, false);
             return false;
          }

          return null;
       }

       /**
        * Get a copy of the CLIENT-PRINCIPAL instance associated with the default connected database.
        *
        * @return   A copy of the CLIENT-PRINCIPAL, or an empty instance if non is associated.
        */
       public static handle getDbClient()
       {
          return getDbClient((character) null);
       }

       /**
        * Get a copy of the CLIENT-PRINCIPAL instance associated with the specified database.
        *
        * @param    ldbName
        *           The logical database name or alias.
        *
        * @return   A copy of the CLIENT-PRINCIPAL, or an empty instance if non is associated.
        */
       public static handle getDbClient(String ldbName)
       {
          return getDbClient(new character(ldbName));
       }

       /**
        * Get a copy of the CLIENT-PRINCIPAL instance associated with the specified database.
        *
        * @param    ldbName
        *           The logical database name or alias.
        *
        * @return   A copy of the CLIENT-PRINCIPAL, or an empty instance if non is associated.
        */
       public static handle getDbClient(character ldbName)
       {
          String db = null;
          if (ldbName != null && !ldbName.isUnknown())
          {
             db = ldbName.getValue();
          }

          int[] nums = { 15875, 12796 };
          String[] texts =
          {
             "Invalid or missing database identifier for GET-DB-CLIENT() (15875)",
             "Error getting client principal. (12796)"
          };

          if (db == null)
          {
             if (ConnectionManager.getConnectedDbCount() != 1)
             {
                ErrorManager.recordOrThrowError(nums, texts, false, true);
                return new handle();
             }
             else
             {
                db = ConnectionManager.getDefaultDatabase();
             }
          }

          db = ConnectionManager.ldbName(db).getValue();
          if (db == null)
          {
             ErrorManager.recordOrThrowError(nums, texts, false, true);
             return new handle();
          }

          ClientPrincipal client = ConnectionManager.getDbClient(db);

          // TODO: if this is the 'default connection identity', then this object can't be used to
          // set the identity for other db connections

          // create an empty CLIENT-PRINCIPAL or create a copy of the entire resource
          handle res = new handle();
          res.assign(client == null ? new ClientPrincipal() : client.clone());

          return res;
       }

       /**
        * Checks if the currently authenticated user has access to a database.
        * <ul>
        *   <li>The user always has access to any database if the <code>SecurityOps</code> is
        *       configured with the default <code>SecurityManagerSecurityOps</code>.
        *   <li>When using <code>MetadataSecurityOps</code>, if the database does not have any users
        *       defined in _user meta table the authentication is not enforced so any user can access
        *       it. Otherwise, the user must have entered a correct combination of userid / password
        *       to get access to respective database.
        * </ul>
        *
        * @param   ldbName
        *          The logical name of the database.
        *
        * @return  <code>true</code> if access to requested database is permitted for currently
        *          authenticated user.
        */
       public static boolean hasAccessToDatabase(String ldbName)
       {
          return customSecurityOps.hasAccessToDatabase(ldbName);
       }

       /**
        * Check the access level for a database. The response depend on CustomSecurityOps implementation
        * on which this method will forward the message.
        *
        * @param   ldbname
        *          The logical database name to query.
        *
        * @return  The access level to specified database.
        */
       public static int getAuthLevel(String ldbname)
       {
          return customSecurityOps.getAuthLevel(ldbname);
       }

       /**
        * Performs a password change procedure for the currently logged on user.
        * <p>
        * Application has no control over the procedure itself.
        *
        * @return   failure/success indication
        *           <code>true</code> means
        */
       public static logical changePassword()
       {
          SecurityManager sm = SecurityManager.getInstance();

          if (sm != null)
          {
             return logical.of(sm.changePassword());
          }

          return logical.FALSE;
       }

       /**
        * <code>ENCODE</code> implementation which can be run in compatible mode (the default
        * which uses {@link #_encode} as a worker) or in the incompatible mode using the J2SE
        * MD5 hashing algorithm. The choice of which version to run is made using a flag
        * that is initialized from the directory when the server starts.
        * <p>
        * <b>WARNING: when the incompatible version is used the implementation is NOT compatible
        * with the Progress 4GL algorithm in any way. Any programs relying on comparisons between
        * data encoded by the Progress 4GL ENCODE and the return value from this method will NOT
        * compare as expected.</b>
        * <p>
        * By default, the Progress 4GL compatible <code>ENCODE</code> algorithm is truly compatible
        * and it will operate on stored/migrated values from the original system as expected. The
        * algorithm takes a source byte array (of any size) and generates a 16-byte one-way hash
        * using a 4GL-proprietary approach combined with a variant of CRC-16.  The basic approach is
        * to spread out the source data in an intermediate accumulator array of 16 unsigned bytes
        * and to repeatedly CRC that data and copy the resulting 2-byte CRC into successive elements
        * of this intermediate accumulator, while building the next CRC results on the accumulated
        * CRC value and the recently modified intermediate accumulator.  This set of CRCs is
        * calculated 8 times (since that will generate 16 bytes).  This basic approach is executed 5
        * times in a row. Each byte of the resulting hashed intermediate accumulator array is then
        * translated into one of the 52 possible uppercase or lowercase English alphabetic
        * characters. That translated result is returned.
        * <p>
        * The detailed COMPATIBLE algorithm:
        * <p>
        * <ol>
        *   <li> Initialize storage that can hold at least 2 unsigned bytes to 0x11. This is the
        *        accumulator for the calculated CRC.
        *   <li> Allocate storage that can hold 16 unsigned bytes.  This will be used as an
        *        intermediate output accumulator which will combine the input data and the
        *        calculated CRC data in a manner that will result in each of the 16 bytes having a
        *        value between 0 and 255 inclusive.
        *   <li> Iterate the following steps 5 times:
        *      <ol>
        *         <li>Walk forward through the array of source data provided and XOR each byte into
        *             the intermediate output accumulator (target array), but with the index of the
        *             target matching a backwards walk. Since the source array can be of any length,
        *             the lower index positions in the target may not ever get source data XOR'd.
        *             Source arrays longer than the target array cause wrapping and that means that
        *             the higher index positions in the target array will have data from multiple
        *             source positions XOR'd. See the {@link #sourceToTargetXor} method for details
        *             as well as the significant cryptographic limitations this causes.
        *         <li>Iterate 8 times, doing the following:
        *            <ol>
        *               <li> Call the {@link #crc16} method, passing in both the intermediate output
        *                    accumulator as well as the CRC accumulator.  This is the core CRC
        *                    algorithm and it will calculate the CRC of the current state of the
        *                    intermediate output accumulator while including the initial state of
        *                    the CRC accumulator.  The resulting returned CRC result is assigned
        *                    back to the CRC accumulator.  The CRC-16 algorithm used is not
        *                    cryptographically sound.  It generates a non-uniform distribution of
        *                    hashed data and collisions are not rare.
        *               <li> The least significant byte (byte 0) of the CRC accumulator will be
        *                    directly stored into an index position in the intermediate output
        *                    accumulator. This index position starts at 0 and increments by 2 for
        *                    every iteration of the nearest containing loop.  Since the intermediate
        *                    output accumulator has 16 elements, that is why the containing loop
        *                    iterates only 8 times.  This byte must be treated as an unsigned value.
        *               <li> The next to least significant byte (byte 1) of the CRC accumulator will
        *                    be directly stored into an index position in the intermediate output
        *                    accumulator. This index position starts at 1 and increments by 2 for
        *                    every iteration of the nearest containing loop. This byte must be
        *                    treated as an unsigned value.
        *            </ol>
        *      </ol>
        *   <li> At this point the intermediate output accumulator contains 16 poorly distributed
        *        hashed bytes.  Translate each byte of the intermediate output accumulator into
        *        a byte from the valid output character set.  Valid output characters include all
        *        English alphabetic letters (both uppercase and lowercase), for 52 possible output
        *        characters.  See the {@link #translateToAlpha} method for details on this process
        *        and for the severe limitations of the resulting non-uniform distribution.
        * </ol>
        * <p>
        * The result of this algorithm is NOT cryptographically sound.  It should not be used for
        * secure purposes.  The flaws in this algorithm come from the processing associated with
        * {@link #sourceToTargetXor}, {@link #crc16} and {@link #translateToAlpha}.
        *
        * @param    data
        *           The data to be encoded.  This must not be <code>null</code>.  The string may be
        *           of any length.  The data will be truncated at any contained null characters.
        *
        * @return   A string with 16 encoded bytes where each byte is one of the 52 possible output
        *           characters.  Returns <code>unknown value</code> if the input is
        *           <code>null</code> or <code>unknown value</code>.
        */
       public static character encode(character data)
       {
          if (data == null || data.isUnknown())
          {
             return new character();
          }

          return encode(data.toStringMessage());
       }

       /**
        * <code>ENCODE</code> implementation which can be run in compatible mode (the default
        * which uses {@link #_encode} as a worker) or in the incompatible mode using the J2SE
        * MD5 hashing algorithm. The choice of which version to run is made using a flag
        * that is initialized from the directory when the server starts.
        * <p>
        * <b>WARNING: when the incompatible version is used the implementation is NOT compatible
        * with the Progress 4GL algorithm in any way. Any programs relying on comparisons between
        * data encoded by the Progress 4GL ENCODE and the return value from this method will NOT
        * compare as expected.</b>
        * <p>
        * By default, the Progress 4GL compatible <code>ENCODE</code> algorithm is truly compatible
        * and it will operate on stored/migrated values from the original system as expected. The
        * algorithm takes a source byte array (of any size) and generates a 16-byte one-way hash
        * using a 4GL-proprietary approach combined with a variant of CRC-16.  The basic approach is
        * to spread out the source data in an intermediate accumulator array of 16 unsigned bytes
        * and to repeatedly CRC that data and copy the resulting 2-byte CRC into successive elements
        * of this intermediate accumulator, while building the next CRC results on the accumulated
        * CRC value and the recently modified intermediate accumulator.  This set of CRCs is
        * calculated 8 times (since that will generate 16 bytes).  This basic approach is executed 5
        * times in a row. Each byte of the resulting hashed intermediate accumulator array is then
        * translated into one of the 52 possible uppercase or lowercase English alphabetic
        * characters. That translated result is returned.
        * <p>
        * The detailed COMPATIBLE algorithm:
        * <p>
        * <ol>
        *   <li> Initialize storage that can hold at least 2 unsigned bytes to 0x11. This is the
        *        accumulator for the calculated CRC.
        *   <li> Allocate storage that can hold 16 unsigned bytes.  This will be used as an
        *        intermediate output accumulator which will combine the input data and the
        *        calculated CRC data in a manner that will result in each of the 16 bytes having a
        *        value between 0 and 255 inclusive.
        *   <li> Iterate the following steps 5 times:
        *      <ol>
        *         <li>Walk forward through the array of source data provided and XOR each byte into
        *             the intermediate output accumulator (target array), but with the index of the
        *             target matching a backwards walk. Since the source array can be of any length,
        *             the lower index positions in the target may not ever get source data XOR'd.
        *             Source arrays longer than the target array cause wrapping and that means that
        *             the higher index positions in the target array will have data from multiple
        *             source positions XOR'd. See the {@link #sourceToTargetXor} method for details
        *             as well as the significant cryptographic limitations this causes.
        *         <li>Iterate 8 times, doing the following:
        *            <ol>
        *               <li> Call the {@link #crc16} method, passing in both the intermediate output
        *                    accumulator as well as the CRC accumulator.  This is the core CRC
        *                    algorithm and it will calculate the CRC of the current state of the
        *                    intermediate output accumulator while including the initial state of
        *                    the CRC accumulator.  The resulting returned CRC result is assigned
        *                    back to the CRC accumulator.  The CRC-16 algorithm used is not
        *                    cryptographically sound.  It generates a non-uniform distribution of
        *                    hashed data and collisions are not rare.
        *               <li> The least significant byte (byte 0) of the CRC accumulator will be
        *                    directly stored into an index position in the intermediate output
        *                    accumulator. This index position starts at 0 and increments by 2 for
        *                    every iteration of the nearest containing loop.  Since the intermediate
        *                    output accumulator has 16 elements, that is why the containing loop
        *                    iterates only 8 times.  This byte must be treated as an unsigned value.
        *               <li> The next to least significant byte (byte 1) of the CRC accumulator will
        *                    be directly stored into an index position in the intermediate output
        *                    accumulator. This index position starts at 1 and increments by 2 for
        *                    every iteration of the nearest containing loop. This byte must be
        *                    treated as an unsigned value.
        *            </ol>
        *      </ol>
        *   <li> At this point the intermediate output accumulator contains 16 poorly distributed
        *        hashed bytes.  Translate each byte of the intermediate output accumulator into
        *        a byte from the valid output character set.  Valid output characters include all
        *        English alphabetic letters (both uppercase and lowercase), for 52 possible output
        *        characters.  See the {@link #translateToAlpha} method for details on this process
        *        and for the severe limitations of the resulting non-uniform distribution.
        * </ol>
        * <p>
        * The result of this algorithm is NOT cryptographically sound.  It should not be used for
        * secure purposes.  The flaws in this algorithm come from the processing associated with
        * {@link #sourceToTargetXor}, {@link #crc16} and {@link #translateToAlpha}.
        *
        * @param    data
        *           The data to be encoded.  This must not be <code>null</code>.  The string may be
        *           of any length.  The data will be truncated at any contained null characters.
        *
        * @return   A string with 16 encoded bytes where each byte is one of the 52 possible output
        *           characters.  Returns <code>unknown value</code> if the input is
        *           <code>null</code>.
        */
       public static character encode(String data)
       {
          if (data == null)
             return new character();

          return new character(compatible ? _encode(data) : _incompatible_encode(data));
       }

       /**
        * Progress 4GL compatible <code>ENCODE</code> algorithm which takes a source byte array
        * (of any size) and generates a 16-byte one-way hash using a proprietary approach
        * combined with a variant of CRC-16.  The basic approach is to spread out the source data
        * in an intermediate accumulator array of 16 unsigned bytes and to repeatedly CRC that data
        * and copy the resulting 2-byte CRC into successive elements of this intermediate
        * accumulator, while building the next CRC results on the accumulated CRC value and the
        * recently modified intermediate accumulator.  This set of CRCs is calculated 8 times
        * (since that will generate 16 bytes).  This basic approach is executed 5 times in a row.
        * Each byte of the resulting hashed intermediate accumulator array is then translated into
        * one of the 52 possible uppercase or lowercase English alphabetic characters. That
        * translated result is returned.
        * <p>
        * The detailed algorithm:
        * <p>
        * <ol>
        *   <li> Initialize storage that can hold at least 2 unsigned bytes to 0x11. This is the
        *        accumulator for the calculated CRC.
        *   <li> Allocate storage that can hold 16 unsigned bytes.  This will be used as an
        *        intermediate output accumulator which will combine the input data and the
        *        calculated CRC data in a manner that will result in each of the 16 bytes having a
        *        value between 0 and 255 inclusive.
        *   <li> Iterate the following steps 5 times:
        *      <ol>
        *         <li>Walk forward through the array of source data provided and XOR each byte into
        *             the intermediate output accumulator (target array), but with the index of the
        *             target matching a backwards walk. Since the source array can be of any length,
        *             the lower index positions in the target may not ever get source data XOR'd.
        *             Source arrays longer than the target array cause wrapping and that means that
        *             the higher index positions in the target array will have data from multiple
        *             source positions XOR'd. See the {@link #sourceToTargetXor} method for details
        *             as well as the significant cryptographic limitations this causes.
        *         <li>Iterate 8 times, doing the following:
        *            <ol>
        *               <li> Call the {@link #crc16} method, passing in both the intermediate output
        *                    accumulator as well as the CRC accumulator.  This is the core CRC
        *                    algorithm and it will calculate the CRC of the current state of the
        *                    intermediate output accumulator while including the initial state of
        *                    the CRC accumulator.  The resulting returned CRC result is assigned
        *                    back to the CRC accumulator.  The CRC-16 algorithm used is not
        *                    cryptographically sound.  It generates a non-uniform distribution of
        *                    hashed data and collisions are not rare.
        *               <li> The least significant byte (byte 0) of the CRC accumulator will be
        *                    directly stored into an index position in the intermediate output
        *                    accumulator. This index position starts at 0 and increments by 2 for
        *                    every iteration of the nearest containing loop.  Since the intermediate
        *                    output accumulator has 16 elements, that is why the containing loop
        *                    iterates only 8 times.  This byte must be treated as an unsigned value.
        *               <li> The next to least significant byte (byte 1) of the CRC accumulator will
        *                    be directly stored into an index position in the intermediate output
        *                    accumulator. This index position starts at 1 and increments by 2 for
        *                    every iteration of the nearest containing loop. This byte must be
        *                    treated as an unsigned value.
        *            </ol>
        *      </ol>
        *   <li> At this point the intermediate output accumulator contains 16 poorly distributed
        *        hashed bytes.  Translate each byte of the intermediate output accumulator into
        *        a byte from the valid output character set.  Valid output characters include all
        *        English alphabetic letters (both uppercase and lowercase), for 52 possible output
        *        characters.  See the {@link #translateToAlpha} method for details on this process
        *        and for the severe limitations of the resulting non-uniform distribution.
        * </ol>
        * <p>
        * The result of this algorithm is NOT cryptographically sound.  It should not be used for
        * secure purposes.  The flaws in this algorithm come from the processing associated with
        * {@link #sourceToTargetXor}, {@link #crc16} and {@link #translateToAlpha}.
        *
        * @param    input
        *           The data to be encoded.  This must not be <code>null</code>.  The string may be
        *           of any length.  The data will be truncated at any contained null characters.
        *
        * @return   A string with 16 encoded bytes where each byte is one of the 52 possible output
        *           characters.
        */
       public static String _encode(String input)
       {
          // the input data is truncated at the first encountered null character
          input = Text.javaTruncateNull(input);

          byte[] bin  = input.getBytes();
          int[]  data = new int[bin.length];

          // copy the input data into a array large enough to handle unsigned characters
          for (int n = 0; n < bin.length; n++)
          {
             data[n] = bin[n];
          }

          short[] storage = new short[OUTPUT_ARRAY_SIZE];

          int crc    = INITIAL_CRC_VALUE;
          int passes = CORE_LOOP_PASSES;

          while (passes != 0)
          {
             sourceToTargetXor(data, storage);

             int idx = 0;

             while (idx < storage.length)
             {
                crc = crc16(storage, crc);

                storage[idx++] = (byte)(crc & 0xFF);
                storage[idx++] = (byte)((crc >> 8) & 0xFF);
             }

             passes--;
          }

          return new String(translateToAlpha(storage));
       }

       /**
        * Converts binary data into a Base64 encoded text.
        *
        * @param    data
        *           The data to be encoded.
        *
        * @return   The encoded string.  <code>unknown</code> will be returned if the given data is
        *           <code>unknown</code> OR if the given data is initialized but the runtime has no
        *           knowledge of the buffer's size.
        */
       public static longchar base64Encode(BinaryData data)
       {
          if (data == null || data.isUnknown())
          {
             return new longchar();
          }

          if (data instanceof memptr)
          {
             return base64Encode((memptr)data);
          }

          return base64Encode(data.getByteArray());
       }

       /**
        * Converts binary data into a Base64 encoded text.
        *
        * @param    data
        *           The data to be encoded.
        *
        * @return   The encoded string.  <code>unknown</code> will be returned if the given data is
        *           <code>null</code>.
        */
       public static longchar base64Encode(byte[] data)
       {
          if (data == null)
          {
             return new longchar();
          }

          return new longchar(Base64.byteArrayToBase64(data));
       }

       /**
        * Decodes a base64 encoded string into a binary value.
        *
        * @param    data
        *           The string to be decoded.
        *
        * @return   a memptr with the decoded data.
        */
       public static memptr base64Decode(Text data)
       {
          if (data == null || data.isUnknown())
          {
             return new memptr();
          }

          return base64Decode(data.getValue());
       }

       /**
        * Converts a base64 encoded string into a binary value and returns a
        * memptr with the decoded value. Shows an error 12119 if the source
        * data do not conforms to special format (that Progress checks).
        *
        * @param    data
        *           The string to be decoded.
        *
        * @return   a memptr with the decoded data.
        */
       public static memptr base64Decode(String data)
       {
          if (data == null)
          {
             return new memptr();
          }

          if (!checkFormat(data))
          {
             ErrorManager.recordOrShowError(12119, ERR12119, false, false, false);
             return new memptr();
          }

          // Apache commons Base64.decodeBase64() is incompatible
          return new memptr(Base64.base64ToByteArray(data));
       }

       /**
        * Converts a hexadecimal encoded string into a binary value and returns a
        * raw with the decoded value.  The data must contain an even number of valid
        * hexadecimal digits.
        *
        * @param    data
        *           The text to be decoded.  <code>null</code> will be treated like
        *           unknown value.
        *
        * @return   The decoded binary data if the input is an even number of valid
        *           hexadecimal digits, otherwise it will return unknown value. The
        *           exception is for an empty string, which will return a 0-sized
        *           buffer.
        */
       public static raw hexDecode(Text data)
       {
          if (data == null || data.isUnknown())
          {
             return raw.instantiateUnknownRaw();
          }

          return hexDecode(data.toStringMessage());
       }

       /**
        * Converts a hexadecimal encoded string into a binary value and returns a
        * raw with the decoded value.  The data must contain an even number of valid
        * hexadecimal digits.
        *
        * @param    data
        *           The string to be decoded.  <code>null</code> will be treated like
        *           unknown value.
        *
        * @return   The decoded binary data if the input is an even number of valid
        *           hexadecimal digits, otherwise it will return unknown value. The
        *           exception is for an empty string, which will return a 0-sized
        *           buffer.
        */
       public static raw hexDecode(String data)
       {
          if (data == null || (data.length() % 2) != 0)
          {
             return raw.instantiateUnknownRaw();
          }

          // TODO: this was written from the Progress docs and has only been lightly
          //       tested; the boundary conditions need to be tested to confirm if this
          //       really is correct

          int    len     = data.length();
          byte[] decoded = new byte[len / 2];

          for (int i = 0; i < len; i += 2)
          {
             int next = Integer.parseInt(data.substring(i, i + 2), 16);

             decoded[i / 2] = (byte) (next & 0xFF);
          }

          return new raw(decoded);
       }

       /**
        * Converts a binary value into a hexadecimal encoded string and returns a
        * the encoded value.  The data will result an even number of valid
        * hexadecimal digits.
        *
        * @param    data
        *           The text to be encoded.  <code>null</code> will be treated like
        *           unknown value.
        *
        * @return   The hexadecimal encoded text if the input is not unknown value, or the
        *           unknown value if that was the input. An empty binary buffer will return
        *           an empty string.
        */
       public static character hexEncode(BinaryData data)
       {
          if (data == null || data.isUnknown())
          {
             return new character();
          }

          String res = hexEncodeImpl(data.getByteArray());
          return new character(res);
       }

       /**
        * Encrypts the source and returns a {@link memptr} instance containing the encrypted data.
        *
        * @param    data
        *           The data to be encrypted.
        *
        * @return   See above.
        */
       public static memptr encrypt(String data)
       {
          return encrypt(data, SecurityPolicyManager.getSymmetricEncryptionKeyInternal());
       }

       /**
        * Encrypts the source using the given key and returns a {@link memptr} instance containing the
        * encrypted data.
        *
        * @param    data
        *           The data to be encrypted.
        * @param    key
        *           The encryption key.
        *
        * @return   See above.
        */
       public static memptr encrypt(String data, raw key)
       {
          return encrypt(data, key, SecurityPolicyManager.getSymmetricEncryptionIV());
       }

       /**
        * Encrypts the source using the given key and initialization vector, and returns a
        * {@link memptr} instance containing the encrypted data.
        *
        * @param    data
        *           The data to be encrypted.
        * @param    key
        *           The encryption key.
        * @param    ivValue
        *           An initialization vector to be used with the given key.
        *
        * @return   See above.
        */
       public static memptr encrypt(String data, raw key, raw ivValue)
       {
          return encrypt(data, key, ivValue, SecurityPolicyManager.getSymmetricEncryptionAlgorithm());
       }

       /**
        * Encrypts the source using the given key, initialization vector and symmetric encryption
        * algorithm, and returns a {@link memptr} instance containing the encrypted data.
        *
        * @param    data
        *           The data to be encrypted.
        * @param    key
        *           The encryption key.
        * @param    ivValue
        *           An initialization vector to be used with the given key.
        * @param    algorithm
        *           The encryption algorithm's name.
        *
        * @return   See above.
        */
       public static memptr encrypt(String data,
                                    raw    key,
                                    raw    ivValue,
                                    String algorithm)
       {
          return encrypt(data,
                         key,
                         ivValue,
                         new character(algorithm));
       }

       /**
        * Encrypts the source using the given key, initialization vector and symmetric encryption
        * algorithm, and returns a {@link memptr} instance containing the encrypted data.
        *
        * @param    data
        *           The data to be encrypted.
        * @param    key
        *           The encryption key.
        * @param    ivValue
        *           An initialization vector to be used with the given key.
        * @param    algorithm
        *           The encryption algorithm's name.
        *
        * @return   See above.
        */
       public static memptr encrypt(String    data,
                                    raw       key,
                                    raw       ivValue,
                                    character algorithm)
       {
          return encrypt(new character(data), key, ivValue, algorithm);
       }

       /**
        * Encrypts the source and returns a {@link memptr} instance containing the encrypted data.
        *
        * @param    data
        *           The data to be encrypted.
        *
        * @return   See above.
        */
       public static memptr encrypt(Text data)
       {
          return encrypt(data, SecurityPolicyManager.getSymmetricEncryptionKeyInternal());
       }

       /**
        * Encrypts the source using the given key and returns a {@link memptr} instance containing the
        * encrypted data.
        *
        * @param    data
        *           The data to be encrypted.
        * @param    key
        *           The encryption key.
        *
        * @return   See above.
        */
       public static memptr encrypt(Text data, raw key)
       {
          return encrypt(data, key, SecurityPolicyManager.getSymmetricEncryptionIV());
       }

       /**
        * Encrypts the source using the given key and initialization vector, and returns a
        * {@link memptr} instance containing the encrypted data.
        *
        * @param    data
        *           The data to be encrypted.
        * @param    key
        *           The encryption key.
        * @param    ivValue
        *           An initialization vector to be used with the given key.
        *
        * @return   See above.
        */
       public static memptr encrypt(Text data, raw key, raw ivValue)
       {
          return encrypt(data, key, ivValue, SecurityPolicyManager.getSymmetricEncryptionAlgorithm());
       }

       /**
        * Encrypts the source using the given key, initialization vector and symmetric encryption
        * algorithm, and returns a {@link memptr} instance containing the encrypted data.
        *
        * @param    data
        *           The data to be encrypted.
        * @param    key
        *           The encryption key.
        * @param    ivValue
        *           An initialization vector to be used with the given key.
        * @param    algorithm
        *           The encryption algorithm's name.
        *
        * @return   See above.
        */
       public static memptr encrypt(Text data,
                                    raw    key,
                                    raw    ivValue,
                                    String algorithm)
       {
          return encrypt(data,
                         key,
                         ivValue,
                         new character(algorithm));
       }

       /**
        * Encrypts the source using the given key, initialization vector and symmetric encryption
        * algorithm, and returns a {@link memptr} instance containing the encrypted data.
        *
        * @param    data
        *           The data to be encrypted.
        * @param    key
        *           The encryption key.
        * @param    ivValue
        *           An initialization vector to be used with the given key.
        * @param    algorithm
        *           The encryption algorithm's name.
        *
        * @return   See above.
        */
       public static memptr encrypt(Text      data,
                                    raw       key,
                                    raw       ivValue,
                                    character algorithm)
       {
          if (data.isUnknown() || data.getValue().isEmpty())
          {
             // can't encrypt an unknown or zero-length data
             return new memptr();
          }

          raw r = new raw();
          String value = data.getValue();
          r.setString(value, 1, value == null ? 0 : value.length());

          return encrypt(r, key, ivValue, algorithm);
       }

       /**

        *
        * @param    data
        *           The data to be encrypted.
        *
        * @return   See above.
        */
       public static memptr encrypt(BinaryData data)
       {
          byte[] bdata = encryptDecryptWorker(Cipher.ENCRYPT_MODE, data,
                SecurityPolicyManager.getSymmetricEncryptionKeyInternal(),
                SecurityPolicyManager.getSymmetricEncryptionIV(),
                SecurityPolicyManager.getSymmetricEncryptionAlgorithm()
          );

          return new memptr(bdata);
       }

       /**
        * Encrypts the source using the given key, initialization vector and symmetric encryption
        * algorithm, and returns a {@link memptr} instance containing the encrypted data.
        *
        * @param    data
        *           The data to be encrypted.
        * @param    key
        *           The encryption key.
        *
        * @return   See above.
        */
       public static memptr encrypt(BinaryData data, raw key)
       {
          byte[] bdata = encryptDecryptWorker(Cipher.ENCRYPT_MODE, data, key,
                SecurityPolicyManager.getSymmetricEncryptionIV(),
                SecurityPolicyManager.getSymmetricEncryptionAlgorithm()
          );

          return new memptr(bdata);
       }

       /**
        * Encrypts the source using the given key, initialization vector and symmetric encryption
        * algorithm, and returns a {@link memptr} instance containing the encrypted data.
        *
        * @param    data
        *           The data to be encrypted.
        * @param    key
        *           The encryption key.
        * @param    ivValue
        *           An initialization vector to be used with the given key.
        *
        * @return   See above.
        */
       public static memptr encrypt(BinaryData data,
                                    raw        key,
                                    raw        ivValue)
       {
          byte[] bdata = encryptDecryptWorker(Cipher.ENCRYPT_MODE, data, key, ivValue,
                SecurityPolicyManager.getSymmetricEncryptionAlgorithm()
          );

          return new memptr(bdata);
       }

       /**
        * Encrypts the source using the given key, initialization vector and symmetric encryption
        * algorithm, and returns a {@link memptr} instance containing the encrypted data.
        *
        * @param    data
        *           The data to be encrypted.
        * @param    key
        *           The encryption key.
        * @param    ivValue
        *           An initialization vector to be used with the given key.
        * @param    algorithm
        *           The encryption algorithm's name.
        *
        * @return   See above.
        */
       public static memptr encrypt(BinaryData data,
                                    raw        key,
                                    raw        ivValue,
                                    character  algorithm)
       {
          byte[] bdata = encryptDecryptWorker(Cipher.ENCRYPT_MODE, data,
                   key == null || key.isUnknown() ?
                         SecurityPolicyManager.getSymmetricEncryptionKeyInternal() : key,
                   ivValue == null || ivValue.isUnknown() ?
                         SecurityPolicyManager.getSymmetricEncryptionIV() : ivValue,
                   algorithm == null || algorithm.isUnknown() ?
                         SecurityPolicyManager.getSymmetricEncryptionAlgorithm() : algorithm
                );

          return new memptr(bdata);
       }

       /**
        * Decrypts the source and returns a {@link memptr} instance containing the decrypted data.
        *
        * @param    data
        *           The data to be decrypted.
        *
        * @return   See above.
        */
       public static memptr decrypt(BinaryData data)
       {
          return decrypt(data,
                SecurityPolicyManager.getSymmetricEncryptionKeyInternal(),
                SecurityPolicyManager.getSymmetricEncryptionIV(),
                SecurityPolicyManager.getSymmetricEncryptionAlgorithm()
          );
       }

       /**
        * Decrypts the source using the given key and returns a {@link memptr} instance containing the
        * decrypted data.
        *
        * @param    data
        *           The data to be decrypted.
        * @param    key
        *           The decryption key.
        *
        * @return   See above.
        */
       public static memptr decrypt(BinaryData data, raw key)
       {
          return decrypt(data, key,
                SecurityPolicyManager.getSymmetricEncryptionIV(),
                SecurityPolicyManager.getSymmetricEncryptionAlgorithm()
          );
       }

       /**
        * Decrypts the source using the given key and initialization vector, and returns a
        * {@link memptr} instance containing the decrypted data.
        *
        * @param    data
        *           The data to be decrypted.
        * @param    key
        *           The encryption key.
        * @param    ivValue
        *           An initialization vector to be used with the given key.
        *
        * @return   See above.
        */
       public static memptr decrypt(BinaryData data, raw key, raw ivValue)
       {
          return decrypt(data, key, ivValue, SecurityPolicyManager.getSymmetricEncryptionAlgorithm());
       }

       /**
        * Decrypts the source using the given key, initialization vector and symmetric encryption
        * algorithm, and returns a {@link memptr} instance containing the decrypted data.
        *
        * @param    data
        *           The data to be decrypted.
        * @param    key
        *           The encryption key.
        * @param    ivValue
        *           An initialization vector to be used with the given key.
        * @param    algorithm
        *           The encryption algorithm's name.
        *
        * @return   See above.
        */
       public static memptr decrypt(BinaryData data,
                                    raw        key,
                                    raw        ivValue,
                                    String     algorithm)
       {
          return decrypt(data, key, ivValue, new character(algorithm));
       }

       /**
        * Decrypts the source using the given key, initialization vector and symmetric encryption
        * algorithm, and returns a {@link memptr} instance containing the decrypted data.
        *
        * @param    data
        *           The data to be decrypted.
        * @param    key
        *           The encryption key.
        * @param    ivValue
        *           An initialization vector to be used with the given key.
        * @param    algorithm
        *           The encryption algorithm's name.
        *
        * @return   See above.
        */
       public static memptr decrypt(BinaryData data,
                                    raw        key,
                                    raw        ivValue,
                                    character  algorithm)
       {
          byte[] bdata = encryptDecryptWorker(Cipher.DECRYPT_MODE, data,
                   key == null || key.isUnknown() ?
                         SecurityPolicyManager.getSymmetricEncryptionKeyInternal() : key,
                   ivValue == null || ivValue.isUnknown() ?
                         SecurityPolicyManager.getSymmetricEncryptionIV() : ivValue,
                   algorithm == null || algorithm.isUnknown() ?
                         SecurityPolicyManager.getSymmetricEncryptionAlgorithm() : algorithm
                );

          return new memptr(bdata);
       }

       /**
        * Generates a random byte-array of the length required by the algorithm specified via
        * SECURITY-POLICY:SYMMETRIC-ENCRYPTION-ALGORITHM.  If this is not specified, then the default
        * AES_CBC_128 algorithm is used, and 16-byte key is generated.
        *
        * @return   The random key.
        *
        */
       public static raw generateRandomKey()
       {
          // generates random byte-array, with its length dependent on the algorithm in use
          // specified via SECURITY-POLICY:SYMMETRIC-ENCRYPTION-ALGORITHM or the default, AES_CBC_128

          // the algorithm is split in "<alg-name>_<alg-mode>_<key-length>"

          int byteLen = SecurityPolicyManager.getSymmetricCiperParams().getKeySize();
          raw r = new raw();
          byte[] bytes = new byte[byteLen];
          RAND.nextBytes(bytes);
          r.setLength(byteLen);
          for (int i = 0; i < bytes.length; i++)
          {
             r.setByte(bytes[i], i + 1);
          }

          return r;
       }

       /**
        * Generates a password-based encryption key, based on the PKCS#5/RFC 2898
        * standard, and returns the key as a {@link raw} instance.
        *
        * @param    password
        *           The password to use in generating the encryption key.
        *
        * @return   See above.
        */
       public static raw generatePBEKey(String password)
       {
          return generatePBEKey(new character(password), null);
       }

       /**
        * Generates a password-based encryption key, based on the PKCS#5/RFC 2898
        * standard, and returns the key as a {@link raw} instance.
        *
        * @param    password
        *           The password to use in generating the encryption key.
        * @param    salt
        *           a random series of 8 bytes to use as salt value in generating
        *           the encryption key.
        *
        * @return   See above.
        */
       public static raw generatePBEKey(String password, raw salt)
       {
          return generatePBEKey(new character(password), salt);
       }

       /**
        * Generates a password-based encryption key, based on the PKCS#5/RFC 2898
        * standard, and returns the key as a {@link raw} instance.
        *
        * @param    password
        *           The password to use in generating the encryption key.
        *
        * @return   See above.
        */
       public static raw generatePBEKey(Text password)
       {
          return generatePBEKey(password, null);
       }

       /**
        * Generates a password-based encryption key, based on the PKCS#5/RFC 2898
        * standard, and returns the key as a {@link raw} instance.
        *
        * @param    password
        *           The password to use in generating the encryption key.
        * @param    salt
        *           a random series of 8 bytes to use as salt value in generating
        *           the encryption key.
        *
        * @return   See above.
        */
       public static raw generatePBEKey(Text password, raw salt)
       {
          if (password == null || password.isUnknown())
          {
             return raw.instantiateUnknownRaw();
          }
          return generatePBEKey(password.getValue().getBytes(StandardCharsets.UTF_8), salt);
       }

       /**
        * Generates a password-based encryption key, based on the PKCS#5/RFC 2898
        * standard, and returns the key as a {@link raw} instance.
        *
        * @param    password
        *           The password to use in generating the encryption key.
        *
        * @return   See above.
        */
       public static raw generatePBEKey(BinaryData password)
       {
          return generatePBEKey(password, SecurityPolicyManager.getEncryptionSalt());
       }

       /**
        * Generates a password-based encryption key, based on the PKCS#5/RFC 2898
        * standard, and returns the key as a {@link raw} instance.
        *
        * @param    password
        *           The password to use in generating the encryption key.
        * @param    salt
        *           a random series of 8 bytes to use as salt value in generating
        *           the encryption key.
        *
        * @return   See above.
        */
       public static raw generatePBEKey(BinaryData password, raw salt)
       {
          if (password == null || password.isUnknown())
          {
             return raw.instantiateUnknownRaw();
          }
          return generatePBEKey(password.getByteArray(), salt);
       }

       /**
        * Generates a password-based encryption key, based on the PKCS#5/RFC 2898
        * standard, and returns the key as a {@link raw} instance.
        *
        * @param    password
        *           The password to use in generating the encryption key.
        * @param    salt
        *           a random series of 8 bytes to use as salt value in generating
        *           the encryption key.
        *
        * @return   See above.
        */
       public static raw generatePBEKey(byte[] password, raw salt)
       {
          if (salt == null || salt.isUnknown())
          {
             salt = SecurityPolicyManager.getEncryptionSalt();
          }
          try
          {
             byte[] pbe = CryptoUtils.generatePbeKey(
                   password,
                   salt.getByteArray(),
                   SecurityPolicyManager.getPbeHashAlgorithm().toStringMessage(),
                   SecurityPolicyManager.getPbeKeyRounds().intValue()
             );
             return new raw(pbe);
          }
          catch (Exception e)
          {
             ErrorManager.recordOrThrowError(-1, "FWD: generatePbeKey failed " +
                   e.getMessage(), true);
             return null;
          }
       }
       /**
        * Generates a random salt value (a series of 8 bytes) to use in generating
        * an encryption key, and returns the salt value as a {@link raw} instance.
        *
        * @return   See above.
        */
       public static raw generatePBESalt()
       {
          byte[] salt = new byte[8];
          RND.nextBytes(salt);
          return new raw(salt);
       }

       /**
        * Generate a random 16-byte UUID.
        *
        * @return  UUID.
        */
       public static raw generateUUID()
       {
          // TODO: this is just a basic implementation, there may be compatibility issues
          //       especially if the 4GL uses a UUID version that is different (not the
          //       "random" version 4 that we use here) OR if the cryptographic support
          //       in the 4GL appreciable differs from Java OR if there is some kind of
          //       other edge conditions/error handling that is not matched

          raw result = new raw();

          // we use a "type 4" random UUID here, which seems different from the 4GL version
          // it will probably work for virtually any use case except a scenario that hard
          // codes to some idiosyncracy of the 4GL output
          UUID uuid = UUID.randomUUID();

          result.setInt64(uuid.getMostSignificantBits(), 1);
          result.setInt64(uuid.getLeastSignificantBits(), 9);

          return result;
       }

       /**
        * Generate a new 16-byte UUID (universally unique id) and return it as a text based
        * hexadecimal representation that matches a GUID (globally unique id) hyphen separated
        * format.
        *
        * @return   The GUID formatted UUID as text.
        */
       public static character convertUniversalUIDToGlobalUID()
       {
          return convertUniversalUIDToGlobalUID(generateUUID());
       }

       /**
        * Convert a 16-byte UUID (universally unique id) into a text based hexadecimal representation
        * that matches a GUID (globally unique id) hyphen separated format.
        *
        * @param    uuid
        *           The 16-byte data to convert.
        *
        * @return   The GUID formatted UUID as text.
        */
       public static character convertUniversalUIDToGlobalUID(raw uuid)
       {
          // TODO: this is just a basic implementation based on simple tests and the 4GL
          //       docs; it is possibly incompatible at least in regard to error handling

          if (uuid == null || uuid.isUnknown())
          {
             return new character();
          }

          if (uuid.lengthOf() != 16)
          {
             ErrorManager.recordOrThrowError(12708,
                                             "Wrong length UUID detected in GUID function",
                                             false);
             return new character();
          }

          String hex = hexEncodeImpl(uuid.getByteArray());

          // split into the canonical 8-4-4-4-12 format
          String result = hex.substring(0, 8)   +
                          "-"                   +
                          hex.substring(8, 12)  +
                          "-"                   +
                          hex.substring(12, 16) +
                          "-"                   +
                          hex.substring(16, 20) +
                          "-"                   +
                          hex.substring(20);

          return new character(result);
       }

       /**
        * Hashes the specified data using the SHA-1 algorithm, and returns a
        * 20-byte binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        *
        * @return   The hash value.
        */
       public static raw sha1Digest(String data)
       {
          return messageDigest("SHA-1", data, (String) null);
       }

       /**
        * Hashes the specified data using the SHA-1 algorithm, and returns a
        * 20-byte binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw sha1Digest(String data, String key)
       {
          return messageDigest("SHA-1", data, new character(key));
       }

       /**
        * Hashes the specified data using the SHA-1 algorithm, and returns a
        * 20-byte binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw sha1Digest(String data, Text key)
       {
          return messageDigest("SHA-1", new character(data), key);
       }

       /**
        * Hashes the specified data using the SHA-1 algorithm, and returns a
        * 20-byte binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw sha1Digest(String data, BinaryData key)
       {
          return messageDigest("SHA-1", new character(data), key);
       }

       /**
        * Hashes the specified data using the SHA-1 algorithm, and returns a
        * 20-byte binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        *
        * @return   The hash value.
        */
       public static raw sha1Digest(Text data)
       {
          return messageDigest("SHA-1", data, (String) null);
       }

       /**
        * Hashes the specified data using the SHA-1 algorithm, and returns a
        * 20-byte binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw sha1Digest(Text data, String key)
       {
          return messageDigest("SHA-1", data, new character(key));
       }

       /**
        * Hashes the specified data using the SHA-1 algorithm, and returns a
        * 20-byte binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw sha1Digest(Text data, Text key)
       {
          return messageDigest("SHA-1", data, key);
       }

       /**
        * Hashes the specified data using the SHA-1 algorithm, and returns a
        * 20-byte binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw sha1Digest(Text data, BinaryData key)
       {
          return messageDigest("SHA-1", data, key);
       }

       /**
        * Hashes the specified data using the SHA-1 algorithm, and returns a
        * 20-byte binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        *
        * @return   The hash value.
        */
       public static raw sha1Digest(BinaryData data)
       {
          return messageDigest("SHA-1", data, (String) null);
       }

       /**
        * Hashes the specified data using the SHA-1 algorithm, and returns a
        * 20-byte binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw sha1Digest(BinaryData data, String key)
       {
          return messageDigest("SHA-1", data, new character(key));
       }

       /**
        * Hashes the specified data using the SHA-1 algorithm, and returns a
        * 20-byte binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw sha1Digest(BinaryData data, Text key)
       {
          return messageDigest("SHA-1", data, key);
       }

       /**
        * Hashes the specified data using the SHA-1 algorithm, and returns a
        * 20-byte binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw sha1Digest(BinaryData data, BinaryData key)
       {
          return messageDigest("SHA-1", data, key);
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(String data)
       {
          return messageDigest("MD5", data, (String) null);
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(String data, String key)
       {
          return messageDigest("MD5", data, new character(key));
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(String data, Text key)
       {
          return messageDigest("MD5", new character(data), key);
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(String data, BinaryData key)
       {
          return messageDigest("MD5", new character(data), key);
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(String data, BaseDataType key)
       {
          // the value may be a proxy; make sure to unwrap and work with the real value
          key = key == null ? null : key.val();
          if (key instanceof Text)
          {
             return messageDigest("MD5", data, (Text) key);
          }
          else if (key instanceof BinaryData)
          {
             return messageDigest("MD5", data, (BinaryData) key);
          }
          else
          {
             ErrorManager.recordOrThrowError(
                      new NumberedException("Unacceptable datatype for MD5-DIGEST argument", 12117));
             return null;
          }
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(Text data)
       {
          return messageDigest("MD5", data, (String) null);
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(Text data, String key)
       {
          return messageDigest("MD5", data, new character(key));
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(Text data, Text key)
       {
          return messageDigest("MD5", data, key);
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(Text data, BinaryData key)
       {
          return messageDigest("MD5", data, key);
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(Text data, BaseDataType key)
       {
          // the value may be a proxy; make sure to unwrap and work with the real value
          key = key == null ? null : key.val();
          if (key instanceof Text)
          {
             return messageDigest("MD5", data, (Text) key);
          }
          else if (key instanceof BinaryData)
          {
             return messageDigest("MD5", data, (BinaryData) key);
          }
          else
          {
             ErrorManager.recordOrThrowError(
                      new NumberedException("Unacceptable datatype for MD5-DIGEST argument", 12117));
             return null;
          }
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(BinaryData data)
       {
          return messageDigest("MD5", data, (String) null);
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(BinaryData data, String key)
       {
          return messageDigest("MD5", data, new character(key));
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(BinaryData data, Text key)
       {
          return messageDigest("MD5", data, key);
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(BinaryData data, BinaryData key)
       {
          return messageDigest("MD5", data, key);
       }

       /**
        * Hashes the specified data using RSA MD5 algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw md5Digest(BinaryData data, BaseDataType key)
       {
          // the value may be a proxy; make sure to unwrap and work with the real value
          key = key == null ? null : key.val();
          if (key instanceof Text)
          {
             return messageDigest("MD5", data, (Text) key);
          }
          else if (key instanceof BinaryData)
          {
             return messageDigest("MD5", data, (BinaryData) key);
          }
          else
          {
             ErrorManager.recordOrThrowError(
                      new NumberedException("Unacceptable datatype for MD5-DIGEST argument", 12117));
             return null;
          }
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(String algo, String data)
       {
          return messageDigest(new character(algo), data, (String) null);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(String algo, String data, String key)
       {
          return messageDigest(new character(algo), data, new character(key));
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(String algo, String data, Text key)
       {
          return messageDigest(new character(algo), new character(data), key);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(String algo, String data, BinaryData key)
       {
          return messageDigest(new character(algo), new character(data), key);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(String algo, Text data)
       {
          return messageDigest(new character(algo), data, (String) null);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(String algo, Text data, String key)
       {
          return messageDigest(new character(algo), data, new character(key));
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(String algo, Text data, Text key)
       {
          return messageDigest(new character(algo), data, key);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(String algo, Text data, BinaryData key)
       {
          return messageDigest(new character(algo), data, key);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(String algo, BinaryData data)
       {
          return messageDigest(new character(algo), data, (String) null);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(String algo, BinaryData data, String key)
       {
          return messageDigest(new character(algo), data, new character(key));
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(String algo, BinaryData data, Text key)
       {
          return messageDigest(new character(algo), data, key);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(String algo, BinaryData data, BinaryData key)
       {
          return messageDigest(new character(algo), data, key);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(character algo, String data)
       {
          return messageDigest(algo, data, (String) null);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(character algo, String data, String key)
       {
          return messageDigest(algo, data, new character(key));
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(character algo, String data, Text key)
       {
          return messageDigest(algo, new character(data), key);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(character algo, String data, BinaryData key)
       {
          return messageDigest(algo, new character(data), key);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(character algo, Text data)
       {
          return messageDigest(algo, data, (String) null);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(character algo, Text data, String key)
       {
          return messageDigest(algo, data, new character(key));
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(character algo, Text data, Text key)
       {
          // TODO: this implementation is "basic"; this means it does match 4GL outputs for simple
          //       tests but it has not been thoroughly tested for compatibility, especially in
          //       regard to errors and boundary conditions

          if (algo == null || algo.isUnknown() || data == null || data.isUnknown())
          {
             raw r = new raw();
             r.setUnknown();
             return r;
          }

          return messageDigest(algorithm(algo), textToBytes(data), textToBytes(key));
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(character algo, Text data, BinaryData key)
       {
          // TODO: this implementation is "basic"; this means it does match 4GL outputs for simple
          //       tests but it has not been thoroughly tested for compatibility, especially in
          //       regard to errors and boundary conditions

          if (algo == null || algo.isUnknown() || data == null || data.isUnknown())
          {
             raw r = new raw();
             r.setUnknown();
             return r;
          }

          return messageDigest(algorithm(algo), textToBytes(data), binaryToBytes(key));
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(character algo, BinaryData data)
       {
          return messageDigest(algo, data, (String) null);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(character algo, BinaryData data, String key)
       {
          return messageDigest(algo, data, new character(key));
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(character algo, BinaryData data, Text key)
       {
          // TODO: this implementation is "basic"; this means it does match 4GL outputs for simple
          //       tests but it has not been thoroughly tested for compatibility, especially in
          //       regard to errors and boundary conditions

          if (algo == null || algo.isUnknown() || data == null || data.isUnknown())
          {
             raw r = new raw();
             r.setUnknown();
             return r;
          }

          return messageDigest(algorithm(algo), binaryToBytes(data), textToBytes(key));
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    data
        *           The data to be hashed.
        * @param    key
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value.
        */
       public static raw messageDigest(character algo, BinaryData data, BinaryData key)
       {
          // TODO: this implementation is "basic"; this means it does match 4GL outputs for simple
          //       tests but it has not been thoroughly tested for compatibility, especially in
          //       regard to errors and boundary conditions

          if (algo == null || algo.isUnknown() || data == null || data.isUnknown())
          {
             raw r = new raw();
             r.setUnknown();
             return r;
          }

          return messageDigest(algorithm(algo), binaryToBytes(data), binaryToBytes(key));
       }

       /**
        * Converts a binary value into a hexadecimal encoded string and returns the encoded value.
        * The data will result an even number of valid hexadecimal digits.
        *
        * @param    input
        *           The text to be encoded.
        *
        * @return   The hexadecimal encoded text if the input is not unknown value, or the
        *           unknown value if that was the input. An empty binary buffer will return
        *           an empty string.
        */
       private static String hexEncodeImpl(byte[] input)
       {
          // TODO: this was written from the Progress docs and has not been tested at
          //       all; also, the boundary conditions need to be tested to confirm if this
          //       really is correct

          char[] res = new char[input.length * 2];

          for (int i = 0; i < input.length; i++)
          {
             String hex = Integer.toHexString(input[i] & 0xFF);
             boolean oneDigit = hex.length() == 1;
             res[i * 2] =  oneDigit ? '0' : hex.charAt(0);
             res[i * 2 + 1] = hex.charAt(oneDigit ? 0 : 1);
          }

          return new String(res);
       }

       /**
        * Hashes the specified data using the given algorithm, and returns a 16-byte
        * binary message digest value in a {@link raw} instance.
        *
        * @param    algo
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".
        * @param    input
        *           The data to be hashed.
        * @param    salt
        *           An optional key value to use in the hash operation.
        *
        * @return   The hash value as a raw.  It may be the unknown value if there was an error.
        */
       private static raw messageDigest(String algo, byte[] input, byte[] salt)
       {
          byte[] hash = null;

          try
          {
             MessageDigest digest = MessageDigest.getInstance(algo);

             // TODO: do we need to check if the salt is 0 length?
             if (salt == null)
             {
                // no salt path
                hash = digest.digest(input);
             }
             else
             {
                // salt path
                digest.update(input);
                hash = digest.digest(salt);
             }
          }
          catch (Exception ex)
          {
             // TODO: improve this to match 4GL errors
             ErrorManager.recordOrThrowError(new NumberedException("Failure in hashing.", -1, ex));
          }

          return new raw(hash);
       }

       /**
        * Validate the message digest agorithm name and return it if it is valid.
        *
        * @param    algoName
        *           The hash algorithm to be used. This should be "MD5", "SHA-1", "SHA-256" or
        *           "SHA-512".  <code>null</code> or unknown value should not be passed. The
        *           case does not matter.
        *
        * @return   The algorithm name as a string or <code>null</code> on any error.
        */
       private static String algorithm(character algoName)
       {
          String algo = algoName.toStringMessage();

          // TODO: are there any undocumented algorithm names that should be supported?
          if (!algo.equalsIgnoreCase("MD5")     &&
              !algo.equalsIgnoreCase("SHA-1")   &&
              !algo.equalsIgnoreCase("SHA-256") &&
              !algo.equalsIgnoreCase("SHA-512"))
          {
             // TODO: what error handling does the 4GL do here?
             ErrorManager.recordOrThrowError(-1, "Invalid message digest algorithm name.");
             return null;
          }

          return algo;
       }

       /**
        * Convert the given text data into a byte array that can be used for hashing. The data
        * will be converted to UTF-8 as part of the process.
        *
        * @param    data
        *           The input data to convert.
        *
        * @return   The byte array or <code>null</code> if there is no data.
        */
       private static byte[] textToBytes(Text data)
       {
          // TODO: does the 4GL have error handling for data that is 0 length or which is very large?
          if (data == null || data.isUnknown())
          {
             return null;
          }

          String input = data.toStringMessage();

          return input.getBytes(StandardCharsets.UTF_8);
       }

       /**
        * Convert the given binary data into a byte array that can be used for hashing.
        *
        * @param    data
        *           The input data to convert.
        *
        * @return   The byte array or <code>null</code> if there is no data.
        */
       private static byte[] binaryToBytes(BinaryData data)
       {
          // TODO: does the 4GL have error handling for data that is 0 length or which is very large?
          if (data == null || data.isUnknown())
          {
             return null;
          }

          return data.getByteArray();
       }

       /**
        * Converts memptr into a Base64 encoded text. Also checks memptr bounds.
        * <p>
        * There were reports that the error 12118 was raised (<i>Unable to allocate memory for result
        * from  function</i>) but at this moment a recreate is unavailable. It is possible there is
        * a scenario in which memory is somehow constrained on the runtime system and this error
        * would be generated.  Since there is no known recreate at this time, the corresponding
        * error processing is not implemented.
        *
        * @param   data
        *          The data to be encoded.
        *
        * @return  The encoded string. {@code unknown} will be returned if the given data is
        *          {@code null} or {@code unknown}
        */
       private static longchar base64Encode(memptr data)
       {
          if (data == null || data.isUnknown() || data.isUninitialized())
          {
             return new longchar();
          }

          return base64Encode(data.getByteArray());
       }

       /**
        * Checks if the input string format conforms to Progress rules:
        * <ol>
        *   <li>All trailing PAD symbols are ignored except the last one.</li>
        *   <li>All characters must be from the base64 alphabet.
        *   See {@link #isBase64(char)}.
        *   </li>
        *   <li>Lets name l - length of the source string. If l % 4 == 0 than we
        *   only check the rule 2. The last char can be the PAD.
        *   </li>
        *   <li>Lets name r = l % 4. If r != 0 than we get the last r chars to
        *   check. The r must be only 3. The last char must be the PAD. The first
        *   and second chars of the remainder part must follow rule 2. At the end
        *   if remainder is good check the main part (of the length aliquot to 4)
        *   by the rule 2.
        *   </li>
        * </ol>
        * @param   data
        *          The string to be decoded by Base64
        *
        * @return  true if data follows Progress rules, false - doesn't follow.
        */
       private static boolean checkFormat(String data)
       {
          char[] chars = data.toCharArray();

          // ignore trailing PAD symbols except only the last one
          int len = chars.length;
          for (; len - 1 > 0; len--)
          {
             if (chars[len - 1] != PAD || chars[len - 2] != PAD)
             {
                break;
             }
          }

          int remainder = len % 4;

          // Check remainder chars first. It's faster.
          if (remainder != 0)
          {
             // common chars num in remainder must be 3 and the last is '='
             if (remainder != 3 || chars[len - 1] != PAD)
             {
                return false;
             }

             // first two symbols must be from the base64 alphabet
             if (!isBase64(chars[len - 2]) || !isBase64(chars[len - 3]))
             {
                return false;
             }
          }

          // Check all chars up to last char with index aliquot to 4
          for (int i = 0; i < len - remainder; i++)
          {
             if (!isBase64(chars[i]))
             {
                boolean lastChar = (remainder == 0 && i == len - remainder - 1);
                if (!lastChar || chars[i] != PAD)
                {
                   return false;
                }
             }
          }

          // passed all checks
          return true;
       }

       /**
        * Checks if the symbol from base64 alphabet:
        * "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
        *
        * @param   c
        *          input character to check
        *
        * @return  true if the c is in alphabet, false - otherwise
        */
       private static boolean isBase64(char c)
       {
          return (c <= 'Z' && c >= 'A') ||
                 (c <= 'z' && c >= 'a') ||
                 (c <= '9' && c >= '0') ||
                 (c == '+' || c == '/');
       }

       /**
        * Hashes an arbitrary input string into a 16-byte output string.  This conversion is one-way
        * (the original data can not be recovered) and the output is always a string of 16-bytes no
        * matter the length of the input string.
        * <p>
        * <b>WARNING: this implementation is NOT compatible with the Progress 4GL algorithm in any
        * way. Any programs relying on comparisons between data encoded by the Progress 4GL ENCODE
        * and the return value from this method will NOT compare as expected.</b>
        * <p>
        * The 16-bytes of output will only include characters in the range <code>A-Z</code> and
        * <code>a-z</code>.
        * <p>
        * This is only here because there is at least 1 customer that has installations that have
        * stored values that were encoded with the incompatible version.  Remove this at such time
        * as it is no longer in use.
        *
        * @param    data
        *           The data to be hashed. Must not be <code>null</code>.
        *
        * @return   The hashed 16-byte string.
        */
       private static String _incompatible_encode(String data)
       {
          StringBuilder sb = new StringBuilder();
          byte[] hashed = null;

          try
          {
             MessageDigest md = MessageDigest.getInstance("MD5");
             hashed = md.digest(data.getBytes());

             for (int i = 0; i < hashed.length; i++)
             {
                // this is not a fully even distribution but it is close
                // enough for an interrim solution
                int base52 = ((0x000000FF & hashed[i]) % 52);
                sb.append(ALPHAS.charAt(base52));
             }
          }
          catch (NoSuchAlgorithmException nsa)
          {
             // we will fail below
          }

          if (hashed == null || hashed.length != 16)
          {
             throw new RuntimeException("Unexpected hashing result for input '" +
                                        data + "' resulting in output '" + Arrays.toString(hashed) + "'.");
          }

          return sb.toString();
       }

       /**
        * Walk forward through the array of source data provided and XOR each byte into the target
        * array, but with the index of the target matching a backwards walk.  The source data can
        * be an array of any length and the target length is not expected to match.  The target
        * indexes are calculated modulo the length of the target array, which means that for a source
        * array longer than the target array, one or more target array elements will store data XOR'd
        * from more than one source source array element (i.e. the XOR processing will wrap around).
        * For a source array smaller than the target array, there will be some low index elements
        * of the target array which will not receive any XOR'd data.  Only in the case where the
        * two arrays are the same length will there be no wrapping and no unmerged elements in the
        * target array.  Only in the case where the source array is modulo the size of the target
        * array will all elements of the target array be modified.
        * <p>
        * From a cryptographic perspective, the algorithm is quite poor.  Source arrays smaller
        * than the target arrays will leave target array bytes unmodified.  Source arrays larger
        * than the target array size will have some (or all) elements modified multiple times.
        * Both cases are causes of concern.  Not modifying elements means that there is less
        * input provided for distribution of results.  Modifying elements more than once can
        * cause issues as well.  Consider the case where the same character appears in the source
        * array in the first byte (index 0) and in the byte modulo the size of the target.  Because
        * of wrapping, this character will be XOR'd twice into the same element of the target
        * array.  XOR is its own inverse.  If you XOR the same data into a byte an even number of
        * times, then the resulting byte will be unchanged. This will have the effect of reducing
        * the distribution of the resulting data for some inputs. This is not suitable for secure
        * purposes.
        *
        * @param    source
        *           Source array to XOR from. Must not be <code>null</code>. May be of any length.
        * @param    target
        *           Target array to XOR into. Must not be <code>null</code>. May be of any length.
        */
       private static void sourceToTargetXor(int[] source, short[] target)
       {
          int len = target.length;
          int max = len - 1;

          for (int i = 0; i < source.length; i++)
          {
             target[max - (i % len)] ^= source[i] & 0xFF;
          }
       }

       /**
        * This implements a standard CRC-16 (also known as CRC-16-IBM or CRC-16-IBM) algorithm that
        * uses a reversed polynomial of 0xA001 and swapped byte ordering. Using a reversed polynomial
        * means it processes each byte's least significant bit first (it shifts the binary data to
        * the right). Using swapped byte ordering means that the input data (which is being treated
        * as an arbitrarily large binary number) must be processed from highest element to lowest
        * element, whereas normally one might consider the most significant byte to be in the highest
        * array index position, this algorithm assumes the opposite.
        * <p>
        * If this algorithm generated uniformly distributed hashes, then in a best case scenario the
        * probability of a collision between any 2 items is (1 / 2^16) or .00152588 %.  That seems
        * good until one considers that between any 300 items there is a 50% chance of collisions
        * and between any 430 items there is a 75% chance of a collision!  This is the well known
        * birthday problem (see <a href="http://en.wikipedia.org/wiki/Birthday_attack">this page</a>).
        * <p>
        * Please note that the CRC-16 algorithm is not suitable for cryptographic hashing.  It does
        * NOT generate uniformly distributed hashes.  This means that the collision rate is not even
        * as good as the best case scenario.  Even if it did have uniform distribution, the small
        * number of bits means that collisions are not very rare.  CRC is more suitable for error
        * detection.  It should NOT be used for secure purposes.
        *
        * @param   data
        *          The bytes of data to CRC.
        * @param   crc
        *          The initial CRC value into which each element of the data will be XOR'd.
        *
        * @return  The calculated CRC value after factoring in all data bytes and binary dividing
        *          the polynomial.
        */
       private static int crc16(short[] data, int crc)
       {
          // iterate from the top of the input array to the bottom
          for (int idx = (data.length - 1); idx >= 0; idx--)
          {
             // XOR the input data into the crc
             crc ^= data[idx] & 0xFF;

             int bit = 7;

             // process each bit
             while (bit >= 0)
             {
                // check if the least significant bit is set (must be done before shifting)
                boolean lsb = ((crc & 0x01) == 1);

                // shift the data right by one bit
                crc >>= 1;

                if (lsb)
                {
                   // the least significant bit was set, XOR the polynomial into the crc
                   crc ^= REVERSED_POLYNOMIAL;
                }

                bit--;
             }
          }

          return crc;
       }

       /**
        * Convert the source array into an array of bytes with the same number of elements, but
        * where each byte may only contain uppercase or lowercase English alphabetic characters
        * (a-z and A-Z).
        * <p>
        * The source array will have each element translated into a corresponding element in the
        * output array.  Only the least significant byte of the source array element is considered
        * in the translation algorithm.  The order of the elements in the output array will be
        * the same order as the source array (e.g. the first source element translates to the
        * first output element and so forth).
        * <p>
        * The following algorithm is used to translate the 256 possible source byte values into
        * the 52 possible output byte values:
        * <p>
        * <ol>
        *   <li> If the least significant 7 bits of the source byte are one of the 52 possible
        *        English alphabetic characters, then that is the resulting byte.  This will
        *        yield a byte with 0x41 ('A') through 0x5A ('Z') or 0x61 ('a') - 0x7A ('z'),
        *        inclusive.
        *   <li> Otherwise, the most significant (upper) nibble of the source byte will be used
        *        to select a character from 'a' through 'q'.  This nibble can only have one of
        *        16 possible values (0 through 15).  This nibble value is used as a direct
        *        index into the 16 possible lowercase output letters.  Thus, a nibble value of
        *        0 (0x0) will yield 'a' (0x61), 1 (0x1) will yield 'b' (0x62) and so on, with
        *        the last possible value of 15 (0xF) yielding 'p' (0x70).
        * </ol>
        * <p>
        * Assuming the source bytes are uniformly distributed, this translation approach is
        * guaranteed to result in a highly UNEVEN distribution of output bytes.  This is
        * cryptographically BAD and should NOT be used for secure purposes. To understand
        * why this occurs:
        * <p>
        * <pre>
        * Source Byte Range    Output Byte                               Distribution Notes
        * -----------------    -----------    ----------------------------------------------------------------------------
        * 0x00 - 0x40          'a' - 'e'      'a' through 'd' are 16X more likely than 'e'
        * 0x41 - 0x5A          'A' - 'Z'      all equally likely (1 to 1 mapping of input and output)
        * 0x5B - 0x60          'f' - 'g'      'f' 5X more likely than 'g'
        * 0x61 - 0x7A          'a' - 'z'      all equally likely (1 to 1 mapping of input and output)
        * 0x7B - 0xC0          'h' - 'm'      'i' through 'l' are 16X more likely than 'm', 'h' is 5X more likely than 'm'
        * 0xC1 - 0xDA          'A' - 'Z'      all equally likely (1 to 1 mapping of input and output)
        * 0xDB - 0xE0          'n' - 'o'      'n' 5X more likely than 'o'
        * 0xE1 - 0xFA          'a' - 'z'      all equally likely (1 to 1 mapping of input and output)
        * 0xFB - 0xFF          'p'            only 'p' is possible
        * </pre>
        * <p>
        * More graphically, here is the exact distribution that will occur for perfectly distributed input:
        * <p>
        * <pre>
        * Character   Frequency          Histogram
        * ---------   ---------   -----------------------
        * A           2           ++
        * B           2           ++
        * C           2           ++
        * D           2           ++
        * E           2           ++
        * F           2           ++
        * G           2           ++
        * H           2           ++
        * I           2           ++
        * J           2           ++
        * K           2           ++
        * L           2           ++
        * M           2           ++
        * N           2           ++
        * O           2           ++
        * P           2           ++
        * Q           2           ++
        * R           2           ++
        * S           2           ++
        * T           2           ++
        * U           2           ++
        * V           2           ++
        * W           2           ++
        * X           2           ++
        * Y           2           ++
        * Z           2           ++
        * a           18          ++++++++++++++++++
        * b           18          ++++++++++++++++++
        * c           18          ++++++++++++++++++
        * d           18          ++++++++++++++++++
        * e           3           +++
        * f           7           +++++++
        * g           3           +++
        * h           7           +++++++
        * i           18          ++++++++++++++++++
        * j           18          ++++++++++++++++++
        * k           18          ++++++++++++++++++
        * l           18          ++++++++++++++++++
        * m           3           +++
        * n           7           +++++++
        * o           3           +++
        * p           7           +++++++
        * q           2           ++
        * r           2           ++
        * s           2           ++
        * t           2           ++
        * u           2           ++
        * v           2           ++
        * w           2           ++
        * x           2           ++
        * y           2           ++
        * z           2           ++
        * </pre>
        * <p>
        * This non-uniform distribution is purely due to the fallback approach of using the
        * upper nibble of some bytes as a selector into a subset of the possible output values.
        * Since the only a subset of the output values are targeted, it makes those values
        * more likely to occur.  In addition, because there are some upper nibble values that
        * are less likely to be encountered (because they rarely trigger the fallback mechanism
        * in the first place), this causes an additional level of non-uniformity even within
        * the subset that is possible to be selected.
        *
        * @param    source
        *           The bytes to convert.  Must not be <code>null</code>.  Only the least
        *           significant byte of each element will be used.
        *
        * @return   The converted array of bytes, with one element for each corresponding
        *           element of the source array.
        */
       private static byte[] translateToAlpha(short[] source)
       {
          byte[] target = new byte[source.length];

          for (int idx = 0; idx < source.length; idx++)
          {
             target[idx] = (byte)(source[idx] & 0x7F);

             if ((target[idx] < 'A' || (target[idx] > 'Z' && target[idx] < 'a') || target[idx] > 'z'))
             {
                target[idx] = (byte)(((source[idx] & 0xFF) >> 4) + 'a');
             }
          }

          return target;
       }

       /**
        * Encrypts or decrypts the source using the given key, initialization vector and symmetric
        * encryption algorithm, and returns a {@link memptr} instance containing the encrypted
        * (or decrypted) data.
        * <p>
        * This API performs validation of the specified encryption key, initialization vector and
        * encryption algorithm, following the legacy 4GL rules.
        * <p>
        * At this time, only the "AES_CBC" algorithm is supported.
        *
        * @param    data
        *           The data to be encrypted.
        * @param    key
        *           The encryption key.
        * @param    ivValue
        *           An initialization vector to be used with the given key.
        * @param    algorithm
        *           The encryption algorithm's name.
        *
        * @return   See above.
        */
       private static byte[] encryptDecryptWorker(int        cipherMode,
                                                  BinaryData data,
                                                  raw        key,
                                                  raw        ivValue,
                                                  character  algorithm)
       {
          byte[] bkey = null;
          byte[] biv = new byte[0];

          if (data.isUnknown())
          {
             return null;
          }

          if (key == null || key.isUnknown())
          {
             // TODO: defaults to SECURITY-POLICY:SYMMETRIC-ENCRYPTION-KEY; if this is unknown too,
             // produces an error

             String msg = "Key must be specifed via Security-Policy:Symmetric-encryption-key " +
                          "or in " + (cipherMode == Cipher.ENCRYPT_MODE ? "ENCRYPT" : "DECRYPT");
             ErrorManager.recordOrShowError(new int[] {12121}, new String[] {msg},
                      false, false, false, false, false);
             return null;
          }
          else
          {
             bkey = key.getByteArray();
          }

          if (ivValue != null)
          {
             if (ivValue.isUnknown())
             {
                // TODO: defaults to SECURITY-POLICY:SYMMETRIC-ENCRYPTION-IV; if also unknown, then
                // do not use an initialization vector
             }
             else
             {
                biv = ivValue.asByteArray();
             }
          }
          else
          {
             // otherwise, remains unset
          }

          String alg = algorithm.toStringMessage();
          Matcher matcher = KEY_LEN.matcher(alg);
          if (!matcher.matches())
          {
             // With 4GL both for ENCRYPT and DECRYPT "ENCRYPT" is used in the message.
             String msg = "Unable to determine key length in bytes, " +
                      (cipherMode == Cipher.ENCRYPT_MODE  ? "ENCRYPT" : "ENCRYPT") + " function";
             ErrorManager.recordOrShowError(new int[] {12124}, new String[] {msg},
                      false, false, false, false, false);
             return null;
          }
          int kl = Integer.valueOf(matcher.group(1));
          if (!(DES.matcher(alg).matches() && bkey.length * 7  == kl) && (bkey.length * 8  != kl))
          {
             String msg = "Inappropriate key length, " +
              (cipherMode == Cipher.ENCRYPT_MODE  ?
                    "ENCRYPT" : "DECRYPT") + " function";
             ErrorManager.recordOrShowError(new int[] {12707}, new String[] {msg},
                                            false, false, false, false, false);
             return null;
          }

          CipherParams cipher;
          try {
             cipher = SecurityPolicyManager.getSymmetricCipherParams(alg);
          }
          catch(IllegalArgumentException e) {
             String msg = "Unsupported cipher for " +
                                     (cipherMode == Cipher.ENCRYPT_MODE  ? "ENCRYPT" : "DECRYPT");
             ErrorManager.recordOrShowError(new int[] {12146}, new String[] {msg},
                                     false, false, false, false, false);
             return null;
          }

          try
          {
             return cipher.process(data.getByteArray(), cipherMode, bkey,
                      cipher.getIvSize() == 0 ? null : biv);
          }
          catch (Exception e)
          {
             String msg = (cipherMode == Cipher.ENCRYPT_MODE  ?
                    "ENCRYPT" : "DECRYPT") + " error";
             ErrorManager.recordOrShowError(new int[] {14272}, new String[] {msg},
                      false, false, false, false, false);

             LOG.log(Level.SEVERE, "Error processing encrypt/decrypt", e);
          }

          return null;
       }

       /**
        * Validate the CLIENT-PRINCIPAL object, if it can be used with the specified database.
        * <p>
        * The authentication being performed depends on the CLIENT-PRINCIPAL state; currently, only
        * SSO authentication is possible - this API expects a sealed CLIENT-PRINCIPAL, and it checks
        * if the domain name and access code configuration is valid.
        *
        * @param    clientPrincipal
        *           The CLIENT-PRINCIPAL instance.
        * @param    db
        *           The database name.
        * @param    callee
        *           where the method is called from
        *
        * @return   <code>true</code> if the object can be authenticated with the specified database.
        */
       private static boolean validateCP(ClientPrincipal clientPrincipal, String db, SealCallee callee)
       {
          if (db == null)
          {
             return false;
          }

          WorkArea wa = local.get();
          Persistence persistence = ConnectionManager.getPersistence(db);
          Optional<Map<QUserId, String>> users = wa.getUsers(db);
          if (!users.isPresent())
          {
             refreshUsers(wa, db, persistence);
             users = wa.getUsers(db);
          }
          refreshDomains(wa, db, persistence);

          Domain domain = wa.findDomain(db, clientPrincipal.getDomainName().getValue());
          // TODO: add support for special domains type and authentication callback
          boolean isDefaultDomainType = domain != null &&
                                        !SPECIAL_AUTH_SYSTEMS.contains(domain.type.name.toLowerCase());
          LoginState loginState = LoginState.valueOf(clientPrincipal.getLoginState().toStringMessage());

          switch (loginState)
          {
             case INITIAL:
                if ((domain != null) && (!domain.enabled || !domain.type.enabled))
                {
                   ClientPrincipal.authenticationFailed(16391, db, "Domain is disabled");
                   clientPrincipal.authenticationFailed("The PAM Registry domain is disabled");
                   return false;
                }
                boolean ok;
                if (isDefaultDomainType)
                {
                   if (!_USER_TABLE_AUTH_SYSTEM.equalsIgnoreCase(domain.type.name))
                   {
                      ClientPrincipal.authenticationFailed(16392, db,
                                                           "Domain authentication action not supported ");
                      clientPrincipal.authenticationFailed("The Domain authentication action is not allowed");
                      return false;
                   }
                   QUserId quid = new QUserId(clientPrincipal);
                   String pwd = users.map(m -> m.get(quid)).orElse(null);
                   ok = clientPrincipal.validatePassword(db, domain.accessCode, pwd, SealCallee.SET_DB_CLIENT)
                                       .booleanValue();
                }
                else
                {
                   if (domain == null)
                   {
                      ClientPrincipal.authenticationFailed(16388, db, "Cannot find Domain configuration ");
                      clientPrincipal.authenticationFailed("Cannot find the named PAM domain service");
                      return false;
                   }
                   ok = clientPrincipal.validateDomainAccessCode(db, domain.accessCode, SealCallee.SET_DB_CLIENT)
                                       .booleanValue();
                }

                loginState = LoginState.valueOf(clientPrincipal.getLoginState().toStringMessage());
                if (!ok && loginState == LoginState.INITIAL)
                {
                   invalidState(db, clientPrincipal.sealFailure());
                }

                return ok;

             case EXPIRED:
                if (clientPrincipal.isSealed())
                {
                   ClientPrincipal.validationFailed(16376, db, "User authentication expired");
                   return false;
                }
                invalidState(db, clientPrincipal.sealFailure());
                return false;

             case LOGIN:
                if (isDefaultDomainType && !clientPrincipal.validateDomainAccessCode(domain.accessCode))
                {
                   ClientPrincipal.validationFailed(16385, db, "The client-principal was corrupt");
                   if (callee != SealCallee.SET_CLIENT)
                   {
                      ErrorManager.recordOrShowError(new int[] { 13691 },
                               new String[] { String.format("Failed to set %s user id.", db) }, false,
                               false, false, false, false);
                   }
                   return false;
                }
                if ((domain != null) && (!domain.enabled || !domain.type.enabled))
                {
                   ClientPrincipal.authenticationFailed(16391, db, "Domain is disabled");
                   clientPrincipal.authenticationFailed("The PAM Registry domain is disabled");
                   return false;
                }
                if (!isDefaultDomainType && domain == null)
                {
                   if (callee == SealCallee.SET_CLIENT)
                   {
                      ClientPrincipal.validationFailed(16388, db, "Cannot find Domain configuration ");
                   }
                   else
                   {
                      ClientPrincipal.validationFailed(16389, db, "Nonexistent Domain");
                   }
                   return false;
                }
                return true;

             case LOGOUT:
                ClientPrincipal.validationFailed(16367, db, "Insufficient privileges");
                return false;

             case FAILED:
                ClientPrincipal.validationFailed(16369, db, "Failed account authentication");
                return false;

             default: // TODO: handle other states
                return false;
          }
       }

       /**
        * Report invalid CLIENT-PRINCIPAL state
        *
        * @param where
        *        subsystem name.
        * @param sealFailure
        *        seal failure reason
        */
       private static void invalidState(String where, SealFailure sealFailure)
       {
          String whatsUp = "is in unexpected state";
          switch (sealFailure)
          {
             case EXPIRED:
             case INVALID_STATE:
                whatsUp = "is in an invalid state";
                break;
             case MISSING_ATTRIBUTES:
                whatsUp = "was missing required attributes";
                break;
          }
          // "princpal" - as in 4GL
          ErrorManager.recordOrShowError(new int[] {15944},
                new String[] {
                      "Failed " + where + " client princpal authentication because: the " +
                      "Client-Principal " + whatsUp
                },
                false, false, false, false, false);
       }

       /**
        * Validate the CLIENT-PRINCIPAL object, if it can be used with the specified database.
        * <p>
        * The authentication being performed depends on the CLIENT-PRINCIPAL state; currently, only
        * SSO authentication is possible - this API expects a sealed CLIENT-PRINCIPAL, and it checks
        * if the domain name and access code configuration is valid.
        *
        * @param    client
        *           The CLIENT-PRINCIPAL instance.
        * @param    db
        *           The database name.
        *
        * @return   <code>true</code> if the object can be authenticated with the specified database.
        */
       private static boolean validateClient(ClientPrincipal client, String db)
       {
          // - TODO: if unsealed, then authenticate, seal, and performed as if it was sealed
          // - if is sealed, then validate the client-principal and set the USERID

          if (!client.isSealed())
          {
             // TODO: for now, we raise an error
             // TODO: client:PRIMARY-PASSPHRASE is required

             // TODO: if the target domain type is '_oeusertable', then use customSecurityOps
             // TODO: if the target domain type is '_oslocal', then use PAM/something else to validate??
             // TODO: if the target domain type is something else???
             return false;
          }

          if (db != null)
          {
             db = db.toLowerCase();
          }

          Domain d = local.get().findDomain(db, client.getDomainName().getValue());
          // TODO: how is client:DOMAIN-TYPE involved here?

          if (d == null)
          {
             // TODO: what error is it thrown?
             return false;
          }

          if (!d.enabled || !d.type.enabled)
          {
             // TODO: what error is it thrown?
             return false;
          }

          return d.accessCode.equals(client.getDomainAccessCode());
       }

       /**
        * Stores context-local state related to security.
        */
       public static class WorkArea
       {
          /** The default domain types. */
          private final Map<String, DomainType> defaultDomainTypes = new HashMap<>();

          /** The default domains. */
          private final Map<String, Domain> defaultDomains = new HashMap<>();

          /** The db-specific domain types. */
          private final Map<String, Map<String, DomainType>> dbDomainTypes = new HashMap<>();

          /** The db-specific domains. */
          private final Map<String, Map<String, Domain>> dbDomains = new HashMap<>();

          /** The db-specific users */
          public final Map<String, Map<QUserId, String>> dbUsers = new HashMap<>();

          /** The timestamps when the _sec-auth-domain were last refreshed. */
          final Map<String, Long> domainsLastRead = new HashMap<>();

          /** The timestamps when the _sec-auth-domain were last refreshed. */
          final Map<String, Long> domainTypesLastRead = new HashMap<>();

          /** The timestamps when the _user were last refreshed. */
          final Map<String, Long> usersLastRead = new HashMap<>();

          /**
           * Initialize the default domains and domain types.
           * <p>
           * For each directory-defined domain and domain type, you can specify either the full
           * definition, or just certain state (like disabled), if this is a default domain or domain
           * type.
           */
          public WorkArea()
          {
             // initialize the default domain types
             addDomainType(null,
                           "_extsso",
                           "Built-in SSO module",
                           true);
             addDomainType(null,
                           "_oeusertable",
                           "Built-in authentication module",
                           true);
             addDomainType(null,
                           "_oslocal",
                           "Built-in authentication module to the local OS user accounts",
                           true);

             // initialize the default domains
             addDomain(null,
                       "",
                       "_oeusertable",
                       "Default user domain",
                       null,
                       true,
                       TenantManager.DEFAULT_TENANT_NAME);
             addDomain(null,
                       "UNIX",
                       "_oslocal",
                       "Built-in domain for UNIX user accounts, reserved for database command " +
                       "line utilities",
                       null,
                       true,
                       TenantManager.DEFAULT_TENANT_NAME);
             addDomain(null,
                       "UNIXID",
                       "_oslocal",
                       "Built-in SSO domain to the UNIX process user, reserved for database " +
                       "command line utilities",
                       null,
                       true,
                       TenantManager.DEFAULT_TENANT_NAME);
             addDomain(null,
                       "WINDOWS",
                       "_oslocal",
                       "Built-in domain for WINDOWS user accounts, reserved for database command " +
                       "line utilities",
                       null,
                       true,
                       TenantManager.DEFAULT_TENANT_NAME);
             addDomain(null,
                       "WINDOWSID",
                       "_oslocal",
                       "Built-in SSO domain to the WINDOWS process user, reserved for database " +
                       "command line utilities",
                       null,
                       true,
                       TenantManager.DEFAULT_TENANT_NAME);

             // read the directory configuration
             DirectoryService ds = DirectoryService.getInstance();
             if (!ds.bind())
             {
                throw new RuntimeException("Directory bind failed");
             }

             try
             {
                String domainTypesPath = Utils.findDirectoryNodePath(ds,
                                                                     "security-domain-types",
                                                                     "container",
                                                                     false);
                if (domainTypesPath != null)
                {
                   String[] nodes = ds.enumerateNodes(domainTypesPath);

                   // load domain types first
                   for (String node : nodes)
                   {
                      String nodeId = domainTypesPath + "/" + node;
                      Boolean enabled = ds.getNodeBoolean(nodeId, "enabled");
                      String description = ds.getNodeString(nodeId, "description");
                      String dbname = ds.getNodeString(nodeId, "dbname");

                      addDomainType(dbname, node, description, enabled);
                   }
                }

                String domainsPath = Utils.findDirectoryNodePath(ds,
                                                                 "security-domains",
                                                                 "container",
                                                                 false);
                if (domainsPath != null)
                {
                   // TODO: can't enable/disable default domain with 'empty' name, as FWD doesn't allow
                   // container node as 'name=""'

                   String[] nodes = ds.enumerateNodes(domainsPath);

                   // load domain last
                   for (String node : nodes)
                   {
                      String nodeId = domainsPath + "/" + node;
                      String type = ds.getNodeString(nodeId, "type");
                      Boolean enabled = ds.getNodeBoolean(nodeId, "enabled");
                      String description = ds.getNodeString(nodeId, "description");
                      String accessCode = ds.getNodeString(nodeId, "access_code");
                      String dbname = ds.getNodeString(nodeId, "dbname");
                      String tenantName = ds.getNodeString(nodeId, "tenant_name");

                      addDomain(dbname, node, type, description, accessCode, enabled, tenantName);
                   }
                }
             }
             finally
             {
                ds.unbind();
             }
          }

          /**
           * Create a domain type with the specified details.
           *
           * @param    dbname
           *           When <code>null</code>, this will be added as a {@link #defaultDomainTypes}.
           * @param    name
           *           The domain type's name.
           * @param    description
           *           The domain type's description.
           * @param    enabled
           *           Flag indicating if this domain is enabled.
           * @return   existing or created domain type for specified database and name
           */
          public DomainType addDomainType(String dbname, String name, String description, Boolean enabled)
          {
             Map<String, DomainType> dbdt;
             if (dbname != null)
             {
                dbname = dbname.toLowerCase();
                dbdt = dbDomainTypes.computeIfAbsent(dbname, k -> new HashMap<>());
             }
             else
             {
                dbdt = defaultDomainTypes;
             }

             String dtkey = name == null ? "" : name.toLowerCase();
             DomainType domainType = findDomainType(dbname, dtkey);
             if (domainType == null)
             {
                dbdt.put(dtkey, domainType = new DomainType(name, description));
             }
             else if (dbname != null && domainType == findDomainType(null, dtkey))
             {
                // add a copy to this db-specific override, as the default was found
                dbdt.put(dtkey, domainType = new DomainType(domainType));
             }

             if (enabled != null)
             {
                domainType.enabled = enabled;
             }
             if (description != null)
             {
                domainType.description = description;
             }
             return domainType;
          }

          /**
           * Create a domain with the specified details.
           *
           * @param    dbname
           *           When <code>null</code>, this will be added as a {@link #defaultDomains}.
           * @param    name
           *           The domain's name.
           * @param    type
           *           The domain's type.
           * @param    description
           *           The domain's description.
           * @param    accessCode
           *           The domain's access code.  If it is not specified and this is a non-default
           *           domain, then the {@link SecurityOps#DEFAULT_DOMAIN_ACCESS_CODE} value will be used.
           * @param    enabled
           *           Flag indicating if this domain is enabled.
           * @param    tenant_name
           *           The tenant name from this domain.
           */
          public void addDomain(String  dbname,
                                String  name,
                                String  type,
                                String  description,
                                String  accessCode,
                                Boolean enabled,
                                String  tenant_name)
          {
             Map<String, Domain> dbd;
             if (dbname != null)
             {
                dbname = dbname.toLowerCase();
                dbd = dbDomains.computeIfAbsent(dbname, k -> new HashMap<>());
             }
             else
             {
                dbd = defaultDomains;
             }

             DomainType domainType = type == null ? null : findDomainType(dbname, type);

             if (type == null)
             {
                throw new IllegalStateException("Domain type was not specified for domain " + name + "!");
             }
             if (domainType == null)
             {
                throw new IllegalStateException(
                      "Domain type " + type + " was not found for domain " + name + "!");
             }

             String dkey = name.toLowerCase();

             Domain domain = findDomain(dbname, dkey);
             if (domain == null)
             {
                if (accessCode == null)
                {
                   accessCode = DEFAULT_DOMAIN_ACCESS_CODE;
                }

                dbd.put(dkey, domain = new Domain(name, domainType, description, accessCode, tenant_name));
             }
             else if (dbname != null && domain == findDomain(null, dkey))
             {
                // add a copy to this db-specific override, as the default was found
                dbd.put(dkey, domain = new Domain(domain));
             }

             domain.type = domainType;

             if (enabled != null)
             {
                domain.enabled = enabled;
             }
             if (description != null)
             {
                domain.description = description;
             }
             if (accessCode != null)
             {
                domain.accessCode = accessCode;
             }
          }

          /**
           * Find the domain type instance; the lookup rules are:
           * <ol>
           *    <li>if <code>dbName</code> is not specified, look in {@link #defaultDomainTypes}.</li>
           *    <li>if <code>dbName</code> is specified, look in {@link #dbDomainTypes}.</li>
           *    <li>if not found in {@link #dbDomainTypes}, look in {@link #defaultDomainTypes}.</li>
           * </ol>
           *
           * @param    dbName
           *           The database name.  May be <code>null</code>.
           * @param    type
           *           The domain type name to look.
           *
           * @return   The found {@link DomainType} instance, or <code>null</code> if not found.
           */
          private DomainType findDomainType(String dbName, String type)
          {
             Map<String, DomainType> dbTypes = null;
             if (dbName != null)
             {
                dbTypes = dbDomainTypes.get(dbName);
             }
             if (dbTypes == null)
             {
                dbTypes = defaultDomainTypes;
             }

             return dbTypes.get(type.toLowerCase());
          }

          /**
           * Find the domain instance; the lookup rules are:
           * <ol>
           *    <li>if <code>dbName</code> is not specified, look in {@link #defaultDomains}.</li>
           *    <li>if <code>dbName</code> is specified, look in {@link #dbDomains}.</li>
           *    <li>if not found in {@link #dbDomains}, look in {@link #defaultDomains}.</li>
           * </ol>
           *
           * @param    dbName
           *           The database name.  May be <code>null</code>.
           * @param    domain
           *           The domain name to look.
           *
           * @return   The found {@link Domain} instance, or <code>null</code> if not found.
           */
          private Domain findDomain(String dbName, String domain)
          {
             Map<String, Domain> dbDomains = null;
             if (dbName != null)
             {
                dbDomains = this.dbDomains.get(dbName.toLowerCase());
             }
             if (dbDomains == null)
             {
                dbDomains = defaultDomains;
             }

             return dbDomains.get(domain.toLowerCase());
          }

          /**
           * Add database user.
           *
           * @param   dbName
           *          The database name.
           * @param   quid
           *          The qualified user id.
           * @param   password
           *          The user's password.
           */
          public void addUser(String dbName, QUserId quid, String password)
          {
             Map<QUserId, String> users = this.dbUsers.computeIfAbsent(dbName, k -> new HashMap<>());
             users.put(quid, password);
          }

          /**
           * Get map of database user's passwords by qualified user id
           *
           * @param   dbName
           *          The database name
           *
           * @return map of database user's passwords by qualified user id
           */
          public Optional<Map<QUserId, String>> getUsers(String dbName)
          {
             Map<QUserId, String> users = this.dbUsers.get(dbName);
             return users == null ? Optional.empty() :
                Optional.of(Collections.unmodifiableMap(users));
          }
       }

       /**
        * Find the domain type instance; the lookup rules are:
        * <ol>
        *    <li>if <code>dbName</code> is not specified, look in defaultDomainTypes.</li>
        *    <li>if <code>dbName</code> is specified, look in dbDomainTypes.</li>
        *    <li>if not found in dbDomainTypes, look in defaultDomainTypes.</li>
        * </ol>
        *
        * @param    dbName
        *           The database name.  May be <code>null</code>.
        * @param    type
        *           The domain type name to look.
        *
        * @return   The found {@link DomainType} instance, or <code>null</code> if not found.
        */
       public static DomainType findDomainType(String dbName, String type)
       {
          return local.get().findDomainType(dbName, type);
       }

       /**
        * Create a domain type with the specified details.
        *
        * @param    dbname
        *           When <code>null</code>, this will be added as a defaultDomainTypes.
        * @param    name
        *           The domain type's name.
        * @param    description
        *           The domain type's description.
        * @param    enabled
        *           Flag indicating if this domain is enabled.
        *
        * @return   existing or created domain type for specified database and name
        */
       public static DomainType addDomainType(String dbname, String name, String description, Boolean enabled)
       {
          return local.get().addDomainType(dbname, name, description, enabled);
       }

       /**
        * Find the domain instance; the lookup rules are:
        * <ol>
        *    <li>if <code>dbName</code> is not specified, look in defaultDomains.</li>
        *    <li>if <code>dbName</code> is specified, look in dbDomains.</li>
        *    <li>if not found in dbDomains, look in defaultDomains.</li>
        * </ol>
        *
        * @param    dbName
        *           The database name.  May be <code>null</code>.
        * @param    domain
        *           The domain name to look.
        *
        * @return   The found {@link Domain} instance, or <code>null</code> if not found.
        */
       public static Domain findDomain(String dbName, String domain)
       {
          return local.get().findDomain(dbName, domain);
       }

       /**
        * Get map of database user's passwords by qualified user id
        *
        * @param dbName
        *        the database name
        *
        * @return map of database user's passwords by qualified user id
        */
       public static Optional<Map<QUserId, String>> getUsers(String dbName)
       {
          return local.get().getUsers(dbName);
       }

       /**
        * Remove previously loaded information about domain types, domains, and users for a database and then
        * reload all over again making sure their up-to-date version id stored in memory.
        *
        * @param   dbName
        *          database name
        * @param   persistence
        *          The {@code Persistence} for the database.
        */
       static void refreshUsersAndDomains(String dbName, Persistence persistence)
       {
          WorkArea wa = local.get();
          refreshUsers(wa, dbName, persistence);
          refreshDomains(wa, dbName, persistence);
       }

       /** Qualified user id. */
       public static class QUserId
       {
          /** The user's name, in upper-case. */
          private final String uid;

          /** The user's domain, also in upper-case. */
          private final String domain;

          /** Precomputed hash code for this object. */
          private final int hc;

          /**
           * Constructor.
           *
           * @param uid
           *        user id
           * @param domain
           *        domain name
           */
          public QUserId(String uid, String domain)
          {
             this.uid = uid == null ? "" : uid.toUpperCase();
             this.domain = domain == null ? "" : domain.toUpperCase();

             // pre-compute the hash code
             int prime = 31;
             int result = 1;
             result = prime * result + ((domain == null) ? 0 : domain.hashCode());
             hc = prime * result + ((uid == null) ? 0 : uid.hashCode());
          }

          /**
           * Constructor.
           *
           * @param cp
           *        {@link ClientPrincipal}
           */
          public QUserId(ClientPrincipalResource cp)
          {
             this(cp.getUserId().value, cp.getDomainName().value);
          }

          /**
           * Returns a hash code value for the object.
           *
           * @return a hash code value for this object.
           */
          @Override
          public int hashCode()
          {
             return hc;
          }

          /**
           * Indicates whether some other object is "equal to" this one.
           *
           * @param   obj   the reference object with which to compare.
           * @return  {@code true} if this object is the same as the obj
           *          argument; {@code false} otherwise.
           */
          @Override
          public boolean equals(Object obj)
          {
             if (this == obj)
             {
                return true;
             }
             if (obj == null)
             {
                return false;
             }
             if (getClass() != obj.getClass())
             {
                return false;
             }

             QUserId other = (QUserId) obj;
             if (domain == null)
             {
                if (other.domain != null)
                {
                   return false;
                }
             }
             else if (!domain.equals(other.domain))
             {
                return false;
             }

             if (uid == null)
             {
                if (other.uid != null)
                {
                   return false;
                }
             }
             else if (!uid.equals(other.uid))
             {
                return false;
             }

             return true;
          }
       }

       /**
        * This defines the configuration of a security domain, with state taken from the 4GL legacy
        * <code>_sec-authentication-system.d</code> system table.
        * <p>
        * When DomainType is enabled then <code>false</code>, all domains having this type will
        * be deactivated.
        */
       public static class DomainType
       {
          /** The domain type's name. */
          private final String name;

          /** Flag indicating if this domain type is enabled. */
          private boolean enabled;

          /** The domain type's description. */
          private String description;

          /**
           * Create a new domain type and initialize it with the given state.
           *
           * @param    name
           *           The domain's name.
           * @param    description
           *           The domain's description.
           */
          public DomainType(String name, String description)
          {
             this(name, description, true);
          }

          /**
           * Create a new domain type and initialize it with the given state.
           *
           * @param    name
           *           The domain's name.
           * @param    description
           *           The domain's description.
           * @param    enabled
           *           Flag indicating if this domain type is enabled.
           */
          public DomainType(String name, String description, boolean enabled)
          {
             this.name = name;
             this.description = description;
             this.enabled = enabled;
          }

          /**
           * Create a new domain type and copy the state from the specified source.
           *
           * @param    src
           *           The source domain type.
           */
          public DomainType(DomainType src)
          {
             this.name = src.name;
             this.enabled = src.enabled;
             this.description = src.description;
          }

          /**
           * Get domain type name.
           * @return domain type name.
           */
          public String getName()
          {
             return name;
          }

          /**
           * Check if domain type is enabled.
           * @return <code>true</code> if domain type is enabled.
           */
          public boolean isEnabled()
          {
             return enabled;
          }

          /**
           * Get domain type description.
           * @return domain type description.
           */
          public String getDescription()
          {
             return description;
          }
       }

       /**
        * This defines the configuration of a security domain, with state taken from the 4GL legacy
        * <code>_sec-authentication-domain.d</code> system table.
        * <p>
        * The  Domain accessCode is the unencrypted value of the access code.
        */
       public static class Domain
       {
          /** The domain's name. */
          private final String name;

          /** The domain's type. */
          private DomainType type;

          /** Flag indicating if this domain is enabled. */
          private boolean enabled;

          /** The domain's description. */
          private String description;

          /** The domain's access code. */
          private String accessCode;

          /** The tenant name for this domain. */
          private String tenantName;

          /**
           * Create a new domain and initialize it with the given state.
           *
           * @param   name
           *          The domain's name.
           * @param   type
           *          The domain's type.
           * @param   description
           *          The domain's description.
           * @param   accessCode
           *          The domain's access code.
           * @param   tenantName
           *          The tenant name for this domain.
           */
          public Domain(String name, DomainType type, String description, String accessCode, String tenantName)
          {
             this(name, type, true, description, accessCode, tenantName);
          }

          /**
           * Create a new domain and initialize it with the given state.
           *
           * @param   name
           *          The domain's name.
           * @param   type
           *          The domain's type.
           * @param   enabled
           *          Flag indicating if this domain is enabled.
           * @param   description
           *          The domain's description.
           * @param   accessCode
           *          The domain's access code.
           * @param   tenantName
           *          The tenant name for this domain.
           */
          public Domain(String     name,
                        DomainType type,
                        boolean    enabled,
                        String     description,
                        String     accessCode,
                        String     tenantName)
          {
             this.name = name;
             this.type = type;
             this.description = description;
             this.accessCode = accessCode;
             this.enabled = enabled;
             this.tenantName = (tenantName == null) ? TenantManager.DEFAULT_TENANT_NAME : tenantName;
          }

          /**
           * Create a new domain and copy the state from the specified source.
           *
           * @param    src
           *           The source domain.
           */
          public Domain(Domain src)
          {
             this.name = src.name;
             this.type = src.type;
             this.enabled = src.enabled;
             this.description = src.description;
             this.accessCode = src.accessCode;
             this.tenantName = src.tenantName;
          }

          /**
           * Get the domain name.
           *
           * @return  The domain name.
           */
          public String getName()
          {
             return name;
          }

          /**
           * Get the domain type.
           *
           * @return  The domain type.
           */
          public DomainType getType()
          {
             return type;
          }

          /**
           * Checks if this domain is enabled.
           *
           * @return  {@code true} if this domain is enabled.
           */
          public boolean isEnabled()
          {
             return enabled;
          }

          /**
           * Get domain description.
           *
           * @return  The domain description.
           */
          public String getDescription()
          {
             return description;
          }

          /**
           * Get domain access code.
           *
           * @return  The domain access code.
           */
          public String getAccessCode()
          {
             return accessCode;
          }

          /**
           * Get the tenant name for this domain.
           *
           * @return  the tenant name for this domain.
           */
          public String getTenantName()
          {
             return tenantName;
          }
       }
    }