All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.dariolucia.ccsds.sle.utl.pdu.PduFactoryUtil Maven / Gradle / Ivy

/*
 *  Copyright 2018-2019 Dario Lucia (https://www.dariolucia.eu)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package eu.dariolucia.ccsds.sle.utl.pdu;

import com.beanit.jasn1.ber.ReverseByteArrayOutputStream;
import com.beanit.jasn1.ber.types.BerInteger;
import com.beanit.jasn1.ber.types.BerNull;
import com.beanit.jasn1.ber.types.BerObjectIdentifier;
import com.beanit.jasn1.ber.types.BerOctetString;
import com.beanit.jasn1.ber.types.string.BerVisibleString;
import eu.dariolucia.ccsds.sle.generated.ccsds.sle.transfer.service.common.types.ConditionalTime;
import eu.dariolucia.ccsds.sle.generated.ccsds.sle.transfer.service.common.types.Credentials;
import eu.dariolucia.ccsds.sle.generated.ccsds.sle.transfer.service.common.types.Time;
import eu.dariolucia.ccsds.sle.generated.ccsds.sle.transfer.service.isp1.credentials.HashInput;
import eu.dariolucia.ccsds.sle.generated.ccsds.sle.transfer.service.isp1.credentials.ISP1Credentials;
import eu.dariolucia.ccsds.sle.generated.ccsds.sle.transfer.service.service.instance.id.OidValues;
import eu.dariolucia.ccsds.sle.generated.ccsds.sle.transfer.service.service.instance.id.ServiceInstanceAttribute;
import eu.dariolucia.ccsds.sle.generated.ccsds.sle.transfer.service.service.instance.id.ServiceInstanceIdentifier;
import eu.dariolucia.ccsds.sle.utl.config.network.RemotePeer;
import eu.dariolucia.ccsds.sle.utl.si.ApplicationIdentifierEnum;
import eu.dariolucia.ccsds.sle.utl.si.HashFunctionEnum;

import jakarta.xml.bind.DatatypeConverter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Utility class for the credentials and time construction and verification.
 */
public class PduFactoryUtil {

    private static final Logger LOG = Logger.getLogger(PduFactoryUtil.class.getName());

    private PduFactoryUtil() {
        // Private constructor
    }

    /**
     * Number of days from 1st Jan 1958 to 1st Jan 1970
     */
    private static final int DAYS_FROM_1958_TO_1970;

    static {
        GregorianCalendar d1958 = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
        d1958.set(1958, Calendar.JANUARY, 1, 0, 0);
        GregorianCalendar d1970 = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
        d1970.set(1970, Calendar.JANUARY, 1, 0, 0);

        Instant i1958 = d1958.toInstant();
        Instant i1970 = d1970.toInstant();
        Duration d = Duration.between(i1958, i1970);
        DAYS_FROM_1958_TO_1970 = (int) d.toDays();
    }

    /**
     * This method builds the service instance identifier object starting from its string representation.
     *
     * @param serviceInstanceIdentifier the SIID as string
     * @param type                      the service type
     * @return the {@link ServiceInstanceIdentifier} object representing the provided string
     */
    public static ServiceInstanceIdentifier buildServiceInstanceIdentifier(final String serviceInstanceIdentifier,
                                                                           final ApplicationIdentifierEnum type) {
        if (LOG.isLoggable(Level.FINER)) {
            LOG.finer("Building SIID from string " + serviceInstanceIdentifier + ", service " + type);
        }
        String[] dotSplit = serviceInstanceIdentifier.split("\\.", -1);
        // Assume 4 entries
        ServiceInstanceIdentifier toReturn = new ServiceInstanceIdentifier();

        String sagr = dotSplit[0].split("=", -1)[1];
        addSiAttribute(toReturn, OidValues.sagr, sagr);


        String spack = dotSplit[1].split("=", -1)[1];
        addSiAttribute(toReturn, OidValues.spack, spack);

        String svtt = dotSplit[2].split("=", -1)[1];
        switch (type) {
            case RAF:
            case RCF:
            case ROCF:
                addSiAttribute(toReturn, OidValues.rslFg, svtt);
                break;
            case FSP:
            case CLTU:
                addSiAttribute(toReturn, OidValues.fslFg, svtt);
                break;
            default:
                throw new IllegalArgumentException("Service type " + type + " unknown");
        }

        String fs = dotSplit[3].split("=", -1)[1];
        switch (type) {
            case RAF:
                addSiAttribute(toReturn, OidValues.raf, fs);
                break;
            case RCF:
                addSiAttribute(toReturn, OidValues.rcf, fs);
                break;
            case ROCF:
                addSiAttribute(toReturn, OidValues.rocf, fs);
                break;
            case FSP:
                addSiAttribute(toReturn, OidValues.fsp, fs);
                break;
            case CLTU:
                addSiAttribute(toReturn, OidValues.cltu, fs);
                break;
            default:
                throw new IllegalArgumentException("Service type " + type + " unknown");
        }

        return toReturn;
    }

    private static void addSiAttribute(ServiceInstanceIdentifier si, BerObjectIdentifier oid, String value) {
        ServiceInstanceAttribute attribute = new ServiceInstanceAttribute();
        ServiceInstanceAttribute.SEQUENCE theSequence = new ServiceInstanceAttribute.SEQUENCE();
        theSequence.setIdentifier(oid);
        theSequence.setSiAttributeValue(new BerVisibleString(value));
        attribute.getSEQUENCE().add(theSequence);
        si.getServiceInstanceAttribute().add(attribute);
    }

    /**
     * This method constructs an empty Credentials object, i.e. credentials are not used.
     *
     * @return the Credentials object
     */
    public static Credentials buildEmptyCredentials() {
        return buildCredentials(false, null, null, HashFunctionEnum.SHA_1);
    }

    /**
     * This method generates the ASN.1 Credentials object.
     *
     * @param fillCredentials whether the credentials must be filled or not (BerNull)
     * @param username        the username to be used in the credentials data
     * @param password        the password
     * @param hashToUse       the has function to use (usually, from SLE version 1 to 3: hash using SHA-1, 20 bytes; 4-5: hash using SHA-256, 32 bytes)
     * @return the Credentials object fully populated
     */
    public static Credentials buildCredentials(boolean fillCredentials, String username, byte[] password, HashFunctionEnum hashToUse) {
        Credentials c = new Credentials();
        if (fillCredentials) {
            // Current time, as per CCSDS 913.1-B-2, 3.1.2.1.1
            long time = System.currentTimeMillis();
            // Random number (positive), as per CCSDS 913.1-B-2, 3.1.2.1.1
            long randomNumber = (new Random(time).nextInt()) & 0x7FFFFFFFL;
            // No support for microsecond resolution
            byte[] buffer = hashCredentialsData(time, 0, randomNumber, username, password);
            // The next variable is what the standard calls 'the protected'
            byte[] hashSignature = calculateTheProtected(hashToUse, buffer);
            // Build the ISP1 Credentials object
            ISP1Credentials isp1Credentials = new ISP1Credentials();
            // Set the time: CDS with implicit P-Field, epoch 1st Jan 1958, microseconds resolution
            // (ref. CCSDS 913.1-B-2)
            isp1Credentials.setTime(new BerOctetString(buildCDSTime(time, 0)));
            // Set the protected credentials data (hashed)
            isp1Credentials.setTheProtected(new BerOctetString(hashSignature));
            // Set the random number
            isp1Credentials.setRandomNumber(new BerInteger(randomNumber));
            // Encode
            ReverseByteArrayOutputStream encoding = new ReverseByteArrayOutputStream(140, true);
            try {
                isp1Credentials.encode(encoding, true);
                encoding.close();
                byte[] encoded = encoding.getArray();
                c.setUsed(new BerOctetString(encoded));
            } catch (IOException e) {
                throw new IllegalStateException("Credential encoding failed", e);
            }
        } else {
            c.setUnused(new BerNull());
        }
        return c;
    }

    private static byte[] calculateTheProtected(HashFunctionEnum hashToUse, byte[] buffer) {
        byte[] hashSignature;// Compute the hash signature of the credentials data, ref. CCSDS 913.1-B-2 3.1.2.1.3
        try {
            MessageDigest md = MessageDigest.getInstance(hashToUse.getHashFunction());
            md.reset();
            md.update(buffer);
            hashSignature = md.digest();
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("Hash function not defined: " + hashToUse.getHashFunction(), e);
        }
        return hashSignature;
    }

    /**
     * This method encodes the credentials data as specified in CCSDS 913.1-B-2, 3.1.2.1.2 using the DER
     * encoding rules as per ISO/IEC 8825-1:2008 (revised version is 2015).
     * 

* HashInput ::= SEQUENCE * { time OCTET STRING (SIZE(8)) * , randomNumber INTEGER (0 .. 2147483647) * , userName VisibleString * , passWord OCTET STRING * } *

* According to the JASN.1 library documentation, the output of the JASN.1 encoder should be DER-compliant (not * verified). * * @param timeMillis time in millisecs since Java epoch * @param microsec microseconds in the millisecond * @param randomNumber the random number * @param username the username * @param password the password (as byte array) * @return the encoded information using DER */ private static byte[] hashCredentialsData(long timeMillis, long microsec, long randomNumber, String username, byte[] password) { try { HashInput hashInput = new HashInput(); hashInput.setTime(new BerOctetString(buildCDSTime(timeMillis, microsec))); hashInput.setRandomNumber(new BerInteger(randomNumber)); hashInput.setUserName(new BerVisibleString(username)); hashInput.setPassWord(new BerOctetString(password)); ReverseByteArrayOutputStream os = new ReverseByteArrayOutputStream(140, true); hashInput.encode(os); // by default calls encode(os, true) os.close(); return os.getArray(); } catch (IOException e) { throw new IllegalStateException("HashInput creation failed", e); } } /** * This method builds the CDS time representation according to the SLE standard: *

    *
  • P-field is implicit
  • *
  • T-field:
  • *
  • 2 octets: number of days since 1958/01/01 00:00:00
  • *
  • 4 octets: number of milliseconds of the day
  • *
  • 2 octets: number of microseconds of the millisecond (set to 0 if not used)
  • *
  • This definition reflects exactly the format of the CCSDS defined time tag as used in spacelink data units.
  • *
* Reference for implementation: CCSDS 301.0-B-4, section 3.3. * * @param timeMillisSinceEpoch time in millisecs since Java epoch * @param microsecsInMillisec microseconds in the millisecond * @return the CDS (8 bytes) representation of the provided time */ public static byte[] buildCDSTime(long timeMillisSinceEpoch, long microsecsInMillisec) { // Guard condition to reject negative values if (microsecsInMillisec < 0 || timeMillisSinceEpoch < 0) { throw new IllegalArgumentException("Negative value provided: " + timeMillisSinceEpoch + ", " + microsecsInMillisec); } // Compute the number of seconds from Java epoch long secs = timeMillisSinceEpoch / 1000; // Compute the number of days from Java epoch and, to be compliant with the CCSDS epoch (1st Jan 1958) // add DAYS_FROM_1958_to_1970 days. DAYS_FROM_1958_to_1970 is the difference from the two epochs dates // (1st Jan 1970 - 1st Jan 1958) long daysFromEpoch = secs / 86400 + DAYS_FROM_1958_TO_1970; // Now compute the milliseconds within the day: number of seconds in the day (remainder) times 1000 plus the // remainder of the milliseconds long millisecsInDay = (secs % 86400) * 1000 + timeMillisSinceEpoch % 1000; // if microseconds is not normalised (i.e. > 999) normalize it and add the result to the milliseconds millisecsInDay += microsecsInMillisec / 1000; // Compute the number of microseconds within the millisecond (normalized) microsecsInMillisec %= 1000; // Finally, encode the result using a ByteBuffer, MSB by default in Java, integers are truncated ByteBuffer bb = ByteBuffer.allocate(8); bb.putShort((short) daysFromEpoch); bb.putInt((int) millisecsInDay); bb.putShort((short) microsecsInMillisec); return bb.array(); } /** * This method builds the CDS time representation according to the SLE standard: *
    *
  • P-field is implicit
  • *
  • T-field:
  • *
  • 2 octets: number of days since 1958/01/01 00:00:00
  • *
  • 4 octets: number of milliseconds of the day
  • *
  • 4 octets: number of picoseconds of the millisecond (set to 0 if not used)
  • *
  • This definition reflects exactly the format of the CCSDS defined time tag as used in spacelink data units.
  • *
* Reference for implementation: CCSDS 301.0-B-4, section 3.3. * * @param timeMillisSinceEpoch time in millisecs since Java epoch * @param picosecsInMillisec picoseconds in the millisecond * @return the CDS (8 bytes) representation of the provided time */ public static byte[] buildCDSTimePico(long timeMillisSinceEpoch, long picosecsInMillisec) { // Guard condition to reject negative values if (picosecsInMillisec < 0 || timeMillisSinceEpoch < 0) { throw new IllegalArgumentException("Negative value provided: " + timeMillisSinceEpoch + ", " + picosecsInMillisec); } // Compute the number of seconds from Java epoch long secs = timeMillisSinceEpoch / 1000; // Compute the number of days from Java epoch and, to be compliant with the CCSDS epoch (1st Jan 1958) // add DAYS_FROM_1958_to_1970 days. DAYS_FROM_1958_to_1970 is the difference from the two epochs dates // (1st Jan 1970 - 1st Jan 1958) long daysFromEpoch = secs / 86400 + DAYS_FROM_1958_TO_1970; // Now compute the milliseconds within the day: number of seconds in the day (remainder) times 1000 plus the // remainder of the milliseconds long millisecsInDay = (secs % 86400) * 1000 + timeMillisSinceEpoch % 1000; // if microseconds is not normalised (i.e. > 999) normalize it and add the result to the milliseconds millisecsInDay += picosecsInMillisec / 1000000000; // Compute the number of microseconds within the millisecond (normalized) picosecsInMillisec %= 1000000000; // Finally, encode the result using a ByteBuffer, MSB by default in Java, integers are truncated ByteBuffer bb = ByteBuffer.allocate(10); bb.putShort((short) daysFromEpoch); bb.putInt((int) millisecsInDay); bb.putInt((int) picosecsInMillisec); return bb.array(); } /** * This method performs the authentication on the basis of the received credentials from the expected user, * according to the specification detailed in CCSDS 913.1-B-2, 3.1.2.2.2-4. * * @param remotePeer the remote peer * @param encodedCredentials the received credentials (to be BER-decoded) * @param authDelayInSeconds the maximum delay in seconds to accept the credentials as good * @return true if the authentication succeeds, false otherwise */ public static boolean performAuthentication(RemotePeer remotePeer, byte[] encodedCredentials, int authDelayInSeconds) { // First: decode the credentials ISP1Credentials isp1Credentials = new ISP1Credentials(); ByteArrayInputStream in = new ByteArrayInputStream(encodedCredentials); try { isp1Credentials.decode(in, true); } catch (IOException e) { LOG.log(Level.WARNING, String.format("Cannot decode credentials from remote peer %s, encoded credentials are %s", remotePeer.getId(), DatatypeConverter.printHexBinary(encodedCredentials)), e); return false; } // From the Credentials time, we extract the time long[] timeMillis; try { timeMillis = buildTimeMillis(isp1Credentials.getTime().value); } catch (IllegalArgumentException e) { if(LOG.isLoggable(Level.WARNING)) { LOG.warning(String.format("Cannot read time from credentials of remote peer %s, CDS time is %s", remotePeer.getId(), DatatypeConverter.printHexBinary(isp1Credentials.getTime().value))); } return false; } // We get the current time, that we use to compute the delay. If above the configured threshold we reject the // invocation. long now = System.currentTimeMillis(); if (now - timeMillis[0] > authDelayInSeconds * 1000) { if(LOG.isLoggable(Level.WARNING)) { LOG.warning(String.format("Cannot verify credentials of remote peer %s, acceptable delay exceeded, now=%d, time=%d, acceptable delay in ms=%d", remotePeer.getId(), now, timeMillis[0], authDelayInSeconds * 1000)); } return false; } // Now we check the hash of the credentials data: we compare the provided protected with the one // we compute locally. If they are equals, all fine. // Local hash using random number and time from the ISP1Credentials object, and locally stored user and pass byte[] localCredentialsData = hashCredentialsData(timeMillis[0], timeMillis[1], isp1Credentials.getRandomNumber().longValue(), remotePeer.getId(), remotePeer.getPassword()); // Compute the hash signature byte[] hashSignature = calculateTheProtected(remotePeer.getAuthenticationHash(), localCredentialsData); // Comparison return Arrays.equals(hashSignature, isp1Credentials.getTheProtected().value); } /** * Returns an array with, at index 0, the number of milliseconds since 1970, at * index 1, the number of microsecs within the millisec. * * @param cdsTime the time in CDS format, implicit P-field, 8 bytes * @return the result * @throws IllegalArgumentException if the number of days is fewer than 1958-1970 difference */ public static long[] buildTimeMillis(byte[] cdsTime) { ByteBuffer bb = ByteBuffer.wrap(cdsTime); int days = Short.toUnsignedInt(bb.getShort()); long millisec = Integer.toUnsignedLong(bb.getInt()); int microsec = Short.toUnsignedInt(bb.getShort()); // To move to the Java epoch, remove DAYS_FROM_1958_to_1970 days if (days < DAYS_FROM_1958_TO_1970) { throw new IllegalArgumentException("Provided CDS time returns a number of days fewer than 1958-1970 difference: " + PduStringUtil.toHexDump(cdsTime)); } days -= DAYS_FROM_1958_TO_1970; return new long[]{days * 86400L * 1000L + millisec, microsec}; } /** * Returns an array with, at index 0, the number of milliseconds since 1970, at * index 1, the number of picosecs within the millisec. * * @param cdsTime the time in CDS format, implicit P-field, 10 bytes * @return the result * @throws IllegalArgumentException if the number of days is fewer than 1958-1970 difference */ public static long[] buildTimeMillisPico(byte[] cdsTime) { ByteBuffer bb = ByteBuffer.wrap(cdsTime); int days = Short.toUnsignedInt(bb.getShort()); long millisec = Integer.toUnsignedLong(bb.getInt()); long picosec = Integer.toUnsignedLong(bb.getInt()); if (days < DAYS_FROM_1958_TO_1970) { throw new IllegalArgumentException("Provided CDS pico-time returns a number of days fewer than 1958-1970 difference: " + PduStringUtil.toHexDump(cdsTime)); } days -= DAYS_FROM_1958_TO_1970; return new long[]{days * 86400L * 1000L + millisec, picosec}; } /** * Maps the provided ConditionalTime object into a Java Date, or null if the time is not set. * * @param theTime the condition time to convert * @return the corresponding Date object */ public static Date toDate(ConditionalTime theTime) { Instant instant = toInstant(theTime); return instant == null ? null : new Date(instant.toEpochMilli()); } /** * Maps the provided ConditionalTime object into a Java Instant, or null if the time is not set. * * @param theTime the condition time to convert * @return the corresponding Instant object */ public static Instant toInstant(ConditionalTime theTime) { if(theTime == null || theTime.getUndefined() != null) { return null; } else { Time t = theTime.getKnown(); if(t.getCcsdsFormat() != null) { // Millisecond resolution long[] components = buildTimeMillis(t.getCcsdsFormat().value); return Instant.ofEpochMilli(components[0]); } else if(t.getCcsdsPicoFormat() != null) { // Picosecond resolution long[] components = buildTimeMillisPico(t.getCcsdsPicoFormat().value); return Instant.ofEpochSecond(components[0]/1000, (components[1] % 1000) * 1000000L + components[0]/1000); } else { // Problem throw new IllegalArgumentException("ConditionalTime does not deliver any time!"); } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy