001/*
002 * (C) Copyright 2016-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Thibaud Arguillere
018 *     Miguel Nixo
019 */
020package org.nuxeo.ecm.platform.pdf;
021
022import java.util.Arrays;
023import java.util.List;
024
025import org.apache.commons.lang3.StringUtils;
026import org.apache.pdfbox.pdmodel.PDDocument;
027import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
028import org.apache.pdfbox.pdmodel.encryption.StandardDecryptionMaterial;
029import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
030import org.nuxeo.ecm.core.api.Blob;
031import org.nuxeo.ecm.core.api.Blobs;
032import org.nuxeo.ecm.core.api.NuxeoException;
033import org.nuxeo.ecm.core.api.impl.blob.FileBlob;
034
035/**
036 * Encrypt/Decrypt a PDF.
037 * <p>
038 * Notice that encryption is not only about pure encryption, it is also about setting the available features on the pdf:
039 * can print, copy, modify, ... (see PDFBox {@link AccessPermission}).
040 * <p>
041 * To use this class and its encrypt()/removeEncryption() methods you must always use the appropriate setters first, to
042 * set the misc info (original owner password so an encrypted PDF can be handled, encryption key length, ...).
043 *
044 * @since 8.10
045 */
046public class PDFEncryption {
047
048    private Blob pdfBlob;
049
050    private PDDocument pdfDoc;
051
052    private static final List<Integer> ALLOWED_LENGTH = Arrays.asList(40, 128);
053
054    private static final int DEFAULT_KEYLENGTH = 128;
055
056    private int keyLength = DEFAULT_KEYLENGTH;
057
058    private String originalOwnerPwd;
059
060    private String ownerPwd;
061
062    private String userPwd;
063
064    /**
065     * Basic constructor.
066     *
067     * @param inBlob Input blob.
068     */
069    public PDFEncryption(Blob inBlob) {
070        pdfBlob = inBlob;
071    }
072
073    /**
074     * Encrypts the PDF with readonly permission.
075     * <p>
076     * WARNING: If you are familiar with PDFBox {@link AccessPermission}, notice our encryptReadOnly() method is not the
077     * same as {@link AccessPermission#AccessPermission#setReadOnly}. The latter just makes sure the code cannot call
078     * other setter later on.
079     * <p>
080     * <code>encryptReadOnly</code> sets the following permissions on the document:
081     * <ul>
082     * <li>Can print: True</li>
083     * <li>Can Modify: False</li>
084     * <li>Can Extract Content: True</li>
085     * <li>Can Add/Modify annotations: False</li>
086     * <li>Can Fill Forms: False</li>
087     * <li>Can Extract Info for Accessibility: True</li>
088     * <li>Can Assemble: False</li>
089     * <li>Can print degraded: True</li>
090     * </ul>
091     * <p>
092     * <b>IMPORTANT
093     * </p>
094     * It is required that the following setters are called <i>before</i>:
095     * <ul>
096     * <li>{@link PDFEncryption#setOriginalOwnerPwd}: Only if the original PDF already is encrypted. This password
097     * allows to open it for modification.</li>
098     * <li>{@link PDFEncryption#setKeyLength}: To set the length of the key.</li>
099     * <li>{@link PDFEncryption#setOwnerPwd}: The password for the owner. If not called, <code>originalOwnerPwd</code>
100     * is used instead.</li>
101     * <li>{@link PDFEncryption#setUserPwd}: The password for the user.</li>
102     * </ul>
103     *
104     * @return A copy of the blob with the readonly permissions set.
105     */
106    public Blob encryptReadOnly() {
107        AccessPermission ap = new AccessPermission();
108        ap.setCanPrint(true);
109        ap.setCanModify(false);
110        ap.setCanExtractContent(true);
111        ap.setCanModifyAnnotations(false);
112        ap.setCanFillInForm(false);
113        ap.setCanExtractForAccessibility(true);
114        ap.setCanAssembleDocument(false);
115        ap.setCanPrintDegraded(true);
116        return encrypt(ap);
117    }
118
119    /**
120     * Encrypts the PDF with the new permissions (see {@link AccessPermission}).
121     * <p>
122     * <b>IMPORTANT
123     * </p>
124     * It is required that the following setters are called <i>before</i>:
125     * <ul>
126     * <li>{@link PDFEncryption#setOriginalOwnerPwd}: Only if the original PDF already is encrypted. This password
127     * allows to open it for modification.</li>
128     * <li>{@link PDFEncryption#setKeyLength}: To set the length of the key.</li>
129     * <li>{@link PDFEncryption#setOwnerPwd}: The password for the owner. If not called, <code>originalOwnerPwd</code>
130     * is used instead.</li>
131     * <li>{@link PDFEncryption#setUserPwd}: The password for the user.</li>
132     * </ul>
133     *
134     * @param inPerm Input permissions.
135     * @return A copy of the blob with the new permissions set.
136     */
137    public Blob encrypt(AccessPermission inPerm) {
138        if (!ALLOWED_LENGTH.contains(keyLength)) {
139            throw new NuxeoException(keyLength + " is not an allowed length for the encrytion key");
140        }
141        ownerPwd = (StringUtils.isBlank(ownerPwd)) ? originalOwnerPwd : ownerPwd;
142        try {
143            StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPwd, userPwd, inPerm);
144            spp.setEncryptionKeyLength(keyLength);
145            pdfDoc = PDDocument.load(pdfBlob.getFile());
146            pdfDoc.protect(spp);
147            Blob result = Blobs.createBlobWithExtension(".pdf");
148            pdfDoc.save(result.getFile());
149            result.setMimeType("application/pdf");
150            if (StringUtils.isNotBlank(pdfBlob.getFilename())) {
151                result.setFilename(pdfBlob.getFilename());
152            }
153            pdfDoc.close();
154            FileBlob fb = new FileBlob(result.getFile());
155            fb.setMimeType("application/pdf");
156            return fb;
157        } catch (Exception e) {
158            throw new NuxeoException("Failed to encrypt the PDF", e);
159        }
160    }
161
162    /**
163     * Removes all protection from the PDF, returns a copy of it. If the PDF was not encrypted, just returns a copy of
164     * it with no changes.
165     * <p>
166     * <b>IMPORTANT
167     * </p>
168     * If the PDF is encrypted, it is required for {@link PDFEncryption#setOriginalOwnerPwd} to be called before to
169     * <code>removeEncryption</code>.
170     * <ul>
171     * <li>{@link PDFEncryption#setOriginalOwnerPwd}: Only if the original PDF already is encrypted. This password
172     * allows to open it for modification.</li>
173     * <li>{@link PDFEncryption#setKeyLength}: To set the length of the key.</li>
174     * <li>{@link PDFEncryption#setOwnerPwd}: The password for the owner. If not called, <code>originalOwnerPwd</code>
175     * is used instead.</li>
176     * <li>{@link PDFEncryption#setUserPwd}: The password for the user.</li>
177     * </ul>
178     */
179    public Blob removeEncryption() {
180        try {
181            String password = (StringUtils.isBlank(originalOwnerPwd)) ? ownerPwd : originalOwnerPwd;
182            pdfDoc = PDDocument.load(pdfBlob.getFile());
183            if (!pdfDoc.isEncrypted()) {
184                pdfDoc.close();
185                return pdfBlob;
186            }
187            pdfDoc.openProtection(new StandardDecryptionMaterial(password));
188            pdfDoc.setAllSecurityToBeRemoved(true);
189            Blob result = Blobs.createBlobWithExtension(".pdf");
190            pdfDoc.save(result.getFile());
191            result.setMimeType("application/pdf");
192            if (StringUtils.isNotBlank(pdfBlob.getFilename())) {
193                result.setFilename(pdfBlob.getFilename());
194            }
195            pdfDoc.close();
196            FileBlob fb = new FileBlob(result.getFile());
197            fb.setMimeType("application/pdf");
198            return fb;
199        } catch (Exception e) {
200            throw new NuxeoException("Failed to remove encryption of the PDF", e);
201        }
202    }
203
204    /**
205     * Set the lentgh of the key to be used for encryption.
206     * <p>
207     * Possible values are 40 and 128. Default value is 128 if <code>keyLength</code> is <= 0.
208     *
209     * @param keyLength Lenght of the encryption key.
210     */
211    public void setKeyLength(int keyLength) throws NuxeoException {
212        if (keyLength < 1) {
213            keyLength = DEFAULT_KEYLENGTH;
214        } else {
215            if (!ALLOWED_LENGTH.contains(keyLength)) {
216                throw new NuxeoException("Cannot use " + keyLength + " is not allowed as lenght for the encrytion key");
217            }
218        }
219        this.keyLength = keyLength;
220    }
221
222    /**
223     * Set the password to use when opening a protected PDF. Must be called <i>before</i> encrypting the PDF.
224     *
225     * @param originalOwnerPwd Original owner password.
226     */
227    public void setOriginalOwnerPwd(String originalOwnerPwd) {
228        this.originalOwnerPwd = originalOwnerPwd;
229    }
230
231    /**
232     * Set the owner password to use when encrypting PDF. Must be called <i>before</i> encrypting the PDF.
233     * <p>
234     * Owners can do whatever they want to the PDF (modify, change protection, ...).
235     *
236     * @param ownerPwd Owner password.
237     */
238    public void setOwnerPwd(String ownerPwd) {
239        this.ownerPwd = ownerPwd;
240    }
241
242    /**
243     * Set the user password to use when encrypting PDF. Must be called <i>before</i> encrypting the PDF.
244     * <p>
245     * Users can have less rights than owners (for example, not being able to remove protection).
246     *
247     * @param userPwd User password.
248     */
249    public void setUserPwd(String userPwd) {
250        this.userPwd = userPwd;
251    }
252
253}