/** * The contents of this file are subject to the license and copyright * detailed in the LICENSE file at the root of the source * tree and available online at * * https://github.com/keeps/roda */ package org.roda.core.plugins.plugins.characterization; import java.io.IOException; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.xml.crypto.MarshalException; import javax.xml.crypto.dsig.XMLSignatureException; import org.apache.commons.io.IOUtils; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.bouncycastle.cms.CMSException; import org.roda.core.RodaCoreFactory; import org.roda.core.common.iterables.CloseableIterable; import org.roda.core.data.common.RodaConstants; import org.roda.core.data.common.RodaConstants.PreservationEventType; import org.roda.core.data.exceptions.AlreadyExistsException; import org.roda.core.data.exceptions.AuthorizationDeniedException; import org.roda.core.data.exceptions.GenericException; import org.roda.core.data.exceptions.NotFoundException; import org.roda.core.data.exceptions.RequestNotValidException; import org.roda.core.data.v2.IsRODAObject; import org.roda.core.data.v2.common.OptionalWithCause; import org.roda.core.data.v2.ip.AIP; import org.roda.core.data.v2.ip.DIP; import org.roda.core.data.v2.ip.DIPFile; import org.roda.core.data.v2.ip.File; import org.roda.core.data.v2.ip.FileLink; import org.roda.core.data.v2.ip.IndexedFile; import org.roda.core.data.v2.ip.Permissions; import org.roda.core.data.v2.ip.Representation; import org.roda.core.data.v2.ip.RepresentationLink; import org.roda.core.data.v2.ip.StoragePath; import org.roda.core.data.v2.jobs.Job; import org.roda.core.data.v2.jobs.PluginType; import org.roda.core.data.v2.jobs.Report; import org.roda.core.data.v2.jobs.Report.PluginState; import org.roda.core.index.IndexService; import org.roda.core.model.ModelService; import org.roda.core.model.utils.ModelUtils; import org.roda.core.plugins.AbstractAIPComponentsPlugin; import org.roda.core.plugins.Plugin; import org.roda.core.plugins.PluginException; import org.roda.core.plugins.orchestrate.SimpleJobPluginInfo; import org.roda.core.plugins.plugins.PluginHelper; import org.roda.core.storage.ContentPayload; import org.roda.core.storage.DirectResourceAccess; import org.roda.core.storage.StorageService; import org.roda.core.storage.fs.FSPathContentPayload; import org.roda.core.util.IdUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.itextpdf.text.DocumentException; public class DigitalSignatureDIPPlugin<T extends IsRODAObject> extends AbstractAIPComponentsPlugin<T> { private static final Logger LOGGER = LoggerFactory.getLogger(DigitalSignatureDIPPlugin.class); private boolean doEmbeddedSignature; public DigitalSignatureDIPPlugin() { super(); doEmbeddedSignature = Boolean .parseBoolean(RodaCoreFactory.getRodaConfigurationAsString("core", "signature", "doEmbeddedSignature")); } public boolean getDoEmbeddedSignature() { return doEmbeddedSignature; } public void setDoEmbeddedSignature(boolean doEmbedded) { doEmbeddedSignature = doEmbedded; } @Override public void init() throws PluginException { // do nothing } @Override public void shutdown() { // do nothing } @Override public String getName() { return "Digital sign entities"; } @Override public String getVersionImpl() { return "1.0"; } @Override public String getDescription() { return "Creates a new Dissemination Information Package (DIP) for this AIP containing all the files in a given representation and " + "appends a digital signature to each of these files.\nThe digital signature (in PKCS#7 format) is an external file with the" + " same name as the original one but with a .p7s extension.\nDigital signatures are generated based on the digital certificate " + "installed under “/config/certificates/”."; } private String getDIPTitle() { return "Digital signed dissemination"; } private String getDIPDescription() { return "Digital signed dissemination of a file, possibly embedded"; } @Override public Report executeOnAIP(IndexService index, ModelService model, StorageService storage, Report report, SimpleJobPluginInfo jobPluginInfo, List<AIP> list, Job job) throws PluginException { for (AIP aip : list) { PluginState pluginState = PluginState.SUCCESS; Report reportItem = PluginHelper.initPluginReportItem(this, aip.getId(), AIP.class); for (Representation representation : aip.getRepresentations()) { String dipId = IdUtils.createUUID(); try { RepresentationLink representationLink = new RepresentationLink(representation.getAipId(), representation.getId()); List<RepresentationLink> links = new ArrayList<>(); links.add(representationLink); DIP dip = new DIP(); dip.setId(dipId); dip.setRepresentationIds(links); dip.setPermissions(aip.getPermissions()); dip.setTitle(getDIPTitle()); dip.setDescription(getDIPDescription()); dip.setType(RodaConstants.DIP_TYPE_DIGITAL_SIGNATURE); dip = model.createDIP(dip, false); LOGGER.debug("Processing representation {}", representation); boolean recursive = true; CloseableIterable<OptionalWithCause<File>> allFiles = model.listFilesUnder(representation.getAipId(), representation.getId(), recursive); for (OptionalWithCause<File> oFile : allFiles) { if (oFile.isPresent() && !oFile.get().isDirectory()) { manageFileSigning(model, index, storage, oFile.get(), dip.getId()); } else { LOGGER.error("Cannot process representation file", oFile.getCause()); } } IOUtils.closeQuietly(allFiles); model.notifyDIPCreated(dip, true); } catch (Exception | LinkageError e) { LOGGER.error("Error processing Representation " + representation.getId() + ": " + e.getMessage(), e); reportItem.setPluginDetails(e.getMessage()); pluginState = PluginState.FAILURE; try { model.deleteDIP(dipId); } catch (GenericException | NotFoundException | AuthorizationDeniedException e1) { // do nothing } } finally { report.addReport(reportItem); PluginHelper.updatePartialJobReport(this, model, reportItem, true, job); } } if (pluginState.equals(PluginState.SUCCESS)) { jobPluginInfo.incrementObjectsProcessedWithSuccess(); } else { jobPluginInfo.incrementObjectsProcessedWithFailure(); } reportItem.setPluginState(pluginState); report.addReport(reportItem); PluginHelper.updatePartialJobReport(this, model, reportItem, true, job); } return report; } @Override public Report executeOnRepresentation(IndexService index, ModelService model, StorageService storage, Report report, SimpleJobPluginInfo jobPluginInfo, List<Representation> list, Job job) throws PluginException { for (Representation representation : list) { Report reportItem = PluginHelper.initPluginReportItem(this, IdUtils.getRepresentationId(representation), Representation.class); String dipId = IdUtils.createUUID(); try { Permissions aipPermissions = model.retrieveAIP(representation.getAipId()).getPermissions(); RepresentationLink representationLink = new RepresentationLink(representation.getAipId(), representation.getId()); List<RepresentationLink> links = new ArrayList<>(); links.add(representationLink); DIP dip = new DIP(); dip.setId(dipId); dip.setRepresentationIds(links); dip.setPermissions(aipPermissions); dip.setTitle(getDIPTitle()); dip.setDescription(getDIPDescription()); dip.setType(RodaConstants.DIP_TYPE_DIGITAL_SIGNATURE); dip = model.createDIP(dip, false); LOGGER.debug("Processing representation {}", representation); boolean recursive = true; CloseableIterable<OptionalWithCause<File>> allFiles = model.listFilesUnder(representation.getAipId(), representation.getId(), recursive); for (OptionalWithCause<File> oFile : allFiles) { if (oFile.isPresent() && !oFile.get().isDirectory()) { manageFileSigning(model, index, storage, oFile.get(), dip.getId()); } else { LOGGER.error("Cannot process representation file", oFile.getCause()); } } model.notifyDIPCreated(dip, true); IOUtils.closeQuietly(allFiles); reportItem.setPluginState(PluginState.SUCCESS); jobPluginInfo.incrementObjectsProcessedWithSuccess(); } catch (Exception | LinkageError e) { LOGGER.error("Error processing Representation " + representation.getId() + ": " + e.getMessage(), e); reportItem.setPluginState(PluginState.FAILURE).setPluginDetails(e.getMessage()); jobPluginInfo.incrementObjectsProcessedWithFailure(); try { model.deleteDIP(dipId); } catch (GenericException | NotFoundException | AuthorizationDeniedException e1) { // do nothing } } finally { report.addReport(reportItem); PluginHelper.updatePartialJobReport(this, model, reportItem, true, job); } } return report; } @Override public Report executeOnFile(IndexService index, ModelService model, StorageService storage, Report report, SimpleJobPluginInfo jobPluginInfo, List<File> list, Job job) throws PluginException { for (File file : list) { String dipId = IdUtils.createUUID(); try { Permissions aipPermissions = model.retrieveAIP(file.getAipId()).getPermissions(); FileLink fileLink = new FileLink(file.getAipId(), file.getRepresentationId(), file.getPath(), file.getId()); List<FileLink> links = new ArrayList<>(); links.add(fileLink); DIP dip = new DIP(); dip.setId(dipId); dip.setFileIds(links); dip.setPermissions(aipPermissions); dip.setTitle(getDIPTitle()); dip.setDescription(getDIPDescription()); dip.setType(RodaConstants.DIP_TYPE_DIGITAL_SIGNATURE); dip = model.createDIP(dip, false); Report reportItem = PluginHelper.initPluginReportItem(this, IdUtils.getFileId(file), File.class); reportItem.setOutcomeObjectId(dip.getId()).setOutcomeObjectClass(DIP.class.getName()); reportItem.setPluginState(PluginState.SUCCESS); try { if (!file.isDirectory()) { manageFileSigning(model, index, storage, file, dip.getId()); } else { CloseableIterable<OptionalWithCause<File>> fileIterable = model.listFilesUnder(file.getAipId(), file.getRepresentationId(), file.getPath(), file.getId(), true); for (OptionalWithCause<File> ofileUnder : fileIterable) { if (ofileUnder.isPresent()) { File fileUnder = ofileUnder.get(); manageFileSigning(model, index, storage, fileUnder, dip.getId()); } } } jobPluginInfo.incrementObjectsProcessedWithSuccess(); model.notifyDIPCreated(dip, true); } catch (Exception | LinkageError e) { LOGGER.error("Error processing File " + file.getId() + ": " + e.getMessage(), e); reportItem.setPluginState(PluginState.FAILURE).setPluginDetails(e.getMessage()); jobPluginInfo.incrementObjectsProcessedWithFailure(); model.deleteDIP(dipId); } finally { report.addReport(reportItem); PluginHelper.updatePartialJobReport(this, model, reportItem, true, job); } } catch (GenericException | AuthorizationDeniedException | RequestNotValidException | NotFoundException e1) { LOGGER.error("Error creating DIP for file " + file.getId()); } } return report; } private void manageFileSigning(ModelService model, IndexService index, StorageService storage, File file, String dipId) throws NotFoundException, GenericException, InvalidFormatException, RequestNotValidException, AuthorizationDeniedException, IOException, GeneralSecurityException, DocumentException, XMLSignatureException, MarshalException, AlreadyExistsException, CMSException { StoragePath fileStoragePath = ModelUtils.getFileStoragePath(file); DirectResourceAccess directAccess = storage.getDirectAccess(fileStoragePath); if (!file.isDirectory()) { LOGGER.debug("Processing file {}", file); IndexedFile ifile = index.retrieve(IndexedFile.class, IdUtils.getFileId(file), RodaConstants.FILE_FORMAT_FIELDS_TO_RETURN); String fileMimetype = ifile.getFileFormat().getMimeType(); String fileFormat = ifile.getId().substring(ifile.getId().lastIndexOf('.') + 1); LOGGER.debug("Running DigitalSignaturePluginDIP on {}", file.getId()); if (doEmbeddedSignature) { Path embeddedFile = DigitalSignatureDIPPluginUtils.addEmbeddedSignature(directAccess.getPath(), fileFormat, fileMimetype); if (embeddedFile != null) { ContentPayload payload = new FSPathContentPayload(embeddedFile); model.createDIPFile(dipId, file.getPath(), file.getId(), embeddedFile.toFile().length(), payload, false); } else { addDIPFileSignature(model, storage, dipId, file.getPath(), file.getId(), directAccess.getPath()); } } else { addDIPFileSignature(model, storage, dipId, file.getPath(), file.getId(), directAccess.getPath()); } } else { ContentPayload payload = new FSPathContentPayload(directAccess.getPath()); model.createDIPFile(dipId, file.getPath(), file.getId(), 0L, payload, false); } IOUtils.closeQuietly(directAccess); } private void addDIPFileSignature(ModelService model, StorageService storage, String dipId, List<String> filePath, String fileId, Path inputFile) throws RequestNotValidException, GenericException, AlreadyExistsException, AuthorizationDeniedException, NotFoundException, IOException, GeneralSecurityException, DocumentException, CMSException { ContentPayload payload = new FSPathContentPayload(inputFile); DIPFile dipFile = model.createDIPFile(dipId, filePath, fileId, inputFile.toFile().length(), payload, false); DirectResourceAccess dipFileDirectAccess = storage.getDirectAccess(ModelUtils.getDIPFileStoragePath(dipFile)); DigitalSignatureDIPPluginUtils.addDetachedSignature(dipFileDirectAccess.getPath()); IOUtils.closeQuietly(dipFileDirectAccess); } @Override public PluginType getType() { return PluginType.AIP_TO_AIP; } @Override public Plugin<T> cloneMe() { return new DigitalSignatureDIPPlugin<>(); } @Override public boolean areParameterValuesValid() { return true; } @Override public PreservationEventType getPreservationEventType() { return PreservationEventType.DIGITAL_SIGNATURE_VALIDATION; } @Override public String getPreservationEventDescription() { return "Digitally signed files on a DIP."; } @Override public String getPreservationEventSuccessMessage() { return "The files were successfully signed."; } @Override public String getPreservationEventFailureMessage() { return "Failed to digitally sign some files."; } @Override public Report beforeAllExecute(IndexService index, ModelService model, StorageService storage) throws PluginException { return null; } @Override public Report afterAllExecute(IndexService index, ModelService model, StorageService storage) throws PluginException { return null; } @Override public List<String> getCategories() { return Arrays.asList(RodaConstants.PLUGIN_CATEGORY_DISSEMINATION); } @SuppressWarnings({"unchecked", "rawtypes"}) @Override public List<Class<T>> getObjectClasses() { List<Class<? extends IsRODAObject>> list = new ArrayList<>(); list.add(AIP.class); list.add(Representation.class); list.add(File.class); return (List) list; } }