/** * DSS - Digital Signature Services * Copyright (C) 2015 European Commission, provided under the CEF programme * * This file is part of the "DSS - Digital Signature Services" project. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package eu.europa.esig.dss.spi; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Provider; import java.security.Security; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.TimeZone; import javax.security.auth.x500.X500Principal; import org.bouncycastle.asn1.ASN1Encoding; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.DERNull; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.DigestInfo; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.CMSSignedData; import org.bouncycastle.crypto.digests.SHAKEDigest; import org.bouncycastle.crypto.io.DigestOutputStream; import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator; import org.bouncycastle.tsp.TimeStampToken; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemReader; import org.bouncycastle.util.io.pem.PemWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import eu.europa.esig.dss.enumerations.DigestAlgorithm; import eu.europa.esig.dss.enumerations.X520Attributes; import eu.europa.esig.dss.model.DSSDocument; import eu.europa.esig.dss.model.DSSException; import eu.europa.esig.dss.model.Digest; import eu.europa.esig.dss.model.InMemoryDocument; import eu.europa.esig.dss.model.identifier.TokenIdentifier; import eu.europa.esig.dss.model.x509.CertificateToken; import eu.europa.esig.dss.spi.client.http.DataLoader; import eu.europa.esig.dss.utils.Utils; public final class DSSUtils { private static final Logger LOG = LoggerFactory.getLogger(DSSUtils.class); static { Security.addProvider(DSSSecurityProvider.getSecurityProvider()); } public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; private static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; /** * This class is an utility class and cannot be instantiated. */ private DSSUtils() { } /** * Formats a date to use for internal purposes (logging, toString) * * @param date * the date to be converted * @return the textual representation (a null date will result in "N/A") */ public static String formatInternal(final Date date) { return formatDateWithCustomFormat(date, DEFAULT_DATE_TIME_FORMAT); } public static String formatDateWithCustomFormat(final Date date, final String format) { return (date == null) ? "N/A" : new SimpleDateFormat(format).format(date); } /** * Converts an array of bytes into a String representing the hexadecimal values of each byte in order. The returned * String will be double the length of the passed array, as it takes two characters to represent any given byte. If * the input array is null then null is returned. The obtained string is converted to uppercase. * * @param value * the value to be converted to hexadecimal * @return the hexadecimal String */ public static String toHex(final byte[] value) { return (value != null) ? Utils.toHex(value) : null; } /** * This method converts the given certificate into its PEM string. * * @param cert * the token to be converted to PEM * @return PEM encoded certificate */ public static String convertToPEM(final CertificateToken cert) { return convertToPEM(cert.getCertificate()); } private static String convertToPEM(Object obj) { try (StringWriter out = new StringWriter(); PemWriter pemWriter = new PemWriter(out)) { pemWriter.writeObject(new JcaMiscPEMGenerator(obj)); pemWriter.flush(); return out.toString(); } catch (Exception e) { throw new DSSException("Unable to convert DER to PEM", e); } } /** * This method returns true if the inputStream starts with an ASN.1 Sequence * * @param is * the inputstream to be tested * @return true if DER encoded */ public static boolean isStartWithASN1SequenceTag(InputStream is) { byte firstByte = readFirstByte(new InMemoryDocument(is)); return DSSASN1Utils.isASN1SequenceTag(firstByte); } /** * This method converts a PEM encoded certificate/crl/... to DER encoded * * @param pemContent * the String which contains the PEM encoded object * @return the binaries of the DER encoded object */ public static byte[] convertToDER(String pemContent) { try (Reader reader = new StringReader(pemContent); PemReader pemReader = new PemReader(reader)) { PemObject readPemObject = pemReader.readPemObject(); return readPemObject.getContent(); } catch (IOException e) { throw new DSSException("Unable to convert PEM to DER", e); } } /** * This method loads a certificate from the given location. The certificate must be DER-encoded and may be supplied * in binary or printable (PEM / Base64) encoding. * * If the certificate is provided in Base64 encoding, it must be bounded at the beginning by * {@code -----BEGIN CERTIFICATE-----}, and must be bounded at the end by {@code -----END CERTIFICATE-----}. * * @param file * the file with the certificate * @return the certificate token */ public static CertificateToken loadCertificate(final File file) { final InputStream inputStream = DSSUtils.toByteArrayInputStream(file); return loadCertificate(inputStream); } /** * This method loads a certificate from the given location. The certificate must be DER-encoded and may be supplied * in binary or printable (PEM / Base64) encoding. * * If the certificate is provided in Base64 encoding, it must be bounded at the beginning by * {@code -----BEGIN CERTIFICATE-----}, and must be bounded at the end by {@code -----END CERTIFICATE-----}. * * @param inputStream * input stream containing the certificate * @return the certificate token */ public static CertificateToken loadCertificate(final InputStream inputStream) { List<CertificateToken> certificates = loadCertificates(inputStream); if (certificates.size() == 1) { return certificates.get(0); } throw new DSSException("Could not parse certificate"); } public static Collection<CertificateToken> loadCertificateFromP7c(InputStream is) { return loadCertificates(is); } private static List<CertificateToken> loadCertificates(InputStream is) { final List<CertificateToken> certificates = new ArrayList<>(); try { @SuppressWarnings("unchecked") final Collection<X509Certificate> certificatesCollection = (Collection<X509Certificate>) CertificateFactory .getInstance("X.509", DSSSecurityProvider.getSecurityProviderName()).generateCertificates(is); if (certificatesCollection != null) { for (X509Certificate cert : certificatesCollection) { certificates.add(new CertificateToken(cert)); } } if (certificates.isEmpty()) { throw new DSSException("No certificate found in the InputStream"); } return certificates; } catch (DSSException e) { throw e; } catch (Exception e) { throw new DSSException("Unable to load certificate(s) : " + e.getMessage(), e); } } /** * This method loads a certificate from the byte array. The certificate must be DER-encoded and may be supplied in * binary or printable * (Base64) encoding. If the certificate is provided in Base64 encoding, it must be bounded at the beginning by * -----BEGIN CERTIFICATE-----, and * must be bounded at the end by -----END CERTIFICATE-----. It throws an {@code DSSException} or return {@code null} * when the * certificate cannot be loaded. * * @param input * array of bytes containing the certificate * @return the certificate token */ public static CertificateToken loadCertificate(final byte[] input) { Objects.requireNonNull(input, "Input binary cannot be null"); try (ByteArrayInputStream inputStream = new ByteArrayInputStream(input)) { return loadCertificate(inputStream); } catch (IOException e) { throw new DSSException(e); } } /** * This method loads a certificate from a base 64 encoded String * * @param base64Encoded * the base64 encoded certificate * @return the certificate token */ public static CertificateToken loadCertificateFromBase64EncodedString(final String base64Encoded) { final byte[] bytes = Utils.fromBase64(base64Encoded); return loadCertificate(bytes); } /** * This method loads the potential issuer certificate(s) from the given locations (AIA). * * @param cert * certificate for which the issuer(s) should be loaded * @param loader * the data loader to use * @return a list of potential issuers */ public static Collection<CertificateToken> loadPotentialIssuerCertificates(final CertificateToken cert, final DataLoader loader) { List<String> urls = DSSASN1Utils.getCAAccessLocations(cert); if (Utils.isCollectionEmpty(urls)) { LOG.info("There is no AIA extension for certificate download."); return Collections.emptyList(); } if (loader == null) { LOG.warn("There is no DataLoader defined to load Certificates from AIA extension (urls : {})", urls); return Collections.emptyList(); } for (String url : urls) { LOG.debug("Loading certificate(s) from {}", url); byte[] bytes = null; try { bytes = loader.get(url); } catch (Exception e) { LOG.warn("Unable to download certificate from '{}': {}", url, e.getMessage()); continue; } if (Utils.isArrayNotEmpty(bytes)) { if (LOG.isDebugEnabled()) { LOG.debug("Base64 content : {}", Utils.toBase64(bytes)); } try (InputStream is = new ByteArrayInputStream(bytes)) { return loadCertificates(is); } catch (Exception e) { LOG.warn("Unable to parse certificate(s) from AIA (url: {}) : {}", url, e.getMessage()); } } else { LOG.warn("Empty content from {}.", url); } } return Collections.emptyList(); } /** * This method digests the given string with SHA1 algorithm and encode returned array of bytes as hex string. * * @param stringToDigest * Everything in the name * @return hex encoded digest value */ public static String getSHA1Digest(final String stringToDigest) { return Utils.toHex(digest(DigestAlgorithm.SHA1, stringToDigest.getBytes(StandardCharsets.UTF_8))); } /** * This method allows to digest the data with the given algorithm. * * @param digestAlgorithm * the algorithm to use * @param data * the data to digest * @return digested array of bytes */ public static byte[] digest(final DigestAlgorithm digestAlgorithm, final byte[] data) { Objects.requireNonNull(data, "The data cannot be null"); switch (digestAlgorithm) { case SHAKE128: return computeDigest(new SHAKEDigest(128), data); case SHAKE256: return computeDigest(new SHAKEDigest(256), data); default: final MessageDigest messageDigest = getMessageDigest(digestAlgorithm); return messageDigest.digest(data); } } private static byte[] computeDigest(org.bouncycastle.crypto.Digest digest, byte[] data) { try (DigestOutputStream dos = new DigestOutputStream(digest)) { dos.write(data); return dos.getDigest(); } catch (IOException e) { throw new DSSException("Unable to compute digest : " + e.getMessage(), e); } } public static MessageDigest getMessageDigest(DigestAlgorithm digestAlgorithm) { Objects.requireNonNull(digestAlgorithm, "The DigestAlgorithm cannot be null"); try { return digestAlgorithm.getMessageDigest(); } catch (NoSuchAlgorithmException e) { throw new DSSException("Unable to create a MessageDigest for algorithm " + digestAlgorithm, e); } } /** * This method wraps the digest value in a DigestInfo (combination of digest * algorithm and value). This encapsulation is required to operate NONEwithRSA * signatures. * * @param digestAlgorithm * the used digest algorithm * @param digest * the digest value * @return DER encoded binaries of the related digest info */ public static byte[] encodeRSADigest(final DigestAlgorithm digestAlgorithm, final byte[] digest) { try { AlgorithmIdentifier algId = new AlgorithmIdentifier(new ASN1ObjectIdentifier(digestAlgorithm.getOid()), DERNull.INSTANCE); DigestInfo digestInfo = new DigestInfo(algId, digest); return digestInfo.getEncoded(ASN1Encoding.DER); } catch (IOException e) { throw new DSSException("Unable to encode digest", e); } } /** * This method allows to digest the data in the {@code InputStream} with the given algorithm. * * @param digestAlgo * the algorithm to use * @param inputStream * the data to digest * @return digested array of bytes */ public static byte[] digest(final DigestAlgorithm digestAlgo, final InputStream inputStream) { try { final MessageDigest messageDigest = getMessageDigest(digestAlgo); final byte[] buffer = new byte[4096]; int count = 0; while ((count = inputStream.read(buffer)) > 0) { messageDigest.update(buffer, 0, count); } return messageDigest.digest(); } catch (IOException e) { throw new DSSException(e); } } public static byte[] digest(DigestAlgorithm digestAlgorithm, DSSDocument document) { try (InputStream is = document.openStream()) { return digest(digestAlgorithm, is); } catch (IOException e) { throw new DSSException(e); } } public static byte[] digest(DigestAlgorithm digestAlgorithm, byte[]... data) { final MessageDigest messageDigest = getMessageDigest(digestAlgorithm); for (final byte[] bytes : data) { messageDigest.update(bytes); } return messageDigest.digest(); } /** * This method returns an {@code InputStream} which needs to be closed, based on * {@code FileInputStream}. * * @param file * {@code File} to read. * @return an {@code InputStream} materialized by a {@code FileInputStream} * representing the contents of the file @ if an I/O error occurred */ public static InputStream toInputStream(final File file) { Objects.requireNonNull(file, "The file cannot be null"); try { return openInputStream(file); } catch (IOException e) { throw new DSSException(e); } } /** * This method returns an {@code InputStream} which does not need to be closed, based on * {@code ByteArrayInputStream}. * * @param file * {@code File} to read * @return {@code InputStream} based on {@code ByteArrayInputStream} */ public static InputStream toByteArrayInputStream(final File file) { return new ByteArrayInputStream(toByteArray(file)); } /** * FROM: Apache * Reads the contents of a file into a byte array. * The file is always closed. * * @param file * the file to read, must not be {@code null} * @return the file contents, never {@code null} */ public static byte[] toByteArray(final File file) { try (InputStream is = openInputStream(file)) { return toByteArray(is); } catch (Exception e) { throw new DSSException(e); } } /** * This method create a new document from a sub-part of another document * * @param origin * the original document * @param start * the start position to retrieve * @param end * the end position to retrieve * @return a new DSSDocument */ public static DSSDocument splitDocument(DSSDocument origin, int start, int end) { try (InputStream is = origin.openStream(); BufferedInputStream bis = new BufferedInputStream(is); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { int i = 0; int r; while ((r = bis.read()) != -1) { if (i >= start && i <= end) { baos.write(r); } i++; } baos.flush(); return new InMemoryDocument(baos.toByteArray()); } catch (Exception e) { throw new DSSException("Unable to split document", e); } } /** * FROM: Apache Opens a {@link java.io.FileInputStream} for the specified file, * providing better error messages than simply calling * {@code new FileInputStream(file)}. At the end of the method either the stream * will be successfully opened, or an exception will have been thrown. An * exception is thrown if the file does not exist. An exception is thrown if the * file object exists but is a directory. An exception is thrown if the file * exists but cannot be read. * * @param file * the file to open for input, must not be {@code null} * @return a new {@link java.io.InputStream} for the specified file * @throws NullPointerException * if the file is null * @throws IOException * if the file cannot be read */ private static InputStream openInputStream(final File file) throws IOException { Objects.requireNonNull(file, "The file cannot be null"); if (file.exists()) { if (file.isDirectory()) { throw new IOException("File '" + file + "' exists but is a directory"); } if (!file.canRead()) { throw new IOException("File '" + file + "' cannot be read"); } } else { throw new FileNotFoundException("File '" + file + "' does not exist"); } return new FileInputStream(file); } /** * Get the contents of an {@code DSSDocument} as a {@code byte[]}. * * @param document * the document to read * @return the content as byte array */ public static byte[] toByteArray(final DSSDocument document) { try (InputStream is = document.openStream()) { return toByteArray(is); } catch (IOException e) { throw new DSSException(e); } } /** * Get the contents of an {@code InputStream} as a {@code byte[]}. * * @param inputStream * the inputstream to read * @return the content of the inputstream as byte array */ public static byte[] toByteArray(final InputStream inputStream) { Objects.requireNonNull(inputStream, "The InputStream cannot be null"); try { return Utils.toByteArray(inputStream); } catch (IOException e) { throw new DSSException(e); } } /** * Gets CMSSignedData from the {@code document} bytes * * @param document {@link DSSDocument} contained CMSSignedData * @return {@link CMSSignedData} */ public static CMSSignedData toCMSSignedData(final DSSDocument document) { try (InputStream inputStream = document.openStream()) { return new CMSSignedData(inputStream); } catch (IOException | CMSException e) { throw new DSSException("Not a valid CAdES file", e); } } /** * Checks if the document contains a TimeStampToken * * @param document * the {@link DSSDocument} to be checked * @return true if the document is a timestamp */ public static boolean isTimestampToken(final DSSDocument document) { TimeStampToken timeStampToken = null; try { CMSSignedData cmsSignedData = toCMSSignedData(document); timeStampToken = new TimeStampToken(cmsSignedData); } catch (Exception e) { // ignore } return timeStampToken != null; } /** * Returns byte size of the given document * @param dssDocument {@link DSSDocument} to get size for * @return long size of the given document */ public static long getFileByteSize(DSSDocument dssDocument) { try (InputStream is = dssDocument.openStream()) { return Utils.getInputStreamSize(is); } catch (IOException e) { throw new DSSException(String.format("Cannot read the document with name [%s]", dssDocument.getName()), e); } } /** * This method saves the given array of {@code byte} to the provided {@code File}. * * @param bytes * the binary to save * @param file * the file where to store */ public static void saveToFile(final byte[] bytes, final File file) { file.getParentFile().mkdirs(); try (InputStream is = new ByteArrayInputStream(bytes); OutputStream os = new FileOutputStream(file)) { Utils.copy(is, os); } catch (IOException e) { throw new DSSException(e); } } /** * This method replaces all special characters by an underscore * * @param str * the string / filename / url to normalize * @return the normalized {@link String} */ public static String getNormalizedString(final String str) { String normalizedStr = str; try { normalizedStr = URLDecoder.decode(str, "UTF-8"); } catch (UnsupportedEncodingException e) { LOG.debug("Cannot decode fileName [{}]. Reason : {}", str, e.getMessage()); } normalizedStr = normalizedStr.replaceAll("\\W", "_"); return normalizedStr; } /** * Return a unique id for a date and the certificateToken id. * * @param signingTime * the signing time * @param id * the token identifier * @return a unique string */ public static String getDeterministicId(final Date signingTime, TokenIdentifier id) { try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos)) { if (signingTime != null) { dos.writeLong(signingTime.getTime()); } if (id != null) { dos.writeChars(id.asXmlId()); } dos.flush(); return "id-" + getMD5Digest(baos.toByteArray()); } catch (IOException e) { throw new DSSException(e); } } /** * Returns a Hex encoded of the MD5 digest of binaries * * @param bytes * the bytes to be digested * @return the hex encoded MD5 digest */ public static String getMD5Digest(byte[] bytes) { return Utils.toHex(digest(DigestAlgorithm.MD5, bytes)); } /** * This method returns the {@code X500Principal} corresponding to the given string or {@code null} if the conversion * is not possible. * * @param x500PrincipalString * a {@code String} representation of the {@code X500Principal} * @return {@code X500Principal} or null */ public static X500Principal getX500PrincipalOrNull(final String x500PrincipalString) { try { return new X500Principal(x500PrincipalString, X520Attributes.getUppercaseDescriptionForOids()); } catch (Exception e) { LOG.warn("Unable to create an instance of X500Principal : {}", e.getMessage()); return null; } } /** * This method returns an UTC date base on the year, the month and the day. * The year must be encoded as 1978... and not 78 * * @param year * the value used to set the YEAR calendar field. * @param month * the month. Month value is 0-based. e.g., 0 for January. * @param day * the value used to set the DAY_OF_MONTH calendar field. * @return the UTC date base on parameters */ public static Date getUtcDate(final int year, final int month, final int day) { final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); calendar.set(year, month, day, 0, 0, 0); calendar.set(Calendar.MILLISECOND, 0); return calendar.getTime(); } /** * This method lists all defined security providers. */ public static void printSecurityProviders() { final Provider[] providers = Security.getProviders(); for (final Provider provider : providers) { LOG.info("PROVIDER: {}", provider.getName()); final Set<Provider.Service> services = provider.getServices(); for (final Provider.Service service : services) { LOG.info("\tALGORITHM: {} / {} / {}", service.getAlgorithm(), service.getType(), service.getClassName()); } } } /** * Reads the first byte from the DSSDocument * * @param dssDocument * the document * @return the first byte */ public static byte readFirstByte(final DSSDocument dssDocument) { byte[] result = new byte[1]; try (InputStream inputStream = dssDocument.openStream()) { inputStream.read(result, 0, 1); } catch (IOException e) { throw new DSSException(String.format("Cannot read first byte of the document. Reason : %s", e.getMessage()), e); } return result[0]; } /** * Reads first {@code byteArray.length} bytes of the {@code dssDocument} and compares them with {@code byteArray} * * @param dssDocument {@link DSSDocument} to read bytes from * @param byteArray {@code byte} array to compare the beginning string with * @return TRUE if the document starts from {@code byteArray}, FALSE otherwise */ public static boolean compareFirstBytes(final DSSDocument dssDocument, byte[] byteArray) { try { byte[] preamble = new byte[byteArray.length]; readAvailableBytes(dssDocument, preamble); return Arrays.equals(byteArray, preamble); } catch (IllegalStateException e) { LOG.warn("Cannot compare first bytes of the document. Reason : {}", e.getMessage()); return false; } } /** * Concatenates all the arrays into a new array. The new array contains all of the element of each array followed by * all of the elements of the next array. When an array is * returned, it is always a new array. * * @param arrays * {@code byte} arrays to concatenate * @return the new {@code byte} array */ public static byte[] concatenate(byte[]... arrays) { if ((arrays == null) || (arrays.length == 0) || ((arrays.length == 1) && (arrays[0] == null))) { return null; } if (arrays.length == 1) { return arrays[0].clone(); } int joinedLength = 0; for (final byte[] array : arrays) { if (array != null) { joinedLength += array.length; } } byte[] joinedArray = new byte[joinedLength]; int destinationIndex = 0; for (final byte[] array : arrays) { if (array != null) { System.arraycopy(array, 0, joinedArray, destinationIndex, array.length); destinationIndex += array.length; } } return joinedArray; } public static String decodeUrl(String uri) { try { return URLDecoder.decode(uri, "UTF-8"); } catch (UnsupportedEncodingException e) { LOG.error("Unable to decode '{}' : {}", uri, e.getMessage(), e); } return uri; } /** * Skip the defined {@code n} number of bytes from the {@code InputStream} * and validates success of the operation * @param is {@link InputStream} to skip bytes from * @param n {@code long} number bytes to skip * @return actual number of bytes have been skipped * @exception IllegalStateException in case of {@code InputStream} reading error */ public static long skipAvailableBytes(InputStream is, long n) throws IllegalStateException { try { long skipped = is.skip(n); if (skipped != n) { throw new IllegalStateException(String.format("The number of skipped bytes [%s] differs from the expected value [%s]! " + "The InputStream is too small, corrupted or not accessible!", skipped, n)); } return skipped; } catch (IOException e) { throw new DSSException("Cannot read the InputStream!", e); } } /** * Read the requested number of bytes from {@code DSSDocument} according to the * size of the provided {@code byte}[] buffer and validates success of the * operation * * @param dssDocument * {@link DSSDocument} to read bytes from * @param b * {@code byte}[] buffer to fill * @return the total number of bytes read into buffer * @throws IllegalStateException * in case of {@code InputStream} reading error */ public static long readAvailableBytes(DSSDocument dssDocument, byte[] b) throws IllegalStateException { try (InputStream is = dssDocument.openStream()) { return readAvailableBytes(is, b); } catch (IOException e) { throw new DSSException("Cannot read a sequence of bytes from the document.", e); } } /** * Read the requested number of bytes from {@code InputStream} according to the size of * the provided {@code byte}[] buffer and validates success of the operation * @param is {@link InputStream} to read bytes from * @param b {@code byte}[] buffer to fill * @return the total number of bytes read into buffer * @throws IllegalStateException in case of {@code InputStream} reading error */ public static long readAvailableBytes(InputStream is, byte[] b) throws IllegalStateException { return readAvailableBytes(is, b, 0, b.length); } /** * Read the requested number of bytes from {@code InputStream} * and validates success of the operation * @param is {@link InputStream} to read bytes from * @param b {@code byte}[] buffer to fill * @param off {@code int} offset in the destination array * @param len {@code int} number of bytes to read * @return the total number of bytes read into buffer * @throws IllegalStateException in case of {@code InputStream} reading error */ public static long readAvailableBytes(InputStream is, byte[] b, int off, int len) throws IllegalStateException { try { long read = is.read(b, off, len); if (read != len) { throw new IllegalStateException(String.format("The number of read bytes [%s] differs from the expected value [%s]! " + "The InputStream is too small, corrupted or not accessible!", read, len)); } return read; } catch (IOException e) { throw new DSSException("Cannot read the InputStream!", e); } } /** * This method encodes an URI to be compliant with the RFC 3986 (see DSS-1475 for details) * @param fileURI the uri to be encoded * @return the encoded result */ public static String encodeURI(String fileURI) { StringBuilder sb = new StringBuilder(); String uriDelimiter = ""; final String[] uriParts = fileURI.split("/"); for (String part : uriParts) { sb.append(uriDelimiter ); sb.append(encodePartURI(part)); uriDelimiter = "/"; } return sb.toString(); } /** * This method encodes a partial URI to be compliant with the RFC 3986 (see DSS-1475 for details) * @param uriPart the partial uri to be encoded * @return the encoded result */ private static String encodePartURI(String uriPart) { try { return URLEncoder.encode(uriPart, "UTF-8").replace("+", "%20"); } catch (Exception e) { LOG.warn("Unable to encode uri '{}' : {}", uriPart, e.getMessage()); return uriPart; } } /** * Returns a message retrieved from an exception, * its cause message if the first is not defined, * or exception class name if non of them is specified * * @param e {@link Exception} to get message for * @return {@link String} exception message */ public static String getExceptionMessage(Exception e) { if (e == null) { throw new DSSException("Cannot retrieve a message. The exception is null!"); } if (e.getMessage() != null) { return e.getMessage(); } else if (e.getCause() != null && e.getCause().getMessage() != null) { return e.getCause().getMessage(); } else { return e.getClass().getName(); } } public static Digest getDigest(DigestAlgorithm digestAlgo, DSSDocument dssDocument) { return new Digest(digestAlgo, digest(digestAlgo, dssDocument)); } /** * Replaces null ASCII characters 00-31 and 127 with '' * * @param str {@link String} to remove Ctrls characters from * @return {@link String} without Ctrls characters */ public static String removeControlCharacters(String str) { if (str != null) { String cleanedString = str.replaceAll("[^\\P{Cntrl}]", ""); if (!str.equals(cleanedString)) { LOG.warn("The string [{}] contains illegal characters and was replaced to [{}]", str, cleanedString); } return cleanedString; } return null; } }