Search Apache POI

Apache POI - Encryption support

Overview#

Apache POI contains support for reading few variants of encrypted office files:

  • Binary formats (.xls, .ppt, .doc, ...)
    encryption is format-dependent and needs to be implemented per format differently.
    Use Biff8EncryptionKey.setCurrentUserPassword(String password) to specify the decryption password before opening the file or (where applicable) before saving. Setting a null password before saving removes the password protection.
    The password is set in a thread local variable. Do not forget to reset it to null after text extraction.
  • XML-based formats (.xlsx, .pptx, .docx, ...)
    use the same encryption logic over all formats. When encrypted, the zipped files will be stored within an OLE file in the EncryptedPackage stream.
    If you plan to use POI to actually generate encrypted documents, be aware not to use anything less than agile encryption, because RC4 is not really secure. Of course you'll need to make sure, that your clients can read the documents, i.e. the various free Excel, Powerpoint, Word viewers have limitations in the cipher or hashing parameters.
    If you want to use high encryption parameters, you need to install the "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files" for your JRE version (Oracle JDK6, JDK7, JDK8).

Some "write-protected" files are encrypted with the built-in password "VelvetSweatshop", POI can read that files too.

Supported feature matrix#

Encryption HSSF HSLF HWPF
XOR obfuscation *) Yes (Writing since 3.16) N/A No
40-bit RC4 encryption Yes (Writing since 3.16) N/A Yes (since 3.17)
Office Binary Document RC4 CryptoAPI Encryption Yes (Since 3.16) Yes Yes (since 3.17)
XSSF XSLF XWPF
Office Binary Document RC4 Encryption **) Yes Yes Yes
ECMA-376 Standard Encryption Yes Yes Yes
ECMA-376 Agile Encryption Yes Yes Yes
ECMA-376 XML Signature Yes Yes Yes

*) the xor encryption is flawed and works only for very small files - see #59857.

**) the MS-OFFCRYPTO documentation only mentions the RC4 (without CryptoAPI) encryption as a "in place" encryption, but apparently there's also a container based method with that key generation logic.

Binary formats#

As mentioned above, use Biff8EncryptionKey.setCurrentUserPassword(String password) to specify the password.

// XOR/RC4 decryption for xls
Biff8EncryptionKey.setCurrentUserPassword("pass");
NPOIFSFileSystem fs = new NPOIFSFileSystem(new File("file.xls"), true);
HSSFWorkbook hwb = new HSSFWorkbook(fs.getRoot(), true);
Biff8EncryptionKey.setCurrentUserPassword(null);
        
// RC4 CryptoApi support ppt - decryption
Biff8EncryptionKey.setCurrentUserPassword("pass");
NPOIFSFileSystem fs = new NPOIFSFileSystem(new File("file.ppt"), true);
HSLFSlideShow hss = new HSLFSlideShow(fs);
...
// Option 1: remove password
Biff8EncryptionKey.setCurrentUserPassword(null);
OutputStream os = new FileOutputStream("decrypted.ppt");
hss.write(os);
os.close();
...
// Option 2: change encryption settings (experimental)
// need to cache data (i.e. read all data) before changing the key size
PictureData picsExpected[] = hss.getPictures();
hss.getDocumentSummaryInformation();
EncryptionInfo ei = hss.getDocumentEncryptionAtom().getEncryptionInfo();
((CryptoAPIEncryptionHeader)ei.getHeader()).setKeySize(0x78);
OutputStream os = new FileOutputStream("file_120bit.ppt");
hss.write(os);
os.close();
        

XML-based formats - Decryption#

XML-based formats are stored in OLE-package stream "EncryptedPackage". Use org.apache.poi.poifs.crypt.Decryptor to decode file:

EncryptionInfo info = new EncryptionInfo(filesystem);
Decryptor d = Decryptor.getInstance(info);

try {
    if (!d.verifyPassword(password)) {
        throw new RuntimeException("Unable to process: document is encrypted");
    }

    InputStream dataStream = d.getDataStream(filesystem);

    // parse dataStream

} catch (GeneralSecurityException ex) {
    throw new RuntimeException("Unable to process encrypted document", ex);
}
    

If you want to read file encrypted with build-in password, use Decryptor.DEFAULT_PASSWORD.

XML-based formats - Encryption#

Encrypting a file is similar to the above decryption process. Basically you'll need to choose between binaryRC4, standard and agile encryption, the cryptoAPI mode is used internally and it's direct use would result in an incomplete file. Apart of the CipherMode, the EncryptionInfo class provides further parameters to specify the cipher and hashing algorithm to be used.

POIFSFileSystem fs = new POIFSFileSystem();
EncryptionInfo info = new EncryptionInfo(EncryptionMode.agile);
// EncryptionInfo info = new EncryptionInfo(EncryptionMode.agile, CipherAlgorithm.aes192, HashAlgorithm.sha384, -1, -1, null);

Encryptor enc = info.getEncryptor();
enc.confirmPassword("foobaa");

// Read in an existing OOXML file
OPCPackage opc = OPCPackage.open(new File("..."), PackageAccess.READ_WRITE);
OutputStream os = enc.getDataStream(fs);
opc.save(os);
opc.close();

// Write out the encrypted version
FileOutputStream fos = new FileOutputStream("...");
fs.writeFilesystem(fos);
fos.close();
     

XML-based formats - Signing (XML Signature)#

An Office document can be digital signed by a XML Signature to protect it from unauthorized modifications, i.e. modifications without having the original certificate. The current implementation is based on the eID Applet which is dual-licensed to Apache License 2.0 and LGPL v3.0. Instead of using the internal JDK API this version is based on Apache Santuario.

The classes have been tested against the following libraries, which need to be included additionally to the default dependencies:

  • BouncyCastle bcpkix and bcprov (tested against 1.58)
  • Apache Santuario "xmlsec" (tested against 2.1.0)
  • and slf4j-api (tested against 1.7.25)

Depending on the configuration and the activated facets various XAdES levels are supported - the support for higher levels (XAdES-T+) depend on supporting services and although the code is adopted, the integration is not well tested ... please support us on integration (testing) with timestamp and revocation (OCSP) services.

Further test examples can be found in the corresponding test class.

Validating a signed office document#

OPCPackage pkg = OPCPackage.open(..., PackageAccess.READ);
SignatureConfig sic = new SignatureConfig();
sic.setOpcPackage(pkg);
SignatureInfo si = new SignatureInfo();
si.setSignatureConfig(sic);
boolean isValid = si.verifySignature();
...
     

Signing an office document#

Signing a file#

// loading the keystore - pkcs12 is used here, but of course jks & co are also valid
// the keystore needs to contain a private key and it's certificate having a
// 'digitalSignature' key usage
char password[] = "test".toCharArray();
File file = new File("test.pfx");
KeyStore keystore = KeyStore.getInstance("PKCS12");
FileInputStream fis = new FileInputStream(file);
keystore.load(fis, password);
fis.close();

// extracting private key and certificate
String alias = "xyz"; // alias of the keystore entry
Key key = keystore.getKey(alias, password);
X509Certificate x509 = (X509Certificate)keystore.getCertificate(alias);

// filling the SignatureConfig entries (minimum fields, more options are available ...)
SignatureConfig signatureConfig = new SignatureConfig();
signatureConfig.setKey(keyPair.getPrivate());
signatureConfig.setSigningCertificateChain(Collections.singletonList(x509));
OPCPackage pkg = OPCPackage.open(..., PackageAccess.READ_WRITE);
signatureConfig.setOpcPackage(pkg);

// adding the signature document to the package
SignatureInfo si = new SignatureInfo();
si.setSignatureConfig(signatureConfig);
si.confirmSignature();
// optionally verify the generated signature
boolean b = si.verifySignature();
assert (b);
// write the changes back to disc
pkg.close();
     

Signing a stream - in-memory#

When saving a OOXML document, POI creates missing relations on the fly. Therefore calling the signing method before would result in an invalid signature. Instead of trying to fix all save invocations, the user is asked to save the stream before in a intermediate byte array (stream) and process this stream instead.

// load the key and setup SignatureConfig ... - see "Signing a file"

SignatureInfo si = new SignatureInfo();
si.setSignatureConfig(signatureConfig);

// populate sample object
XSSFWorkbook wb = new XSSFWorkbook();
wb.createSheet().createRow(1).createCell(1).setCellValue("Test");
ByteArrayOutputStream bos = new ByteArrayOutputStream(100000);
wb.write(bos);
wb.close();

// process the
OPCPackage pkg = OPCPackage.open(new ByteArrayInputStream(bos.toByteArray()));

signatureConfig.setOpcPackage(pkg);
si.confirmSignature();
bos.reset();
pkg.save(bos);
pkg.close();

// bos now contains the signed ooxml document
     

Encrypting temporary files created when unzipping an OOXML document#

For security-conscious environments where data at rest must be stored encrypted, the creation of plaintext temporary files is a grey area.

The code example, written by PJ Fanning, modifies the behavior of SXSSFWorkbook to extract an OOXML spreadsheet zipped container and write the contents to disk using AES encryption.

See SXSSFWorkbookWithCustomZipEntrySource.java and other files that are needed for this example.

Debugging XML signature issues#

Finding the source of a XML signature problem can be sometimes a pain in the ... neck, because the hashing of the canonicalized form is more or less intransparent done in the background.

One of the tripping hazards are different linebreaks in Windows/Unix, therefore use the non-indent form of the xmls.

The next thing is to compare successful signed documents from Office vs. POIs generated signature, i.e. unzip both files and look for differences. Usually the package relations (*.rels) will be different, and the sig1.xml, core.xml and [Content_Types].xml due to different order of the references.

The package relationsships (*.rels) will be specially handled, i.e. they will be filtered and only a subset will be processed - see 13.2.4.24 Relationships Transform Algorithm.

To check the processed files in the canonicalized form, the below UnsyncBufferedOutputStream class needs to be injected/replaced. Put the .class file in separate directory and add the following JVM parameters:

-Djava.io.tmpdir=<custom temp directory>
-Xbootclasspath/p:<preload dir, which contains /org/apache/xml/security/utils/UnsyncBufferedOutputStream.class>
-Dorg.apache.poi.util.POILogger=org.apache.poi.util.CommonsLogger
-Djava.util.logging.config.file=<a dir containing ...>/logging.properties
       

UnsyncBufferedOutputStream:#

package org.apache.xml.security.utils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class UnsyncBufferedOutputStream extends OutputStream {
    static final int size = 8*1024;
    static int filecnt = 0;

    private int pointer = 0;
    private final OutputStream out;
    private final FileOutputStream out2;

    private final byte[] buf;

    public UnsyncBufferedOutputStream(OutputStream out) {
        buf = new byte[size];
        this.out = out;
        synchronized(UnsyncBufferedOutputStream.class) {
            try {
                String tmpDir = System.getProperty("java.io.tmpdir");
                if (tmpDir == null) {
                    tmpDir = "build";
                }
                File f = new File(tmpDir, "unsync-"+filecnt+".xml");
                out2 = new FileOutputStream(f);
            } catch (IOException e) {
                throw new RuntimeException(e);
            } finally {
                filecnt++;
            }
        }
    }

    public void write(byte[] arg0) throws IOException {
        write(arg0, 0, arg0.length);
    }

    public void write(byte[] arg0, int arg1, int len) throws IOException {
        int newLen = pointer+len;
        if (newLen > size) {
            flushBuffer();
            if (len > size) {
                out.write(arg0, arg1,len);
                out2.write(arg0, arg1,len);
                return;
            }
            newLen = len;
        }
        System.arraycopy(arg0, arg1, buf, pointer, len);
        pointer = newLen;
    }

    private void flushBuffer() throws IOException {
        if (pointer > 0) {
            out.write(buf, 0, pointer);
            out2.write(buf, 0, pointer);
        }
        pointer = 0;

    }

    public void write(int arg0) throws IOException {
        if (pointer >= size) {
            flushBuffer();
        }
        buf[pointer++] = (byte)arg0;

    }

    public void flush() throws IOException {
        flushBuffer();
        out.flush();
        out2.flush();
    }

    public void close() throws IOException {
        flush();
        out.close();
        out2.close();
    }

}

logging.properties#

handlers = org.slf4j.bridge.SLF4JBridgeHandler
.level=ALL
org.slf4j.bridge.SLF4JBridgeHandler.level=ALL
     
by Maxim Valyanskiy, Andreas Beeker