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));
}
}