ClientPrincipal.java

/*
** Module   : ClientPrincipal.java
** Abstract : Implementation of the CLIENT-PRINCIPAL resource.
**
** Copyright (c) 2018-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 CA  20181128 Created initial version.
** 002 IAS 20190507 Added more attributes and methods.
** 003 IAS 20190524 Added support for CLIENT-PRINCIPAL:(LOGIN-STATE, LOGIN-EXPIRATION-TIMESTAMP,
**                  SEAL-TIMESTAMP, PRIMARY-PASSPHRASE, QUALIFIED-USER-ID)
** 004 IAS 20190625 Added support for SECURITY-POLICY:SET-CLIENT
** 005 IAS 20190703 Changed support for SET-DB-CLIENT
** 006 CA  20191119 Import must set the properties in the attributes#PROPS.  An attempt to fix
**                  validateSeal (by computing the MAC and comparing it with the existing one).
** 007 IAS 20200112 EXPORT/IMPORT fixed, added DOMAIN-ACCESS-CODE validation with MAC,
**                  fixed MAC calculation, fixed VALIDATE-SEAL.
** 008 IAS 20200914 Re-work (de)serialization
**     OM  20201030 Invalid attribute API support for getters/setters.
** 009 CA  20211005 CLIENT-PRINCIPAL can't be added to unnamed pools.
**     CA  20211222 Fixed memory leak: when a CLIENT-PRINCIPAL is deleted, it must process the super.delete().
**     CA  20220318 Prevent a NPE in validateSeal, in case MAC is not loaded by importPrincipal.
**     IAS 20220415 Misc. fixes for error reporting.
**     IAS 20220520 Fixed DAC validation for value which is empty string.
**     IAS 20220714 Fixed DAC validation for imported sealed CLIENT-PRINCIPAL.
** 010 GBB 20230719 Fix for SESSION-ID, moved to a separate interface.
** 011 CA  20230724 Further reduce context-local usage.
** 012 LS  20250127 In readtimeStamp(), create the ts using the epoch and the offset.
** 013 OM  20250301 Performance optimisations. Code maintenance.
** 014 ICP 20250312 Added stubs for TENANT-NAME and TENANT-ID functions.
*/

/*
** 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 static com.goldencode.util.NativeTypeSerializer.*; 
import java.io.*;
import java.nio.*;
import java.nio.charset.*;
import java.security.*;
import java.time.*;
import java.time.format.*;
import java.util.*;
import java.util.function.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import org.apache.commons.io.input.*;
import org.apache.commons.lang3.*;

/**
 * Implements the CLIENT-PRINCIPAL resource and all associated APIs.
 */
public class ClientPrincipal
extends HandleResource
implements ClientPrincipalResource,
           CommonSessionId,
           Cloneable,
           Deletable,
           Externalizable
{
   private static final Set<AttrInfo> MANDATORY = Collections.unmodifiableSet(EnumSet.of(
         AttrInfo.USER_ID,
         AttrInfo.DOMAIN_NAME,
         AttrInfo.SESSION_ID
   ));

   private static final Set<AttrInfo> NOT_FOR_MAC = Collections.unmodifiableSet(EnumSet.of(
         AttrInfo.MAC,
         AttrInfo.PRIMARY_PASSPHRASE,
         AttrInfo.SEAL_TIMESTAMP,
         AttrInfo.SEAL_TIMESTAMP_TZ
   ));

   /** A token used for non repetitive logging of missing tenantid implementation.
    *  It is to be removed from the class attributes after the feature is implemented. */
   private static volatile Object tenantidContextToken = null;

   /** A token used for non repetitive logging of missing tenantname implementation.
    *  It is to be removed from the class attributes after the feature is implemented. */
   private static volatile Object tenantnameContextToken = null;

   /** Attributes. */
   private final Map<AttrInfo, Optional<Object>> attributes = new LinkedHashMap<>();

   /** Flag indicating if this resource is was was deleted. */
   private boolean deleted = false;

   /** Generated SESSION-ID. */
   private final String generatedSessionId = generateSessionID(); 
   
   /** The set properties in this CLIENT-PRINCIPAL. */
   private final Map<String, String> properties = new LinkedHashMap<>();
   
   /** Flag indicating this object has been {@link #seal sealed()}. */
   private Boolean sealed = null;
   
   /** The domain access code used to {@link #seal seal} this object. */
   private String domainAccessCode = null;

   /**  A character value that represents the current state of the client-principal object.*/
   private LoginState loginState = LoginState.INITIAL; 
   
   /** Flag indicated that CP was exported at least once. */
   private boolean exported = false;
   
   /** Flag indicated that attributed values should not be exposed (see #4801). */
   private boolean hideValues = false;
   
   /** SEAL failure reason. */
   private SealFailure sealFailure = null;

   /** Flag indicated that CP was sealed on import */
   private boolean sealedOnImport = false;

   /** The value of the 0x00d0001c undocumented hidden attribute */
   private long magic = 2;

   /** default constructor. */
   public ClientPrincipal()
   {
      addHiddenAttrs();
   }

   /**
    * Create a new resource and assign it to the specified handle.
    * 
    * @param    h
    *           The handle where to save the resource.  Must be not-null.
    */
   public static void create(handle h)
   {
      create(h, (character) null);
   }
   
   /**
    * Create a new resource and assign it to the specified handle.
    * 
    * @param    h
    *           The handle where to save the resource.  Must be not-null.
    * @param    widgetPool
    *           The named widget pool where to save the widget.  If <code>null</code>, use the
    *           closest unnamed pool.
    */
   public static void create(handle h, String widgetPool)
   {
      create(h, new character(widgetPool));
   }
   
   /**
    * Create a new resource and assign it to the specified handle.
    * 
    * @param    h
    *           The handle where to save the resource.  Must be not-null.
    * @param    widgetPool
    *           The named widget pool where to save the widget.  If <code>null</code>, use the
    *           closest unnamed pool.
    */
   public static void create(handle h, character widgetPool)
   {
      // validate widget pool before anything else!!!
      if (!WidgetPool.validWidgetPool(widgetPool))
      {
         return;
      }
      
      ClientPrincipal client = new ClientPrincipal();
      h.assign(client);

      if (widgetPool != null)
      {
         // unnamed pools are not allowed
         client.addToPool(widgetPool);
      }
   }

   /**
    * Reports if this object is valid for use.  
    *
    * @return   <code>true</code> if we are valid (can be used).
    */
   @Override
   public boolean valid()
   {
      return !deleted;
   }

   /**
    * Perform actual delete of an resource. At the time of this call, it is assumed the resource
    * is valid for deletion (the handle and the resource are both valid).
    */
   @Override
   public void delete()
   {
      super.delete();
      
      // just set it as invalid and clear some state
      deleted = true;
      attributes.clear();
      properties .clear();
      domainAccessCode = null;
   }

   /**
    * Get the USER-ID attribute.
    * 
    * @return   See above.
    */
   @Override
   public character getUserId()
   {
      return getCharAttrValue(AttrInfo.USER_ID);
   }

   /**
    * Set the USER-ID Attribute.
    * 
    * @param    userId
    *           The value of the USER-ID attribute.
    */
   @Override
   public void setUserId(character userId)
   {
      setCharAttrValue(AttrInfo.USER_ID, userId, true);
   }

   /**
    * Set the USER-ID Attribute.
    * 
    * @param    userId
    *           The value of the USER-ID attribute.
    */
   @Override
   public void setUserId(String userId)
   {
      setUserId(new character(userId));
   }

   /**
    * Get the DOMAIN-TYPE attribute.
    * 
    * @return   See above.
    */
   @Override
   public character getDomainType()
   {
      return getCharAttrValue(AttrInfo.DOMAIN_TYPE);
   }

   /**
    * Set the DOMAIN-TYPE Attribute.
    * 
    * @param    domType
    *           The value of the DOMAIN-TYPE attribute.
    */
   @Override
   public void setDomainType(character domType)
   {
      setCharAttrValue(AttrInfo.DOMAIN_TYPE, domType);
   }

   /**
    * Set the DOMAIN-TYPE Attribute.
    * 
    * @param    domainType
    *           The value of the DOMAIN-TYPE attribute.
    */
   @Override
   public void setDomainType(String domainType)
   {
      setDomainType(new character(domainType));
   }

   /**
    * Get the DOMAIN-NAME attribute.
    * 
    * @return   See above.
    */
   @Override
   public character getDomainName()
   {
      return getCharAttrValue(AttrInfo.DOMAIN_NAME);
   }

   /**
    * Set the DOMAIN-NAME Attribute.
    * 
    * @param    domainName
    *           The value of the DOMAIN-NAME attribute.
    */
   @Override
   public void setDomainName(character domainName)
   {
      setCharAttrValue(AttrInfo.DOMAIN_NAME, domainName, true);
   }

   /**
    * Set the DOMAIN-NAME Attribute.
    * 
    * @param    domainName
    *           The value of the DOMAIN-NAME attribute.
    */
   @Override
   public void setDomainName(String domainName)
   {
      setDomainName(new character(domainName));
   }

   /**
    * Get the SESSION-ID attribute.
    * 
    * @return   See above.
    */
   @Override
   public character getSessionId()
   {
      return getCharAttrValue(AttrInfo.SESSION_ID);
   }

   /**
    * Set the SESSION-ID Attribute.
    * 
    * @param    sessionId
    *           The value of the SESSION-ID attribute.
    */
   @Override
   public void setSessionId(character sessionId)
   {
      if (sessionId != null && !sessionId.isUnknown() && 
               StringUtils.isEmpty(sessionId.toStringMessage()))
      {
         attributeEmptyError("SESSION-ID");
         return;
      }
      setCharAttrValue(AttrInfo.SESSION_ID, sessionId, true);
   }

   /**
    * Set the SESSION-ID Attribute.
    * 
    * @param    sessionId
    *           The value of the SESSION-ID attribute.
    */
   @Override
   public void setSessionId(String sessionId)
   {
      setSessionId(new character(sessionId));
   }

   /**
    * Get the DOMAIN-DESCRIPTION attribute.
    * 
    * @return   See above.
    */
   @Override
   public character getDomainDescription()
   {
      return getCharAttrValue(AttrInfo.DOMAIN_DESCRIPTION);
   }

   /**
    * Set the DOMAIN-DESCRIPTION Attribute.
    * 
    * @param    desc
    *           The value of the DOMAIN-DESCRIPTION attribute.
    */
   @Override
   public void setDomainDescription(String desc)
   {
      setDomainDescription(new character(desc));
   }

   /**
    * Set the DOMAIN-DESCRIPTION Attribute.
    * 
    * @param    desc
    *           The value of the DOMAIN-DESCRIPTION attribute.
    */
   @Override
   public void setDomainDescription(character desc)
   {
      setCharAttrValue(AttrInfo.DOMAIN_DESCRIPTION, desc);
   }

   /**
    * Get the CLIENT-TTY attribute.
    * 
    * @return   See above.
    */
   @Override
   public character getClientTty()
   {
      return getCharAttrValue(AttrInfo.CLIENT_TTY);
   }
   
   /**
    * Set the CLIENT-TTY Attribute.
    * 
    * @param    tty
    *           The value of the CLIENT-TTY attribute.
    */
   @Override
   public void setClientTty(String tty)
   {
      setClientTty(new character(tty));
   }

   /**
    * Set the CLIENT-TTY Attribute.
    * 
    * @param    tty
    *           The value of the CLIENT-TTY attribute.
    */
   @Override
   public void setClientTty(character tty)
   {
      setCharAttrValue(AttrInfo.CLIENT_TTY, tty);
   }

   /**
    * Get the CLIENT-WORKSTATION attribute.
    * 
    * @return   See above.
    */
   @Override
   public character getClientWks()
   {
      return getCharAttrValue(AttrInfo.CLIENT_WORKSTATION);
   }

   /**
    * Set the CLIENT-WORKSTATION Attribute.
    * 
    * @param    wks
    *           The value of the CLIENT-WORKSTATION attribute.
    */
   @Override
   public void setClientWks(String wks)
   {
      setClientWks(new character(wks));
   }

   /**
    * Set the CLIENT-WORKSTATION Attribute.
    * 
    * @param    wks
    *           The value of the CLIENT-WORKSTATION attribute.
    */
   @Override
   public void setClientWks(character wks)
   {
      setCharAttrValue(AttrInfo.CLIENT_WORKSTATION, wks);
   }


   /**
    * Get the AUDIT-EVENT-CONTEXT attribute.
    * 
    * @return   See above.
    */
   @Override
   public character getAuditEventContext()
   {
      return getCharAttrValue(AttrInfo.AUDIT_EVENT_CONTEXT);
   }

   /**
    * Set the AUDIT-EVENT-CONTEXT Attribute.
    * 
    * @param    ctx
    *           The value of the AUDIT-EVENT-CONTEXT attribute.
    */
   @Override
   public void setAuditEventContext(String ctx)
   {
      setAuditEventContext(new character(ctx));
   }

   /**
    * Set the AUDIT-EVENT-CONTEXT Attribute.
    * 
    * @param    ctx
    *           The value of the AUDIT-EVENT-CONTEXT attribute.
    */
   @Override
   public void setAuditEventContext(character ctx)
   {
      setCharAttrValue(AttrInfo.AUDIT_EVENT_CONTEXT, ctx);
   }

   /**
    * Get the LOGIN-HOST attribute.
    * 
    * @return   See above.
    */
   @Override
   public character getLoginHost()
   {
      return getCharAttrValue(AttrInfo.LOGIN_HOST);
   }

   /**
    * Set the LOGIN-HOST Attribute.
    * 
    * @param    host
    *           The value of the LOGIN-HOST attribute.
    */
   @Override
   public void setLoginHost(String host)
   {
      setLoginHost(new character(host));
   }

   /**
    * Set the LOGIN-HOST Attribute.
    * 
    * @param    host
    *           The value of the LOGIN-HOST attribute.
    */
   @Override
   public void setLoginHost(character host)
   {
      setCharAttrValue(AttrInfo.LOGIN_HOST, host);
   }

   /**
    * Get the ROLES attribute.
    * 
    * @return   See above.
    */
   @Override
   public character getRoles()
   {
      return getCharAttrValue(AttrInfo.ROLES);
   }

   /**
    * Set the ROLES Attribute.
    * 
    * @param    roles
    *           The value of the ROLES attribute.
    */
   @Override
   public void setRoles(String roles)
   {
      setRoles(new character(roles));
   }

   /**
    * Set the ROLES Attribute.
    * 
    * @param    rs
    *           The value of the ROLES attribute.
    */
   @Override
   public void setRoles(character rs)
   {
      setCharAttrValue(AttrInfo.ROLES, rs);
   }

   /**
    * Get the LOGIN-STATE attribute.
    * 
    * @return   A character value that represents the current state of the 
    *           client-principal object.
    */
   @Override
   public character getLoginState()
   {
      return new character(loginState.state());
   }

   /**
    * Set the LOGIN-STATE attribute. For internal use.
    * 
    * @param state   
    *        A character value that represents the current state of the 
    *        client-principal object.
    */
   public void setLoginState(LoginState state)
   {
      this.loginState = state;
   }

   /**
    * Get the LOGIN-EXPIRATION-TIMESTAMP attribute.
    * 
    * @return   The time stamp specifying when the client-principal object will expire 
    *           client-principal object.
    */
   @Override
   public datetimetz getLoginExpirationTimestamp()
   {
      return (datetimetz)attributes.getOrDefault(AttrInfo.LOGIN_EXPIRATION_TIMESTAMP, 
            Optional.of(new datetimetz())).orElse(new datetimetz());
   }
   
   /**
    * Get the LOGIN-EXPIRATION-TIMESTAMP attribute.
    * 
    * @param    ts
    *           The time stamp specifying when the client-principal object will expire 
    *           client-principal object.
    */
   @Override
   public void setLoginExpirationTimestamp(datetimetz ts)
   {
      // TODO: 4GL seems to include multiple versions of this attribute to serialized form  
      if (wasSealed())
      {
         sealedError(AttrInfo.LOGIN_EXPIRATION_TIMESTAMP.aname());
         return;
      }
      if (ts == null || ts.isUnknown())
      {
         attributes.put(AttrInfo.LOGIN_EXPIRATION_TIMESTAMP, Optional.of(ts));
         return;
      }
      // expiration can have a minimum value of 1/1/1970 00:00:01.000-00:00
      if (ts.getYear() < 1970 || (ts.getYear() == 1970 && ts.getTime() < 1000))
      {
         ts = new datetimetz();
         return;
      }
      // expiration can have no greater precision than seconds
      ts.setTime(1000 * Math.round(ts.getTime() / 1000.0));
      attributes.put(AttrInfo.LOGIN_EXPIRATION_TIMESTAMP, Optional.of(ts));
   }

   /**
    * Get the SEAL-TIMESTAMP attribute.
    * 
    * @return   The time stamp (as a DATETIME-TZ value) for when the client-principal 
    *           object was sealed() in the LOGIN state
    */
   @Override
   public datetimetz getSealTimestamp()
   {
      return (datetimetz)attributes.getOrDefault(AttrInfo.SEAL_TIMESTAMP, 
            Optional.of(new datetimetz())).orElse(new datetimetz());
   }

   /**
    * Get the PRIMARY-PASSPHRASE attribute.
    * Always returns error
    * 
    * @return <code>null</code>
    */
   @Override
   public character getPrimaryPassphrase()
   {
      ErrorManager.recordOrShowError(16617, "PRIMARY-PASSPHRASE", "queryable", type());
      // <attribute> is not a <settable/queryable> attribute for <handle type>.
      return null;
   }
   
   /**
    * Get the PRIMARY-PASSPHRASE attribute value (for internal use).
    * 
    * @return PRIMARY-PASSPHRASE attribute value. 
    */
   @Override
   public character primaryPassphrase()
   {
      return new character((String)attributes.getOrDefault(AttrInfo.PRIMARY_PASSPHRASE, Optional.empty()).orElse(null));
   }

   /**
    * Set the PRIMARY-PASSPHRASE attribute.
    */
   @Override
   public void setPrimaryPassphrase(String passphrase)
   {
      setPrimaryPassphrase(new character(passphrase));
   }

   /**
    * Set the PRIMARY-PASSPHRASE attribute.
    */
   @Override
   public void setPrimaryPassphrase(character passphrase)
   {
      setCharAttrValue(AttrInfo.PRIMARY_PASSPHRASE, passphrase, true);
   }

   /**
    * Get the QUALIFIED-USER-ID attribute.
    */
   @Override
   public character getQualifiedUid()
   {
      String userId = getUserId().value;
      String domainName = getDomainName().value;
      String s;
      if (StringUtils.isEmpty(userId) )
      {
         s = StringUtils.isEmpty(domainName) ? "" : "@" + domainName;
      }
      else 
      {
         s = StringUtils.isEmpty(domainName) ?  userId : userId + "@" + domainName; 
      }
      return new character(s);
   }

   /**
    * Set the QUALIFIED-USER-ID attribute.
    */
   @Override
   public void setQualifiedUid(String uid)
   {
      setQualifiedUid(new character(uid));
   }

   /**
    * Set the QUALIFIED-USER-ID attribute.
    */
   @Override
   public void setQualifiedUid(character uid)
   {
      if (uid == null || uid.isUnknown())
      {   
         attributeUnknownError("QUALIFIED-USER-ID");
         return;
      }
      if (wasSealed())
      {
         sealedError("QUALIFIED-USER-ID");
         return;
      }
      setQualifiedIserId(uid.toStringMessage());
   }

   /**
    * Set the STATE-DETAIL attribute.
    */
   @Override
   public character getStateDetail()
   {
      String state;
      if (sealed == null)
      {
         state = "The CLIENT-PRINCIPAL object has never been sealed"; 
      }
      else
      {
         switch(this.loginState)
         {
         case LOGIN:
            state = "The CLIENT-PRINCIPAL object is currently logged in";
            break;
         case FAILED:
            Optional<Object> reason = attributes.get(AttrInfo.FAIL_REASON);
            if (reason != null && reason.isPresent())
            {
               state = (String)reason.get();
            }
            else
            {
               state = "The CLIENT-PRINCIPAL object authentication failed";
            }
            break;
         case EXPIRED:
            state = "The CLIENT-PRINCIPAL object has expired";
            break;
         case LOGOUT:
            state = "The CLIENT-PRINCIPAL object is currently logged out and unsealed";
            break;
         default: // TODO: the exact 4GL message is not known yet. This one is a placeholder 
            state = "The CLIENT-PRINCIPAL object is in the " + this.loginState.state() + " state.";
         }
      }
      return new character(state);
   }

   /**
    * Get a comma-separated list of logical database names that is stored in the 
    * client-principal object.
    *  
    * @return a comma-separated list of logical database names.
    */
   @LegacyAttribute(name = "DB-LIST")
   public character getDbList()
   {
      // TODO: implement
      return new character();
   }

   /**
    * Set this property in the CLIENT-PRINCIPAL object.
    * 
    * @param    prop
    *           The property's name.
    * @param    val
    *           The property's value.
    *            
    * @return   <code>true</code> if the property could be set.
    */
   @Override
   public logical setProperty(character prop, character val)
   {
      return setProperty(prop.getValue(), val.getValue());
   }

   /**
    * Set this property in the CLIENT-PRINCIPAL object.
    * 
    * @param    prop
    *           The property's name.
    * @param    val
    *           The property's value.
    *            
    * @return   <code>true</code> if the property could be set.
    */
   @Override
   public logical setProperty(character prop, String val)
   {
      return setProperty(prop.getValue(), val);
   }

   /**
    * Set this property in the CLIENT-PRINCIPAL object.
    * 
    * @param    prop
    *           The property's name.
    * @param    val
    *           The property's value.
    *            
    * @return   <code>true</code> if the property could be set.
    */
   @Override
   public logical setProperty(String prop, character val)
   {
      return setProperty(prop, val.getValue());
   }

   /**
    * Set this property in the CLIENT-PRINCIPAL object.
    * 
    * @param    prop
    *           The property's name.
    * @param    val
    *           The property's value.
    *            
    * @return   <code>true</code> if the property could be set.
    */
   @Override
   public logical setProperty(String prop, String val)
   {
      if (prop == null || val == null)
      {
         ErrorManager.recordOrShowError(4065, 
                                        "**The SET-PROPERTY attribute on the CLIENT-PRINCIPAL " +
                                        "widget has invalid arguments", 
                                        true, false, false, false);
         return logical.FALSE;
      }
      if (wasSealed())
      {
         ErrorManager.recordOrShowError(14548, 
                                       "Cannot set attributes or properties for a sealed " +
                                       "CLIENT-PRINCIPAL", 
                                       true, false, false, false);
         
         return logical.FALSE;
      }
      
      if (this.exported)
      {
         return logical.FALSE;
      }
      
      properties.put(prop, val);
      
      return logical.TRUE;
   }

   /**
    * Get the value of the specified property, from the CLIENT-PRINCIPAL object.
    * 
    * @param    prop
    *           The property's name.
    *            
    * @return   The property's value.
    */
   @Override
   public character getProperty(character prop)
   {
      return getProperty(prop.getValue());
   }

   /**
    * Get the value of the specified property, from the CLIENT-PRINCIPAL object.
    * 
    * @param    prop
    *           The property's name.
    *            
    * @return   The property's value.
    */
   @Override
   public character getProperty(String prop)
   {
      if (prop == null)
      {
         ErrorManager.recordOrShowError(4065, 
                                        "**The GET-PROPERTY attribute on the CLIENT-PRINCIPAL " +
                                        "widget has invalid arguments", 
                                        true, false, false, false);
         return new character("");
      }
      
      
      return new character(properties.get(prop));
   }

   /**
    * Get a list of all application-defined properties stored in the client-principal object.
    * 
    * @return   Returns a comma-separated list of all application-defined properties stored in 
    *           the client-principal object
    */
   @Override
   public character listPropertyNames()
   {
      return new character(String.join(",", properties.keySet()));
   }

   /**
    * Export the state of this CLIENT-PRINCIPAL object to a byte representation.
    * 
    * @return   The byte representation as a {@link raw} instance.
    */
   @Override
   public raw exportPrincipal()
   {
      this.exported = true;
      return new raw(serializePrincipal());
   }

   /**
    * Import the state of this CLIENT-PRINCIPAL object from the specified byte representation.
    * 
    * @param    data
    *           The byte representation.
    */
   @Override
   public logical importPrincipal(raw data)
   {
      if (data == null || data.isUnknown() )
      {
         ErrorManager.recordOrShowError(4065, 
               "The IMPORT-PRINCIPAL attribute on the CLIENT-PRINCIPAL widget has invalid " +
               "arguments", 
               false, true, false, false);
         return logical.FALSE; 
      }
      byte[] b = data.getByteArray();
      
      return logical.of(importPrincipal(b));
   }
   
   /**
    * Get the tenant ID associated with the client-principal object.
    *
    * @return   The tenant ID as an integer.
    */
   @Override
   public integer tenantId()
   {
      tenantidContextToken =
         UnimplementedFeature.missing("CLIENTPRINCIPAL:TENANT-ID method is not implemented.",
                                      tenantidContextToken);

      return new integer(0);
   }

   /**
    * Retrieves the tenant ID associated with the user identity sealed in the client-principal
    * and assigned to a connection to the specified multi-tenant database.
    *
    * @param    dbExp
    *           The database expression specifying the multi-tenant database connection.
    *
    * @return   The tenant ID as an integer.
    */
   @Override
   public integer tenantId(character dbExp)
   {
      tenantidContextToken =
         UnimplementedFeature.missing("CLIENTPRINCIPAL:TENANT-ID method is not implemented.",
                                      tenantidContextToken);
      return new integer(0);
   }

   /**
    * Get the tenant name associated with the client-principal object.
    *
    * @return   The time stamp specifying when the client-principal object will expire
    *           client-principal object.
    */
   @Override
   public character tenantName()
   {
      tenantnameContextToken =
         UnimplementedFeature.missing("CLIENTPRINCIPAL:TENANT-NAME method is not implemented.",
                                      tenantnameContextToken);
      return new character("");
   }

   /**
    * Retrieves the tenant name associated with the user identity sealed in the client-principal
    * and assigned to a connection to the specified multi-tenant database.
    *
    * @param    dbExp
    *           The database expression specifying the multi-tenant database connection.
    *
    * @return   The tenant name.
    */
   @Override
   public character tenantName(character dbExp)
   {
      tenantnameContextToken =
         UnimplementedFeature.missing("CLIENTPRINCIPAL:TENANT-NAME method is not implemented.",
                                      tenantnameContextToken);
      return new character("");
   }
   
   /**
    * Seal this CLIENT-PRINCIPAL, using the given domain access code.
    * 
    * @param    domainAccessCode
    *           The access code.
    */
   @Override
   public logical seal(character domainAccessCode)
   {
      return seal(domainAccessCode.getValue());
   }

   /**
    * Seal this CLIENT-PRINCIPAL, using the given domain access code.
    * 
    * @param    domainAccessCode
    *           The access code.
    */
   @Override
   public logical seal(String domainAccessCode)
   {
      if (domainAccessCode == null)
      {
         ErrorManager.recordOrShowError(4065, 
                                        "**The SEAL attribute on the CLIENT-PRINCIPAL " +
                                        "widget has invalid arguments", 
                                        false, false, false, false);
         return logical.FALSE;
      }
      return seal(domainAccessCode, SealCallee.APP);
   }

   /**
    * Seal this CLIENT-PRINCIPAL, using the given domain access code, for internal use.
    * 
    * @param    domainAccessCode
    *           The access code.
    * @param    callee
    *           the method callee.

    * @return   <code>true</code> id successful.
    */
   @Override
   public logical seal(String domainAccessCode, SealCallee callee)
   {
      boolean expired = false;
      datetimetz now = datetimetz.now();
      datetimetz loginExpirationTimestamp = getLoginExpirationTimestamp(); 
      if (loginExpirationTimestamp != null && !loginExpirationTimestamp.isUnknown() &&
          loginExpirationTimestamp.compareTo(now) <= 0)
      {
         expired = true;
         this.sealed = Boolean.FALSE;
         this.loginState = LoginState.EXPIRED;
         this.sealFailure = SealFailure.EXPIRED;
      }
      if (StringUtils.isEmpty(domainAccessCode))
      {
         // current MAC calculation algorithm doesn't accept empty strings
         domainAccessCode = " ";
      }
      if (hideValues || !canBeSealed())
      {
         if (sealFailure == null)
         {
            sealFailure = SealFailure.MISSING_ATTRIBUTES;
         }
         if (callee != SealCallee.SET_DB_CLIENT)
         {
            sealFailed("not all required attributes have been set");
         }
         else {
         }
         return logical.FALSE;
      }

      if (loginState != LoginState.INITIAL)
      {
         if (!getSealTimestamp().isUnknown() || 
             loginState == LoginState.LOGIN || 
             loginState == LoginState.FAILED)
         {
            sealFailure = SealFailure.INVALID_STATE;
            sealFailed("object is in an improper login state");
            return logical.FALSE;
         }
         else if (loginState == LoginState.EXPIRED)
         {
            if (callee == SealCallee.APP)
            {
               sealFailed("object expired");
            }
         }
         else
         {
             finalizeSeal(domainAccessCode);
             sealed = Boolean.TRUE;
             return logical.TRUE;
         }
      }
      
      // no validation for the (domain-name, access-code) pair is done here - this is validated
      // when the auth is attempted via SET-DB-CLIENT
      this.domainAccessCode = domainAccessCode;

      if (expired)
      {
         return logical.FALSE;

      }
      this.loginState = LoginState.LOGIN;
      finalizeSeal(domainAccessCode);

      this.sealed = Boolean.TRUE;
      return logical.TRUE;
   }
   
   /**
    * Get the SEAL operation failure reason.
    *
    * @return SEAL operation failure reason.
    */
   public SealFailure sealFailure()
   {
      return sealFailure;
   }

   /**
    * Indicates that the identity asserted in the unsealed client-principal object cannot be authenticated. 
    * 
    * @return   <code>true</code> id successful
    */
   @Override
   public logical authenticationFailed()
   {
      return authenticationFailed((character)null);
   }

   /**
    * Indicates that the identity asserted in the unsealed client-principal object cannot be authenticated. 
    * 
    * @param    reason
    *           An optional character expression that specifies the reason for the authentication 
    *           failure.
    *           
    * @return   <code>true</code> id successful
    */
   @Override
   public logical authenticationFailed(character reason)
   {
      return authenticationFailed(reason, false);
   }
   /**
    * Indicates that the identity asserted in the unsealed client-principal object cannot be authenticated. 
    * 
    * @param    reason
    *           An optional character expression that specifies the reason for the authentication 
    *           failure.
    * @param    fromSetClient
    *           flag indicating that called from SET-CLIENT.
    *           
    * @return   <code>true</code> id successful.
    */
   @Override
   public logical authenticationFailed(character reason, boolean fromSetClient)
   {
      if (wasSealed() && !fromSetClient)
      {
         ErrorManager.recordOrShowError(new int[] {14549},
               new String[] {
                     "CLIENT-PRINCIPAL:AUTHENTICATION-FAILED error because object is sealed"
               },
               false, false, false, false, false);
         return logical.FALSE;
      }
      if (reason != null && !reason.isUnknown())
      {
         attributes.put(AttrInfo.FAIL_REASON, Optional.of(reason.toStringMessage()));
      }
      this.sealed = Boolean.FALSE;
      this.loginState = LoginState.FAILED;
      // It is unclear what is the MAC key in this case.
      finalizeSeal(" ");
      return logical.TRUE;
   }
   
   /**
    * Indicates that the identity asserted in the unsealed client-principal object cannot be authenticated. 
    * 
    * @param    reason
    *           An optional character expression that specifies the reason for the authentication 
    *           failure.
    * @param    fromSetClient
    *           flag indicating that called from SET-CLIENT.
    *           
    * @return   <code>true</code> id successful
    */
   @Override
   public logical authenticationFailed(String reason, boolean fromSetClient)
   {
      return authenticationFailed(new character(reason), fromSetClient);
   }
   
   /**
    * Indicates that the identity asserted in the unsealed client-principal object cannot be authenticated. 
    * 
    * @param    reason
    *           An optional character expression that specifies the reason for the authentication 
    *           failure.
    *           
    * @return   <code>true</code> id successful.
    */
   @Override
   public logical authenticationFailed(String reason)
   {
      return authenticationFailed(new character(reason));
   }
   
   /**
    * Validates the message authentication code (MAC) generated by the SEAL( ) method to seal a 
    * client-principal object.
    * 
    */
   @Override
   public logical validateSeal()
   {
      return validateSeal(new character());
   }

   /**
    * Validates the message authentication code (MAC) generated by the SEAL( ) method to seal a 
    * client-principal object.
    * 
    * @param    domainAccessCode
    *           The access code.
    */
   @Override
   public logical validateSeal(String domainAccessCode)
   {
      return validateSeal(new character(domainAccessCode));
   }
   
   /**
    * Validates the message authentication code (MAC) generated by the SEAL( ) method to seal a 
    * client-principal object.
    * 
    * @param    domainAccessCode
    *           The access code.
    */
   @Override
   public logical validateSeal(character domainAccessCode)
   {
      switch(loginState)
      {
         case LOGIN:
            // temporarily remove the MAC, so that we can re-compute the MAC with the new password
            Optional<Object> mac = attributes.remove(AttrInfo.MAC);
            try
            {
               if (mac != null && mac.isPresent())
               {
                  String dac;
                  if (domainAccessCode == null || domainAccessCode.isUnknown())
                  {
                     character domain = getCharAttrValue(AttrInfo.DOMAIN_NAME);
                     Optional<String> odac = SecurityPolicyManager.getDomainAccessCode(
                           domain.getValue(),
                           err -> 
                              ErrorManager.recordOrShowError(new int[] {err.code}, 
                                 new String[] {
                                       "CLIENT-PRINCIPAL:VALIDATE-SEAL failed because " + err.message
                                 },
                                 false, false, false, false, false)
                           );
                     if (!odac.isPresent())
                     {
                        return logical.FALSE;
                     }
                     dac = odac.get();
                  }
                  else
                  {
                     dac = domainAccessCode.getValue();
                  }
                  byte[] mac1 = mac(dac);
                  byte[] mac2 = (byte[]) mac.get();
                  if (Arrays.equals(mac1, mac2))
                  {
                     return logical.TRUE;
                  }
               }
            }
            finally
            {
               attributes.put(AttrInfo.MAC, mac);
            }
            ErrorManager.recordOrShowError(new int[] {14541}, 
                  new String[] {
                        "CLIENT-PRINCIPAL:VALIDATE-SEAL failed because keys do not match"
                  },
                  false, false, false, false, false);
            break;
            
         case FAILED:
            ErrorManager.recordOrShowError(new int[] {16369}, 
                  new String[] {
                    "CLIENT-PRINCIPAL:VALIDATE-SEAL failed because - Failed account authentication"
                  },
                  false, false, false, false, false);
            break;
            
         default:
            ErrorManager.recordOrShowError(new int[] {14541},
                  new String[] {
                     "CLIENT-PRINCIPAL:VALIDATE-SEAL failed because object is in an improper state"
                  },
                  false, false, false, false, false);
            break;
      }
      return logical.FALSE;
   }
   
   /**
    * Indicates that the user represented by the sealed() client-principal object (in the LOGIN 
    * state) has logged out of their current user login session.
    */
   @Override
   public logical logout()
   {
      if (isSealed())
      {
         if (loginState == LoginState.LOGIN || loginState == LoginState.SSO)
         {
            loginState = LoginState.LOGOUT;
            attributes.put(AttrInfo.MAC, Optional.of(mac(domainAccessCode)));
            return logical.TRUE;
         }
         ErrorManager.recordOrShowError(new int[] {14552},
               new String[] {
                     "CLIENT-PRINCIPAL:LOGOUT failed because object is in wrong login state"
               },
               false, false, false, false, false);
         return logical.FALSE;
      }
      
      ErrorManager.recordOrShowError(new int[] {14551},
            new String[] {
                  "CLIENT-PRINCIPAL:LOGOUT failed because object is not sealed"
            },
            false, false, false, false, false);
      return logical.FALSE;
   }

   /**
    * Initializes the current object by setting the passed parameters after first validating them.
    * 
    * @param   qualifiedUid
    *          A character expression that evaluates to a fully qualified user ID (user name and 
    *          domain name delimited by the '@' character)
    * 
    * @return  <code>true</code> on success
    */
   @Override
   public logical initialize(character qualifiedUid)
   {
      return init(qualifiedUid, new character(), new datetimetz(), null);
   }

   /**
    * Initializes the current object by setting the passed parameters after first validating them.
    * 
    * @param   qualifiedUid
    *          A character expression that evaluates to a fully qualified user ID (user name and 
    *          domain name delimited by the '@' character)
    * @param   sessionId
    *          An optional character expression that evaluates to the user's application login session ID.
    *
    * @return  <code>true</code> on success
    */
   @Override
   public logical initialize(character qualifiedUid, character sessionId)
   {
      return init(qualifiedUid, sessionId, new datetimetz(), null);
   }

   /**
    * Initializes the current object by setting the passed parameters after first validating them.
    * 
    * @param   qualifiedUid
    *          A character expression that evaluates to a fully qualified user ID (user name and 
    *          domain name delimited by the '@' character)
    * @param   sessionId
    *          An optional character expression that evaluates to the user's application login 
    *          session ID.
    * @param   expiration
    *          An optional DATETIME-TZ expression that evaluates to a date and time value that 
    *          specifies the expiration of thee client-principal user credentials
    *
    * @return  <code>true</code> on success
    */
   @Override
   public logical initialize(character qualifiedUid, character sessionId, BaseDataType expiration)
   {
      return init(qualifiedUid, sessionId, expiration, null);
   }

   /**
    * Initializes the current object by setting the passed parameters after first validating them.
    * 
    * @param   qualifiedUid
    *          A character expression that evaluates to a fully qualified user ID (user name and 
    *          domain name delimited by the '@' character)
    * @param   sessionId
    *          An optional character expression that evaluates to the user's application login 
    *          session ID.
    * @param   expiration
    *          An optional DATETIME-TZ expression that evaluates to a date and time value that 
    *          specifies the expiration of thee client-principal user credentials
    * @param   primaryPassphrase
    *          An optional character expression that evaluates to the cleartext or encrypted value 
    *          of the user's account password.
    *
    * @return  <code>true</code> on success
    */
   @Override
   public logical initialize(character qualifiedUid, character sessionId, 
                             BaseDataType expiration, character primaryPassphrase)
   {
      if (primaryPassphrase == null || primaryPassphrase.isUnknown())
      {
         ErrorManager.recordOrThrowError(9178, 
               "Invalid primary-passphrase used in INITIALIZE method",false, false);
         return logical.FALSE;
      }
      return init(qualifiedUid, sessionId, expiration, primaryPassphrase.toStringMessage());
   }

   /**
    * Initializes the current object by setting the passed parameters after first validating them.
    *
    * @param   qualifiedUid
    *          A character expression that evaluates to a fully qualified user ID (user name and 
    *          domain name delimited by the '@' character)
    * @param   sessionId
    *          An optional character expression that evaluates to the user's application login 
    *          session ID.
    * @param   expiration
    *          An optional DATETIME-TZ expression that evaluates to a date and time value that 
    *          specifies the expiration of thee client-principal user credentials
    * @param   primaryPassphrase
    *          An optional character expression that evaluates to the cleartext or encrypted value 
    *          of the user's account password.
    *
    * @return  <code>true</code> on success
    */
   private logical init(character qualifiedUid,
                        character sessionId,
                        BaseDataType expiration,
                        String primaryPassphrase)
   {
      if (qualifiedUid == null || qualifiedUid.isUnknown())
      {
         ErrorManager.recordOrThrowError(15918, 
               "Required parameter for INITIALIZE was passed the Unknown value", 
               false, false);
         return logical.FALSE;
      }
      if ((expiration != null) && !expiration.isUnknown() && !(expiration instanceof datetimetz))
      {
         ErrorManager.recordOrThrowError(9178, 
               "Invalid expiration used in INITIALIZE method", 
               false, false);
         return logical.FALSE;
      }
      if (sessionId != null && !sessionId.isUnknown() && 
          StringUtils.isEmpty(sessionId.toStringMessage()))
      {
         ErrorManager.recordOrThrowError(new int[] {14553, 9178},
                  new String[] {
                      "Required attribute can not be set to the empty string",
                      "Invalid session-id used in INITIALIZE method.", 
                  },
                  false, false, false);
            return logical.FALSE;
      }

      this.loginState = LoginState.INITIAL;
      reset();
      
      setQualifiedIserId(qualifiedUid.toStringMessage());
      setSessionId(sessionId == null || sessionId.isUnknown() ? 
                                       generatedSessionId :
                                       sessionId.toStringMessage());
      this.magic = 0xf;
      if (expiration != null && !expiration.isUnknown())
      {
         this.magic |= 0x24000;
         setLoginExpirationTimestamp((datetimetz)expiration);
      }

      if (primaryPassphrase != null)
      {
         this.magic |= 0x80;
         setPrimaryPassphrase(primaryPassphrase);
      }
      addHiddenAttrs();
      return logical.TRUE;
   }
   
   /** Reset state. */
   private void reset()
   {
      this.sealed = null;
      this.exported = false;
      this.sealedOnImport = false;
      this.hideValues = false;
      this.magic = 2;
      attributes.clear();
      properties.clear();
   }
   
   /**
    * Create a copy of this CLIENT-PRINCIPAL instance.
    * 
    * <p>Relies on {@link #exportPrincipal} and {@link #importPrincipal} to transfer the state 
    * from this instance to the copy.
    * 
    * @return   A copy of this instance.
    */
   @Override
   public ClientPrincipal clone() 
   {
      raw bytes = exportPrincipal();
      
      ClientPrincipal copy = new ClientPrincipal();
      copy.importPrincipal(bytes);
      
      return copy;
   }
   
   /**
    * Validate CLIENT-PRINCIPAL against domain password.
    * 
    * @param where
    *        subsystem name
    * @param dac
    *        domain access code
    * @param password
    *        domain password
    * @param callee
    *        where the method is called from
    *
    * @return <code>true</code> if successful
    */
   @Override
   public logical validatePassword(String where, String dac, String password, SealCallee callee)
   {
      character pwd = primaryPassphrase();
      if (password == null)
      {
         if (callee != SealCallee.APP && pwd.isUnknown())
         {
            authenticationFailed(16373, where, "Cannot access user account information");
            authenticationFailed("Cannot access the account authentication information");
         }
         else 
         {
            authenticationFailed(16374, where, "User account does not exist");
            authenticationFailed("The user account does not exist");
         }
         return logical.FALSE;
      }
      logical rc = trySeal(where, dac, callee);
      if (rc.booleanValue())
      {
         if (!SecurityOps.encode(pwd).toStringMessage().equals(password))
         {
            authenticationFailed(16369, where, "Failed account authentication");
            authenticationFailed("The user failed account authentication", true);
            return logical.FALSE;
         }
      }
      return rc;
   }

   /**
    * Validate CLIENT-PRINCIPAL against domain access code.
    * 
    * @param where
    *        subsystem name
    * @param dac
    *        domain access code
    * @param callee
    *        where the method is called from
    *
    * @return <code>true</code> if successful
    */
   @Override
   public logical validateDomainAccessCode(String where, String dac, SealCallee callee)
   {
      logical rc = trySeal(where, dac, callee);
      if (rc.booleanValue())
      {
         if (StringUtils.isEmpty(dac))
         {
            // current MAC calculation algorithm doesn't accept empty strings
            dac = " ";
         }
         if ((getDomainAccessCode() != null && !dac.equals(getDomainAccessCode())) ||
             // imported CP
             (getDomainAccessCode() == null && !validateDomainAccessCode(dac)))
         {
            // TODO: check the error message
            if (callee != SealCallee.SET_CLIENT)
            {
               authenticationFailed(16369, where, "Failed account authentication");
               authenticationFailed("The user failed account authentication", true);
            }
            return logical.FALSE;
         }
      }
      return rc;
   }

   /**
    * Validate domain access code for a sealed principal
    * 
    * @param dac
    *        domain access code
    *        
    * @return <code>true</code> if successful
    */
   public boolean validateDomainAccessCode(String dac)
   {
      Optional<Object> omac = attributes.get(AttrInfo.MAC);
      if (omac == null || !omac.isPresent())
      {
         throw new IllegalStateException("MC attribute is set for a sealed principal");
      }
      byte[] mac = (byte[])omac.get();
      return Arrays.equals(mac, mac(dac));
   }
   /**
    * Check if this CLIENT-PRINCIPIAL has been sealed.
    * 
    * @return   The sealed state.
    */
   @Override
   public boolean isSealed()
   {
      return Boolean.TRUE.equals(sealed);
   }
   
   /**
    * Import the state of this CLIENT-PRINCIPAL object from the specified byte representation.
    * 
    * @param    data
    *           The byte representation.
    */
   private boolean importPrincipal(byte[] data)
   {
      if (wasSealed())
      {
         ErrorManager.recordOrShowError(new int[] {14543}, 
               new String[] {
                 "CLIENT-PRINCIPAL:IMPORT failed because object already sealed" 
               },
               false, false, false, false, false);
         return false; 
      }
      reset();
      InputStream bis = new ByteArrayInputStream(data);
      CountingInputStream counter = new CountingInputStream(bis);
      int p;
      try(DataInputStream dis = new DataInputStream(counter))
      {
         p = counter.getCount();
         if (dis.readShort() != 0x0030)
         {
            throw new RuntimeException("Invalid stream at " + p);
         }
         p = counter.getCount();
         if (dis.readShort() != AttrInfo.LOGIN_STATE.type().code())
         {
            throw new RuntimeException("Invalid stream at " + p);
         }
         p = counter.getCount();
         if (dis.readShort() != AttrInfo.LOGIN_STATE.id())
         {
            throw new RuntimeException("Invalid stream at " + p);
         }
         p = counter.getCount();
         if (dis.readShort() != 2)
         {
            throw new RuntimeException("Invalid stream at " + p);
         }
         this.loginState = LoginState.valueOf(dis.readShort());
         
         p = counter.getCount();
         for (; p < data.length; p = counter.getCount())
         {
            AttrType atype = AttrType.valueOf(dis.readShort());
            AttrInfo ai = AttrInfo.valueOf(atype, dis.readShort());
            int alen = dis.readInt();
            byte[] bb;
            switch (atype)
            {
               case CHAR:
                  bb = new byte[alen];
                  dis.read(bb);
                  setCharAttrValue(ai, new character(new String(bb, 0, alen -1, StandardCharsets.UTF_8)));
                  break;
                  
               case LONG:
                  if (alen != 8)
                  {
                     throw new RuntimeException("Invalid LONG attribute length: " + ai.aname());
                  }
                  long value = dis.readLong();
                  attributes.put(ai, Optional.ofNullable(value));
                  if (ai == AttrInfo.ATTR_001C)
                  {
                     magic = value;
                  }
                  break;
                  
               case BYTES:
                  bb = new byte[alen];
                  dis.read(bb);
                  attributes.put(ai, Optional.ofNullable(bb));
                  break;
                  
               case DATETIME_TZ:
                  readTimestamp(ai, dis);
                  break;
                  
               case LIST:
                  if (ai == AttrInfo.PROPS)
                  {
                     bb = new byte[alen];
                     dis.read(bb);
                     readProperties(bb);
                     // add a placeholder to the 'attributes' map (for a correct position in the export). 
                     // The actual access to the properties
                     // is implemented using 'properties' map.
                     attributes.put(AttrInfo.PROPS, Optional.of(new ArrayList<String>()));
                  }
                  else 
                  {
                     // TODO: implement DB-LIST deserialization
                     attributes.put(AttrInfo.DB_LIST, Optional.of(new ArrayList<String>()));
                  }
                  break;
                  
               default:
                  throw new RuntimeException("Unexpected attribute: " + ai.aname());
            }
         }
      }
      catch (Exception e)
      {
         ErrorManager.recordOrShowError(new int[] {16366}, 
               new String[] {
                 "CLIENT-PRINCIPAL:IMPORT failed because - Internal Authentication subsystem error [28]" 
               },
               false, false, false, false, false);
         return false;
      }
      
      LoginState prevState = this.loginState;
      this.hideValues = (!canBeSealed() || getDomainAccessCode() == null || 
                         prevState != LoginState.INITIAL) && (magic & 0xff) != 0x8f;
      datetimetz now = datetimetz.now();
      datetimetz loginExpirationTimestamp = getLoginExpirationTimestamp(); 
      if ( this.loginState == LoginState.INITIAL &&
            loginExpirationTimestamp != null && !loginExpirationTimestamp.isUnknown() &&
            loginExpirationTimestamp.compareTo(now) <= 0)
      {
         this.loginState = LoginState.EXPIRED;
         this.sealed = Boolean.FALSE;
         this.sealedOnImport = true;
         if (canBeSealed() && (magic & 0xff) == 0x8fL)
         {
            finalizeSeal(" ");
         }
      }
      
      switch (this.loginState)
      {
         case LOGIN:
         case SSO:
            this.sealed = Boolean.TRUE;
            break;
         case EXPIRED:
         case LOGOUT:
         case FAILED:
            this.sealed = Boolean.FALSE;
            break;
         default:
            break;
      }
      this.hideValues &= (sealed == null) || properties.isEmpty(); //|= prevState == this.loginState;
      return true;
   }

   /**
    * Check if object was ever sealed
    * 
    * @return <code>true</code> if object was ever sealed
    */
   private boolean wasSealed()
   {
      return sealed != null;
   }
   
   /**
    * Get this object's domain access code (valid only if it was {@link #seal sealed()}.
    * 
    * @return   The {@link #domainAccessCode domain access code}.
    */
   String getDomainAccessCode()
   {
      return domainAccessCode;
   }
   
   /**
    * Report CLIENT-PRINCIPAL validation error
    * 
    * @param code
    *        error code
    * @param where
    *        subsystem name
    * @param reason
    *        reason
    */
   public static void validationFailed(int code, String where, String reason)
   {
      ErrorManager.recordOrShowError(new int[] {code}, 
            new String[] {
                  String.format("CLIENT-PRINCIPAL validation failed in %s because - %s",
                        where, reason) 
            },
            false, false, false, false, false);
   }

   /**
    * Report CLIENT-PRINCIPAL authentication error
    * 
    * @param code
    *        error code.
    * @param where
    *        subsystem name.
    * @param reason
    *        reason.
    */
   public static void authenticationFailed(int code, String where, String reason)
   {
      ErrorManager.recordOrShowError(new int[] {code, 13691}, 
            new String[] {
                  String.format("CLIENT-PRINCIPAL authentication failed in %s because - %s",
                        where, reason),
                  String.format("Failed to set %s user id.", where)
            },
            false, false, false, false, false);
   }

   /**
    * Raise or log an error for the specified attribute, as it can't be set to unknown value.
    */
   private static void argumentUnknownError()
   {
      String[] texts = 
      {
         "Required parameter for INITIALIZE was passed the Unknown value",
      };
      int[] nums = { 15918 };
      ErrorManager.recordOrThrowError(nums, texts, false, true);
   }

   /**
    * Raise or log an error for the specified attribute, as it can't be set to unknown value.
    * 
    * @param    attr
    *           The attribute being accessed.
    */
   private static void attributeUnknownError(String attr)
   {
      String[] texts = 
      {
         "Required attribute can not be set to the Unknown value",
         "Unable to set attribute " + attr + " in widget  of type CLIENT-PRINCIPAL."
      };
      int[] nums = { 14553, 3131 };
      ErrorManager.recordOrThrowError(nums, texts, false, false, false);
   }
   
   /**
    * Raise or log an error for the specified attribute, as it can't be set to unknown value.
    * 
    * @param    attr
    *           The attribute being accessed.
    */
   private static void attributeEmptyError(String attr)
   {
      String[] texts = 
      {
         "Required attribute can not be set to the empty string",
         "Unable to set attribute " + attr + " in widget  of type CLIENT-PRINCIPAL."
      };
      int[] nums = { 14553, 3131 };
      ErrorManager.recordOrThrowError(nums, texts, false, false, false);
   }

   /**
    * Raise or log an error for the specified attribute, as the object is sealed().
    * 
    * @param    attr
    *           The attribute being accessed.
    */
   private void sealedError(String attr)
   {
      String[] texts = 
      {
         isSealed() ? "Cannot set attributes or properties for a sealed CLIENT-PRINCIPAL"
                  : String.format(
                        "Cannot set attributes or properties in LOGIN-STATE: %s", loginState  
                     ),
         "Unable to set attribute " + attr + " in widget  of type CLIENT-PRINCIPAL."
      };
      int[] nums = { isSealed() ? 14548 : 14547, 3131 };
      ErrorManager.recordOrThrowError(nums, texts, false, false, false);
   }
   
   /**
    * Report SEAL failure.
    * 
    * @param reason
    *        fail reason.
    */
   private void sealFailed(String reason)
   {
      ErrorManager.recordOrShowError(new int[] {14540},
            new String[] {
                  "CLIENT-PRINCIPAL:SEAL failed because " + reason
            },
            false, false, false, false, false);
      
   }

   /** Add SEAL-TIMESTAMP and calculate MAC. */
   private void finalizeSeal(String macPwd)
   {
      attributes.put(AttrInfo.SEAL_TIMESTAMP, Optional.of(datetimetz.now()));
      attributes.put(AttrInfo.MAC, Optional.of(mac(macPwd)));
   }
   
   /**
    * Try to seal CLIENT-PRINCIPAL if unsealed
    * 
    * @param where
    *        subsystem name
    * @param dac
    *        domain access code
    * @param callee
    *        where the method is called from
    *        
    * @return <code>true</code> if successful
    */
   private logical trySeal(String where, String dac,  SealCallee callee)
   {
      logical rc = new logical(true);
      if (this.loginState == LoginState.INITIAL)
      {
         // It is unclear which access-code (MAC key) is used in this case 
         rc = seal(dac, callee);
         if (this.loginState == LoginState.EXPIRED)
         {
            authenticationFailed(16376, where, "User authentication expired");
            return rc;
         }
         if (this.loginState == LoginState.FAILED)
         {
            return rc;
         }
      }
      return rc;
   }

   /**
    * Calculate Message Authentication Code (MAC) of the CP.
    *  
    * @param macPwd
    *        MAC password.
    * 
    * @return MAC value.
    */
   private byte[] mac(String macPwd)
   {
      String algorithm =  "HmacMD5";
      if (StringUtils.isEmpty(macPwd))
      {
         macPwd = " ";
      }
      byte[] keyData = macPwd.getBytes(StandardCharsets.UTF_8);
      SecretKeySpec key = new SecretKeySpec(keyData, algorithm);
      Mac mac;
      try
      {
         mac = Mac.getInstance(algorithm);
         mac.init(key);
      } 
      catch (NoSuchAlgorithmException | InvalidKeyException e) // should never happen
      {
         throw new RuntimeException(e);
      }
      
      Consumer<byte[]> consumer =  b -> {
         try 
         {
            mac.update(b);
         }
         catch(IllegalStateException e) // should never happen
         {
            throw new RuntimeException(e); 
         }
      };
      
      processPrincipal(ai -> !NOT_FOR_MAC.contains(ai), consumer);
      return mac.doFinal();
   }

   /** 
    * Generate SESSION-ID.
    * 
    * @return random session ID (22 chars base64 encoded random UUID) 
   */
   private static String generateSessionID()
   {
      UUID uuid = UUID.randomUUID();
      byte[] b = ByteBuffer.allocate(16)
            .putLong(uuid.getMostSignificantBits())
            .putLong(uuid.getLeastSignificantBits())
            .array();
      return Base64.getEncoder().encodeToString(b).substring(0, 22);
   }

   /**
    * Split QUALIFIED-USER-ID attribute value and set USER-ID and DOMAIN-NAME.
    * 
    * @param quid
    *        QUALIFIED-USER-ID attribute value
    */
   private void setQualifiedIserId(String quid)
   {
      int p = quid.indexOf('@');
      if (p < 0)
      {
         setUserId(quid);
         setDomainName("");
      }
      else
      {
         setUserId(quid.substring(0, p));
         setDomainName(quid.substring(p + 1));
      }
   }
   
   /**
    * Return value of the CHARACTER attribute.
    * 
    * @param ai
    *        attribute descriptor.
    *        
    * @return value of the CHARACTER attribute.
    */
   private character getCharAttrValue(AttrInfo ai)
   {
      return hideValues ? new character("") :
            new character((String)attributes.getOrDefault(ai, Optional.of("")).orElse(null));
   }
   
   /**
    * Set a new value of the optional CHARACTER attribute.
    * 
    * @param ai
    *        attribute descriptor.
    * @param value
    *        a new value of the attribute.
    *        
    */
   private  void setCharAttrValue(AttrInfo ai, character value)
   {
      setCharAttrValue(ai, value, false);
   }
   
   /**
    * Set a new value of the optional CHARACTER attribute.
    * 
    * @param ai
    *        attribute descriptor.
    * @param value
    *        a new value of the attribute.
    * @param mandatory
    *        boolean flag indicating that the attribute value cannot by null or UNKNOWN. 
    */
   private  void setCharAttrValue(AttrInfo ai, character value, boolean mandatory)
   {
      if (mandatory && (value == null || value.isUnknown()))
      {
         attributeUnknownError(ai.aname());
         return;
      }
      if (wasSealed())
      {
         sealedError(ai.aname());
         return;
      }
      
      if (!mandatory && (value == null || value.isUnknown()))
      {
         Optional<?> avalue = attributes.get(ai);
         if (avalue == null)
         {
            attributes.put(ai, Optional.empty());
         }
         else 
         {
            // ignore
         }
      }
      else // add new value to the end of the list 
      {
         if (attributes.remove(ai) != null && MANDATORY.contains(ai) )
         {
            this.hideValues = this.exported;
         }
         attributes.put(ai, Optional.of(value.toStringMessage()));
      }
   }

   /**
    * Serialize CP.
    * 
    * @return byte array containing serialized data. 
    */
   private byte[] serializePrincipal()
   {
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      DataOutputStream dos = new DataOutputStream(bos);
      Consumer<byte[]> consumer =  b -> 
      {
         try 
         {
            dos.write(b);
         }
         catch(IOException e) // should never happen
         {
            throw new RuntimeException(e); 
         }
      };
      processPrincipal(ai -> true, consumer);
      try
      {
         dos.flush();
         dos.close();
         return bos.toByteArray(); 
      } 
      catch (IOException e) // should never happen
      {
         throw new RuntimeException(e); 
      }
   }

   /**
    * Serialize CP attributes (including "hidden") one by one
    * and submit results to a consumer.
    *  
    * @param consumer
    *        <code>Consumer</code> instance to process serialized attributes.
    * @param filter
    *        <code>Predicate</code> instance to filter attributes  
    */
   private void processPrincipal(
         Predicate<AttrInfo> filter,
         Consumer<byte[]> consumer)
   {
      consumer.accept(new byte[] {0x00, 0x30});
      processAttribute(AttrInfo.LOGIN_STATE, loginState.code, consumer);
      attributes.entrySet().stream().filter(e -> filter.test(e.getKey()))
         .forEach(e -> processAttribute(e.getKey(), e.getValue().orElse(null), consumer));
   }

   /**
    * Serialize CP attributes
    * and submit result to a consumer.
    * 
    * @param ai 
    *        attribute descriptor.
    * @param value
    *        attribute value.
    * @param dest
    *        <code>Consumer</code> instance to process serialized attributes.
    */
   private void processAttribute(AttrInfo ai, Object value, Consumer<byte[]> dest)
   {
      if (value == null)
      {
         return;
      }
      byte[] data;
      dest.accept(toBytes(ai.type().code()));
      dest.accept(toBytes(ai.id()));
      switch(ai.type())
      {
      case CHAR:
         data = ((String)value).getBytes(StandardCharsets.UTF_8);
         dest.accept(toBytes(data.length + 1));
         if (ai.hideOnExport  && 
               (isSealed() || this.loginState == LoginState.FAILED ||  
               (sealedOnImport && (magic & 0xff) != 2)))
         {
            Arrays.fill(data, (byte)'*');
            dest.accept(data);
            dest.accept(new byte[] {(byte)'*'});
         }
         else
         {
            dest.accept(data);
            dest.accept(new byte[1]);
         }
         break;
      case DATETIME_TZ:
         datetimetz ts = (datetimetz)value;
         AttrInfo tz = ai == AttrInfo.SEAL_TIMESTAMP ? 
               AttrInfo.SEAL_TIMESTAMP_TZ :
               AttrInfo.LOGIN_EXPIRATION_TIMESTAMP_TZ;
         dest.accept(toBytes(8));
         if (!ts.isUnknown())
         {
            // Need ms since epoch, but getAbsoluteTimeOffset() returns something different
            // Is there more straightforward way?
            OffsetDateTime offsetDateTime = OffsetDateTime.parse(
                  ts.getIsoDate(),
                  DateTimeFormatter.ISO_OFFSET_DATE_TIME
            );
            long epoch = Date.from(Instant.from(offsetDateTime)).getTime() / 1000;
            dest.accept(toBytes(epoch));
            processAttribute(tz, ts.getTimeZoneOffset().longValue(), dest);
         }
         else 
         {
            dest.accept(toBytes(0L));
            processAttribute(tz, 0L, dest);
         }
         break;
      case TYPE_000C:
         dest.accept(toBytes((short)2));
         dest.accept(toBytes((short)value));
         break;
      case BYTES:
         data = (byte[])value;
         dest.accept(toBytes(data.length));
         dest.accept(data);
         break;
      case LONG:
         dest.accept(toBytes(8));
         dest.accept(toBytes((long)value));
         break;
      case LIST:
         if (ai == AttrInfo.PROPS)
         {
            data = serializeProps();
            dest.accept(toBytes(data.length));
            dest.accept(data);
         }
         else
         {
            // TODO: implement
            dest.accept(toBytes(0));
         }
         break;
      }
   }

   /**
    * Serialize CP properties.
    * 
    * @return byte array containing serialized data.
    */
   private byte[] serializeProps()
   {
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      DataOutputStream dos = new DataOutputStream(bos);
      
      try
      {
         for (Map.Entry<String, String> p: properties.entrySet())
         {
            dos.writeInt(0x000a00014);
            byte[] b1 = p.getKey().getBytes(StandardCharsets.UTF_8);
            byte[] b2 = p.getValue().getBytes(StandardCharsets.UTF_8);
            dos.writeInt(b1.length + b2.length + 2);
            dos.write(b1);
            dos.writeByte(0);
            dos.write(b2);
            dos.writeByte(0);
         }
         dos.flush();
         dos.close();
      } 
      catch (IOException e) // should never happen
      {
         throw new RuntimeException(e);
      }
      return bos.toByteArray();
   }

   /**
    * Convert short to byte[].
    * 
    * @param v
    *        value to be converted.
    *        
    * @return
    *        converted value, MSB first.
    */
   private static byte[] toBytes(short v)
   {
      return ByteBuffer.allocate(2).putShort(v).array();
   }

   /**
    * Convert int to byte[].
    * 
    * @param v
    *        value to be converted.
    *        
    * @return
    *        converted value, MSB first.
    */
   private static byte[] toBytes(int v)
   {
      return ByteBuffer.allocate(4).putInt(v).array();
   }

   /**
    * Convert long to byte[].
    * 
    * @param v
    *        value to be converted.
    *        
    * @return
    *        converted value, MSB first.
    */
   private static byte[] toBytes(long v)
   {
      return ByteBuffer.allocate(8).putLong(v).array();
   }

   /**
    * Add standard "hidden" attributes.
    */
   private void addHiddenAttrs()
   {
      attributes.put(AttrInfo.PROPS, Optional.of(properties));
      attributes.put(AttrInfo.ATTR_001C, Optional.of(magic));
      attributes.put(AttrInfo.DB_LIST, Optional.of(new ArrayList<String>()));
   }
   
   /**
    * Read properties.
    *
    * @param   data
    *          serialized properties data.
    *
    * @throws  java.io.IOException
    *          If any issue is encountered while accessing the input stream. 
    */
   private int readProperties(byte[] data) 
   throws IOException
   {
      int nprops = 0;
      InputStream bis = new ByteArrayInputStream(data);
      CountingInputStream counter = new CountingInputStream(bis);
      try (DataInputStream dis = new DataInputStream(counter))
      {
         while (counter.getByteCount() < data.length)
         {
            AttrType atype = AttrType.valueOf(dis.readShort());
            AttrInfo ai = AttrInfo.valueOf(atype, dis.readShort());
            if (ai != AttrInfo.PROPERTY)
            {
               throw new RuntimeException("Invalid property tag: " + ai.aname());
            }
            int len = dis.readInt();
            byte[] pdata = new byte[len];
            dis.read(pdata);
            int p = 0;
            for (; p < len && pdata[p] != 0; p++)
            {
               // skip
            }
            if (p == 0 || p >= len)
            {
               throw new RuntimeException("Invalid property data: " + p);
            }
            String key = new String(pdata, 0, p, StandardCharsets.UTF_8);
            String val = new String(pdata, p + 1, len - p - 2, StandardCharsets.UTF_8);
            setProperty(key, val);
            nprops++;
         }
         return nprops;
      }
   }
   
   /**
    * Read DATETIME-TZ attribute.
    *
    * @param   ai
    *          attribute descriptor.
    * @param   dis
    *          input stream.
    *
    * @throws  java.io.IOException
    *          If any issue is encountered while accessing the input stream. 
    */
   private void readTimestamp(AttrInfo ai, DataInputStream dis)
   throws IOException
   {
      long epoch = dis.readLong();

      AttrType tztype = AttrType.valueOf(dis.readShort());
      AttrInfo tzinfo = AttrInfo.valueOf(tztype, dis.readShort());
      if ((ai == AttrInfo.SEAL_TIMESTAMP && tzinfo != AttrInfo.SEAL_TIMESTAMP_TZ) || 
            (ai == AttrInfo.LOGIN_EXPIRATION_TIMESTAMP && 
            tzinfo != AttrInfo.LOGIN_EXPIRATION_TIMESTAMP_TZ))
      {
         throw new RuntimeException("Invalid timezone attribute");
      }
      if (dis.readInt() != 8)
      {
         throw new RuntimeException("Invalid timezone attribute length");
      }
      long offset = dis.readLong();

      datetimetz ts = new datetimetz(new Date(epoch * 1000), (int) offset);

      if (epoch > 0)
      {
         attributes.put(ai, Optional.of(ts));
      }
   }

   /**
    * Check if all mandatory attributes are set.
    * 
    * @return <code>true</code> if all mandatory attributes are set.
    */
   private boolean canBeSealed()
   {
      return //!hideValues && 
               attributes.get(AttrInfo.USER_ID) != null &&  
               attributes.get(AttrInfo.DOMAIN_NAME) != null && 
               attributes.get(AttrInfo.SESSION_ID) != null;
   }
   /** attributes' types. */
   private static enum AttrType
   {
      CHAR(0x00a0),
      DATETIME_TZ(0x0090),
      // data types with not yet known semantics
      TYPE_000C(0x000C), // used for LOGIN-STATE only
      BYTES(0x000B), // ? 
      LIST(0x00c0),  // ?
      LONG(0x00d0);
      
      /**
       * Get AttrType by code.
       * 
       * @param code
       *        attribute code.
       *        
       * @return AttrType with such code.
       */
      public static AttrType valueOf(short code)
      {
         return Arrays.stream(values()).filter(at -> at.code == code)
               .findFirst()
               .orElseThrow(() -> new IllegalArgumentException("Unknown attr. type: " + code));
      }

      /** type code. */
      private final short code;
      
      /**
       * Constructor.
       * 
       * @param code
       *        type code.
       */
      private AttrType(int code)
      {
         this.code = (short)code;
      }
      
      /**
       * Return type code.
       * 
       * @return type code.
       */
      public short code()
      {
         return code;
      }
   }
   
   /** CP login states. */
   public static enum LoginState
   {
      INITIAL(0x00),
      LOGIN(0x0b),
      SSO,
      LOGOUT(0x0a),
      EXPIRED(0x05),
      FAILED(0x02),
      NO_LOGIN,
      NO_ACCESS,
      REVOKED,
      DISABLED,
      LOCKED;
      
      /**
       * Get LoginState by code.
       * 
       * @param code
       *        LoginState code.
       *        
       * @return LoginState with such code.
       */
      public static LoginState valueOf(short code)
      {
         return Arrays.stream(values()).filter(s -> s.code == code)
               .findFirst()
               .orElseThrow(() -> new IllegalArgumentException("Unknown LOGIN-STATE: " + code));
      }
      
      /** state code. */
      private final short code;
      
      /**
       * No-arg constructor.
       */
      private LoginState()
      {
         this((short)-1);
      }

      /**
       * Constructor.
       * 
       * @param code
       *        state code.
       */
      private LoginState(int code)
      {
         this.code = (short)code;
      }

      /**
       * Return state code.
       * 
       * @return state code.
       */
      public short code()
      {
         return code;
      }

      /**
       * Return state name.
       * 
       * @return state name.
       */
      public String state()
      {
         return name().replaceAll("_", "-");
      }
      
   }

   /** attributes' descriptor. */
   private static enum AttrInfo
   {
      LOGIN_STATE(AttrType.TYPE_000C, 0, "LOGIN-STATE"),
      DOMAIN_NAME(AttrType.CHAR, 1, "DOMAIN-NAME"),
      USER_ID(AttrType.CHAR, 2, "USER-ID"),
      CLIENT_TTY(AttrType.CHAR, 3, "CLIENT-TTY"),
      LOGIN_HOST(AttrType.CHAR, 4, "LOGIN-HOST"),
      PRIMARY_PASSPHRASE(AttrType.CHAR, 6, "PRIMARY-PASSPHRASE", true),
      AUDIT_EVENT_CONTEXT(AttrType.CHAR, 0xa, "AUDIT-EVENT-CONTEXT"),
      SEAL_TIMESTAMP(AttrType.DATETIME_TZ, 0xb, "SEAL-TIMESTAMP"),
      SEAL_TIMESTAMP_TZ(AttrType.LONG, 0x17, "SEAL-TIMESTAMP-TZ"),
      ROLES(AttrType.CHAR, 0xd, "ROLES"),
      DOMAIN_TYPE(AttrType.CHAR, 0xe, "DOMAIN_TYPE"),
      SESSION_ID(AttrType.CHAR, 0xf, "SESSION-ID"),
      LOGIN_EXPIRATION_TIMESTAMP(AttrType.DATETIME_TZ, 0x10, "LOGIN-EXPIRATION-TIMESTAMP"),
      LOGIN_EXPIRATION_TIMESTAMP_TZ(AttrType.LONG, 0x1b, "LOGIN-EXPIRATION-TIMESTAMP-TZ"),
      CLIENT_WORKSTATION(AttrType.CHAR, 0x18, "CLIENT-WORKSTATION"),
      DOMAIN_DESCRIPTION(AttrType.CHAR, 0x19, "DOMAIN-DESCRIPTION"),
      // "hidden" attributes with not yet known/undocumented semantics 
      PROPS(AttrType.LIST, 0x14, "PROPERTIES"), 
      PROPERTY(AttrType.CHAR, 0x14, "PROPERTY"), 
      MAC(AttrType.BYTES, 0x15, "MAC"),             // 16 bytes (MAC ?)
      DB_LIST(AttrType.LIST, 0x16, "DB-LIST"),          // (?)
      FAIL_REASON(AttrType.CHAR, 0x1a, "FAIL-REASON"),  // fail reason (?)
      ATTR_001C(AttrType.LONG, 0x1c, "ATTR_001C"); // 8 bytes (initialization method ?)
      
      /**
       * Get AttrInfo by type and id.
       * 
       * @param atype
       *        attribute code.
       * @param id        
       *        attribute id.
       * @return AttrInfo with such type and id.
       */
      public static AttrInfo valueOf(AttrType atype, short id)
      {
         return Arrays.stream(values()).filter(ai -> ai.type == atype && ai.id == id)
               .findFirst()
               .orElseThrow(() -> new IllegalArgumentException(
                     "Unknown attr. data: " + atype.code() + ":" + id
                ));
      }

      /** attribute name. */
      private final String aname;
      /** attribute type. */
      private final AttrType type;
      /** attribute id. */
      private final short id;
      /** hide attribute value on export if CP was sealed. */
      private final boolean hideOnExport;

      /**
       * Constructor.
       * 
       * @param type
       *        attribute type.
       * @param id
       *        attribute id.
       * @param aname
       *        attribute name.
       */
      private AttrInfo(AttrType type, int id, String aname)
      {
         this(type, id, aname, false);
      }
      
      /**
       * Constructor.
       * 
       * @param type
       *        attribute type.
       * @param id
       *        attribute id.
       * @param aname
       *        attribute name.
       * @param hideOnExport
       *        hide attribute value on export if CP was sealed.
       */
      private AttrInfo(AttrType type, int id, String aname, boolean hideOnExport)
      {
         this.type = type;
         this.id = (short)id;
         this.aname = aname;
         this.hideOnExport = hideOnExport;
      }

      /**
       * Return attribute type.
       * 
       * @return attribute type.
       */
      public AttrType type()
      {
         return type;
      }

      /**
       * Return attribute id.
       * 
       * @return attribute id.
       */
      public short id()
      {
         return id;
      }

      /**
       * Return attribute name.
       * 
       * @return attribute name.
       */
      public String aname()
      {
         return aname;
      }
   }
   
   /**
    * Replacement for the default object writing method.
    * 
    * @param    out
    *           The output destination to which fields will be saved.
    *
    * @throws   IOException
    *           In case of I/O errors.
    */
   @Override
   public void writeExternal(ObjectOutput out) throws IOException
   {
      writeByteArray(out, serializePrincipal());
   }

   /**
    * Replacement for the default object reading method.
    * 
    * @param    in
    *           Input source from which fields will be restored.
    *
    * @throws   IOException
    *           In case of I/O errors.
    * @throws   ClassNotFoundException
    *           If payload can't be instantiated.
    */
   @Override
   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
   {
      importPrincipal(readByteArray(in));
   }
}