blob: ed2ee87e02328c55ee06d73b2c8beff44dd16c0f [file] [log] [blame]
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 org.apache.poi.poifs.crypt.tests;
import static org.apache.poi.poifs.crypt.CryptoFunctions.getMessageDigest;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.DigestInputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.Iterator;
import java.util.Random;
import javax.crypto.Cipher;
import org.apache.poi.POIDataSamples;
import org.apache.poi.openxml4j.opc.ContentTypes;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.Decryptor;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionMode;
import org.apache.poi.poifs.crypt.EncryptionVerifier;
import org.apache.poi.poifs.crypt.Encryptor;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.crypt.agile.AgileDecryptor;
import org.apache.poi.poifs.crypt.agile.AgileEncryptionHeader;
import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentEntry;
import org.apache.poi.poifs.filesystem.DocumentNode;
import org.apache.poi.poifs.filesystem.Entry;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.poifs.filesystem.TempFilePOIFSFileSystem;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.NullOutputStream;
import org.apache.poi.util.TempFile;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class TestEncryptor {
@Test
void binaryRC4Encryption() throws Exception {
// please contribute a real sample file, which is binary rc4 encrypted
// ... at least the output can be opened in Excel Viewer
String password = "pass";
final byte[] payloadExpected;
try (InputStream is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream("SimpleMultiCell.xlsx")) {
payloadExpected = IOUtils.toByteArray(is);
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (POIFSFileSystem fs = new POIFSFileSystem()) {
EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4);
Encryptor enc = ei.getEncryptor();
enc.confirmPassword(password);
try (OutputStream os = enc.getDataStream(fs.getRoot())) {
os.write(payloadExpected);
}
fs.writeFilesystem(bos);
}
final byte[] payloadActual;
try (POIFSFileSystem fs = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray()))) {
EncryptionInfo ei = new EncryptionInfo(fs);
Decryptor dec = ei.getDecryptor();
boolean b = dec.verifyPassword(password);
assertTrue(b);
try (InputStream is = dec.getDataStream(fs.getRoot())) {
payloadActual = IOUtils.toByteArray(is);
}
}
assertArrayEquals(payloadExpected, payloadActual);
}
@Test
void tempFileAgileEncryption() throws Exception {
String password = "pass";
final byte[] payloadExpected;
try (InputStream is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream("SimpleMultiCell.xlsx")) {
payloadExpected = IOUtils.toByteArray(is);
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (POIFSFileSystem fs = new TempFilePOIFSFileSystem()) {
EncryptionInfo ei = new EncryptionInfo(EncryptionMode.agile);
Encryptor enc = ei.getEncryptor();
enc.confirmPassword(password);
try (OutputStream os = enc.getDataStream(fs.getRoot())) {
os.write(payloadExpected);
}
fs.writeFilesystem(bos);
}
final byte[] payloadActual;
try (POIFSFileSystem fs = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray()))) {
EncryptionInfo ei = new EncryptionInfo(fs);
Decryptor dec = ei.getDecryptor();
boolean b = dec.verifyPassword(password);
assertTrue(b);
try (InputStream is = dec.getDataStream(fs.getRoot())) {
payloadActual = IOUtils.toByteArray(is);
}
}
assertArrayEquals(payloadExpected, payloadActual);
}
@Test
void agileEncryption() throws Exception {
int maxKeyLen = Cipher.getMaxAllowedKeyLength("AES");
assumeTrue(maxKeyLen == 0x7FFFFFFF, "Please install JCE Unlimited Strength Jurisdiction Policy files for AES 256");
File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-pass.docx");
String pass = "pass";
final byte[] payloadExpected, encPackExpected;
final long decPackLenExpected;
final EncryptionInfo infoExpected;
final Decryptor decExpected;
try (POIFSFileSystem nfs = new POIFSFileSystem(file, true)) {
// Check the encryption details
infoExpected = new EncryptionInfo(nfs);
decExpected = Decryptor.getInstance(infoExpected);
boolean passed = decExpected.verifyPassword(pass);
assertTrue(passed, "Unable to process: document is encrypted");
// extract the payload
try (InputStream is = decExpected.getDataStream(nfs)) {
payloadExpected = IOUtils.toByteArray(is);
}
decPackLenExpected = decExpected.getLength();
assertEquals(decPackLenExpected, payloadExpected.length);
final DirectoryNode root = nfs.getRoot();
final DocumentEntry entry = (DocumentEntry)root.getEntry(Decryptor.DEFAULT_POIFS_ENTRY);
try (InputStream is = root.createDocumentInputStream(entry)) {
// ignore padding block
encPackExpected = IOUtils.toByteArray(is, entry.getSize()-16);
}
}
// check that same verifier/salt lead to same hashes
final byte[] verifierSaltExpected = infoExpected.getVerifier().getSalt();
final byte[] verifierExpected = decExpected.getVerifier();
final byte[] keySalt = infoExpected.getHeader().getKeySalt();
final byte[] keySpec = decExpected.getSecretKey().getEncoded();
final byte[] integritySalt = decExpected.getIntegrityHmacKey();
// the hmacs of the file always differ, as we use PKCS5-padding to pad the bytes
// whereas office just uses random bytes
// byte integrityHash[] = d.getIntegrityHmacValue();
final EncryptionInfo infoActual = new EncryptionInfo(
EncryptionMode.agile
, infoExpected.getVerifier().getCipherAlgorithm()
, infoExpected.getVerifier().getHashAlgorithm()
, infoExpected.getHeader().getKeySize()
, infoExpected.getHeader().getBlockSize()
, infoExpected.getVerifier().getChainingMode()
);
Encryptor e = Encryptor.getInstance(infoActual);
e.confirmPassword(pass, keySpec, keySalt, verifierExpected, verifierSaltExpected, integritySalt);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (POIFSFileSystem fs = new POIFSFileSystem()) {
try (OutputStream os = e.getDataStream(fs)) {
os.write(payloadExpected);
}
fs.writeFilesystem(bos);
}
final EncryptionInfo infoActual2;
final byte[] payloadActual, encPackActual;
final long decPackLenActual;
try (POIFSFileSystem nfs = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray()))) {
infoActual2 = new EncryptionInfo(nfs.getRoot());
Decryptor decActual = Decryptor.getInstance(infoActual2);
boolean passed = decActual.verifyPassword(pass);
assertTrue(passed, "Unable to process: document is encrypted");
// extract the payload
try (InputStream is = decActual.getDataStream(nfs)) {
payloadActual = IOUtils.toByteArray(is);
}
decPackLenActual = decActual.getLength();
final DirectoryNode root = nfs.getRoot();
final DocumentEntry entry = (DocumentEntry)root.getEntry(Decryptor.DEFAULT_POIFS_ENTRY);
try (InputStream is = root.createDocumentInputStream(entry)) {
// ignore padding block
encPackActual = IOUtils.toByteArray(is, entry.getSize()-16);
}
}
AgileEncryptionHeader aehExpected = (AgileEncryptionHeader)infoExpected.getHeader();
AgileEncryptionHeader aehActual = (AgileEncryptionHeader)infoActual.getHeader();
assertArrayEquals(aehExpected.getEncryptedHmacKey(), aehActual.getEncryptedHmacKey());
assertEquals(decPackLenExpected, decPackLenActual);
assertArrayEquals(payloadExpected, payloadActual);
assertArrayEquals(encPackExpected, encPackActual);
}
@Test
void standardEncryption() throws Exception {
File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-solrcell.docx");
final String pass = "solrcell";
final byte[] payloadExpected;
final EncryptionInfo infoExpected;
final Decryptor d;
try (POIFSFileSystem nfs = new POIFSFileSystem(file, true)) {
// Check the encryption details
infoExpected = new EncryptionInfo(nfs);
d = Decryptor.getInstance(infoExpected);
boolean passed = d.verifyPassword(pass);
assertTrue(passed, "Unable to process: document is encrypted");
// extract the payload
try (InputStream is = d.getDataStream(nfs)) {
payloadExpected = IOUtils.toByteArray(is);
}
}
// check that same verifier/salt lead to same hashes
final byte[] verifierSaltExpected = infoExpected.getVerifier().getSalt();
final byte[] verifierExpected = d.getVerifier();
final byte[] keySpec = d.getSecretKey().getEncoded();
final byte[] keySalt = infoExpected.getHeader().getKeySalt();
final EncryptionInfo infoActual = new EncryptionInfo(
EncryptionMode.standard
, infoExpected.getVerifier().getCipherAlgorithm()
, infoExpected.getVerifier().getHashAlgorithm()
, infoExpected.getHeader().getKeySize()
, infoExpected.getHeader().getBlockSize()
, infoExpected.getVerifier().getChainingMode()
);
final Encryptor e = Encryptor.getInstance(infoActual);
e.confirmPassword(pass, keySpec, keySalt, verifierExpected, verifierSaltExpected, null);
assertArrayEquals(infoExpected.getVerifier().getEncryptedVerifier(), infoActual.getVerifier().getEncryptedVerifier());
assertArrayEquals(infoExpected.getVerifier().getEncryptedVerifierHash(), infoActual.getVerifier().getEncryptedVerifierHash());
// now we use a newly generated salt/verifier and check
// if the file content is still the same
final byte[] encBytes;
try (POIFSFileSystem fs = new POIFSFileSystem()) {
final EncryptionInfo infoActual2 = new EncryptionInfo(
EncryptionMode.standard
, infoExpected.getVerifier().getCipherAlgorithm()
, infoExpected.getVerifier().getHashAlgorithm()
, infoExpected.getHeader().getKeySize()
, infoExpected.getHeader().getBlockSize()
, infoExpected.getVerifier().getChainingMode()
);
final Encryptor e2 = Encryptor.getInstance(infoActual2);
e2.confirmPassword(pass);
try (OutputStream os = e2.getDataStream(fs)) {
os.write(payloadExpected);
}
final ByteArrayOutputStream bos = new ByteArrayOutputStream(50000);
fs.writeFilesystem(bos);
encBytes = bos.toByteArray();
}
final byte[] payloadActual;
try (POIFSFileSystem nfs = new POIFSFileSystem(new ByteArrayInputStream(encBytes))) {
final EncryptionInfo ei = new EncryptionInfo(nfs);
Decryptor d2 = Decryptor.getInstance(ei);
assertTrue(d2.verifyPassword(pass), "Unable to process: document is encrypted");
try (InputStream is = d2.getDataStream(nfs)) {
payloadActual = IOUtils.toByteArray(is);
}
}
assertArrayEquals(payloadExpected, payloadActual);
}
/**
* Ensure we can encrypt a package that is missing the Core
* Properties, eg one from dodgy versions of Jasper Reports
* See https://github.com/nestoru/xlsxenc/ and
* http://stackoverflow.com/questions/28593223
*/
@Test
void encryptPackageWithoutCoreProperties() throws Exception {
// Open our file without core properties
final byte[] encBytes;
try (InputStream is = POIDataSamples.getOpenXML4JInstance().openResourceAsStream("OPCCompliance_NoCoreProperties.xlsx");
OPCPackage pkg = OPCPackage.open(is)) {
// It doesn't have any core properties yet
assertEquals(0, pkg.getPartsByContentType(ContentTypes.CORE_PROPERTIES_PART).size());
assertNotNull(pkg.getPackageProperties());
assertNotNull(pkg.getPackageProperties().getLanguageProperty());
assertFalse(pkg.getPackageProperties().getLanguageProperty().isPresent());
// Encrypt it
EncryptionInfo info = new EncryptionInfo(EncryptionMode.agile);
Encryptor enc = info.getEncryptor();
enc.confirmPassword("password");
try (POIFSFileSystem fs = new POIFSFileSystem()) {
try (OutputStream os = enc.getDataStream(fs)) {
pkg.save(os);
}
// Save the resulting OLE2 document, and re-open it
ByteArrayOutputStream baos = new ByteArrayOutputStream();
fs.writeFilesystem(baos);
encBytes = baos.toByteArray();
}
}
try (POIFSFileSystem inpFS = new POIFSFileSystem(new ByteArrayInputStream(encBytes))) {
// Check we can decrypt it
EncryptionInfo info = new EncryptionInfo(inpFS);
Decryptor d = Decryptor.getInstance(info);
assertTrue(d.verifyPassword("password"));
try (OPCPackage inpPkg = OPCPackage.open(d.getDataStream(inpFS))) {
// Check it now has empty core properties
assertEquals(1, inpPkg.getPartsByContentType(ContentTypes.CORE_PROPERTIES_PART).size());
assertNotNull(inpPkg.getPackageProperties());
assertNotNull(inpPkg.getPackageProperties().getLanguageProperty());
assertFalse(inpPkg.getPackageProperties().getLanguageProperty().isPresent());
}
}
}
@Test
@Disabled
void inPlaceRewrite() throws Exception {
File f = TempFile.createTempFile("protected_agile", ".docx");
try (FileOutputStream fos = new FileOutputStream(f);
InputStream fis = POIDataSamples.getPOIFSInstance().openResourceAsStream("protected_agile.docx")) {
IOUtils.copy(fis, fos);
}
try (POIFSFileSystem fs = new POIFSFileSystem(f, false)) {
// decrypt the protected file - in this case it was encrypted with the default password
EncryptionInfo encInfo = new EncryptionInfo(fs);
Decryptor d = encInfo.getDecryptor();
boolean b = d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
assertTrue(b);
try (InputStream docIS = d.getDataStream(fs);
XWPFDocument docx = new XWPFDocument(docIS)) {
// do some strange things with it ;)
XWPFParagraph p = docx.getParagraphArray(0);
p.insertNewRun(0).setText("POI was here! All your base are belong to us!");
p.insertNewRun(1).addBreak();
// and encrypt it again
Encryptor e = encInfo.getEncryptor();
e.confirmPassword("AYBABTU");
try (OutputStream os = e.getDataStream(fs)) {
docx.write(os);
}
}
}
}
private void listEntry(DocumentNode de, String ext, String path) throws IOException {
path += "\\" + de.getName().replaceAll("[\\p{Cntrl}]", "_");
System.out.println(ext+": "+path+" ("+de.getSize()+" bytes)");
String name = de.getName().replaceAll("[\\p{Cntrl}]", "_");
InputStream is = ((DirectoryNode)de.getParent()).createDocumentInputStream(de);
FileOutputStream fos = new FileOutputStream("solr."+name+"."+ext);
IOUtils.copy(is, fos);
fos.close();
is.close();
}
@SuppressWarnings("unused")
private void listDir(DirectoryNode dn, String ext, String path) throws IOException {
path += "\\" + dn.getName().replace('\u0006', '_');
System.out.println(ext+": "+path+" ("+dn.getStorageClsid()+")");
Iterator<Entry> iter = dn.getEntries();
while (iter.hasNext()) {
Entry ent = iter.next();
if (ent instanceof DirectoryNode) {
listDir((DirectoryNode)ent, ext, path);
} else {
listEntry((DocumentNode)ent, ext, path);
}
}
}
/*
* this test simulates the generation of bugs 60320 sample file
* as the padding bytes of the EncryptedPackage stream are random or in POIs case PKCS5-padded
* one would need to mock those bytes to get the same hmacValues - see diff below
*
* this use-case is experimental - for the time being the setters of the encryption classes
* are spreaded between two packages and are protected - so you would need to violate
* the packages rules and provide a helper class in the *poifs.crypt package-namespace.
* the default way of defining the encryption settings is via the EncryptionInfo class
*/
@Test
void bug60320CustomEncrypt() throws Exception {
int maxKeyLen = Cipher.getMaxAllowedKeyLength("AES");
assumeTrue(maxKeyLen == 0x7FFFFFFF, "Please install JCE Unlimited Strength Jurisdiction Policy files for AES 256");
// --- src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java (revision 1766745)
// +++ src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java (working copy)
// @@ -208,6 +208,13 @@
// protected int invokeCipher(int posInChunk, boolean doFinal) throws GeneralSecurityException {
// byte plain[] = (_plainByteFlags.isEmpty()) ? null : _chunk.clone();
//
// + if (posInChunk < 4096) {
// + _cipher.update(_chunk, 0, posInChunk, _chunk);
// + byte bla[] = { (byte)0x7A,(byte)0x0F,(byte)0x27,(byte)0xF0,(byte)0x17,(byte)0x6E,(byte)0x77,(byte)0x05,(byte)0xB9,(byte)0xDA,(byte)0x49,(byte)0xF9,(byte)0xD7,(byte)0x8E,(byte)0x03,(byte)0x1D };
// + System.arraycopy(bla, 0, _chunk, posInChunk-2, bla.length);
// + return posInChunk-2+bla.length;
// + }
// +
// int ciLen = (doFinal)
// ? _cipher.doFinal(_chunk, 0, posInChunk, _chunk)
// : _cipher.update(_chunk, 0, posInChunk, _chunk);
//
// --- src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java (revision 1766745)
// +++ src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java (working copy)
//
// @@ -300,7 +297,7 @@
// protected static Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk, EncryptionInfo encryptionInfo, SecretKey skey, int encryptionMode)
// throws GeneralSecurityException {
// EncryptionHeader header = encryptionInfo.getHeader();
// - String padding = (lastChunk ? "PKCS5Padding" : "NoPadding");
// + String padding = "NoPadding"; // (lastChunk ? "PKCS5Padding" : "NoPadding");
// if (existing == null || !existing.getAlgorithm().endsWith(padding)) {
// existing = getCipher(skey, header.getCipherAlgorithm(), header.getChainingMode(), header.getKeySalt(), encryptionMode, padding);
// }
final EncryptionInfo infoOrig;
final byte[] zipInput, epOrigBytes;
try (InputStream is = POIDataSamples.getPOIFSInstance().openResourceAsStream("60320-protected.xlsx");
POIFSFileSystem fsOrig = new POIFSFileSystem(is)) {
infoOrig = new EncryptionInfo(fsOrig);
Decryptor decOrig = infoOrig.getDecryptor();
boolean b = decOrig.verifyPassword("Test001!!");
assertTrue(b);
try (InputStream decIn = decOrig.getDataStream(fsOrig)) {
zipInput = IOUtils.toByteArray(decIn);
}
try (InputStream epOrig = fsOrig.getRoot().createDocumentInputStream("EncryptedPackage")) {
// ignore the 16 padding bytes
epOrigBytes = IOUtils.toByteArray(epOrig, 9400);
}
}
EncryptionInfo eiNew = new EncryptionInfo(EncryptionMode.agile);
AgileEncryptionHeader aehHeader = (AgileEncryptionHeader)eiNew.getHeader();
aehHeader.setCipherAlgorithm(CipherAlgorithm.aes128);
aehHeader.setHashAlgorithm(HashAlgorithm.sha1);
AgileEncryptionVerifier aehVerifier = (AgileEncryptionVerifier)eiNew.getVerifier();
// this cast might look strange - if the setters would be public, it will become obsolete
// see http://stackoverflow.com/questions/5637650/overriding-protected-methods-in-java
((EncryptionVerifier)aehVerifier).setCipherAlgorithm(CipherAlgorithm.aes256);
aehVerifier.setHashAlgorithm(HashAlgorithm.sha512);
Encryptor enc = eiNew.getEncryptor();
enc.confirmPassword("Test001!!",
infoOrig.getDecryptor().getSecretKey().getEncoded(),
infoOrig.getHeader().getKeySalt(),
infoOrig.getDecryptor().getVerifier(),
infoOrig.getVerifier().getSalt(),
infoOrig.getDecryptor().getIntegrityHmacKey()
);
final byte[] epNewBytes;
final EncryptionInfo infoReload;
try (POIFSFileSystem fsNew = new POIFSFileSystem()) {
try (OutputStream os = enc.getDataStream(fsNew)) {
os.write(zipInput);
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
fsNew.writeFilesystem(bos);
try (POIFSFileSystem fsReload = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray()))) {
infoReload = new EncryptionInfo(fsReload);
try (InputStream epReload = fsReload.getRoot().createDocumentInputStream("EncryptedPackage")) {
epNewBytes = IOUtils.toByteArray(epReload, 9400);
}
}
}
assertArrayEquals(epOrigBytes, epNewBytes);
Decryptor decReload = infoReload.getDecryptor();
assertTrue(decReload.verifyPassword("Test001!!"));
AgileEncryptionHeader aehOrig = (AgileEncryptionHeader)infoOrig.getHeader();
AgileEncryptionHeader aehReload = (AgileEncryptionHeader)infoReload.getHeader();
assertEquals(aehOrig.getBlockSize(), aehReload.getBlockSize());
assertEquals(aehOrig.getChainingMode(), aehReload.getChainingMode());
assertEquals(aehOrig.getCipherAlgorithm(), aehReload.getCipherAlgorithm());
assertEquals(aehOrig.getCipherProvider(), aehReload.getCipherProvider());
assertEquals(aehOrig.getCspName(), aehReload.getCspName());
assertArrayEquals(aehOrig.getEncryptedHmacKey(), aehReload.getEncryptedHmacKey());
// this only works, when the paddings are mocked to be the same ...
// assertArrayEquals(aehOrig.getEncryptedHmacValue(), aehReload.getEncryptedHmacValue());
assertEquals(aehOrig.getFlags(), aehReload.getFlags());
assertEquals(aehOrig.getHashAlgorithm(), aehReload.getHashAlgorithm());
assertArrayEquals(aehOrig.getKeySalt(), aehReload.getKeySalt());
assertEquals(aehOrig.getKeySize(), aehReload.getKeySize());
AgileEncryptionVerifier aevOrig = (AgileEncryptionVerifier)infoOrig.getVerifier();
AgileEncryptionVerifier aevReload = (AgileEncryptionVerifier)infoReload.getVerifier();
assertEquals(aevOrig.getBlockSize(), aevReload.getBlockSize());
assertEquals(aevOrig.getChainingMode(), aevReload.getChainingMode());
assertEquals(aevOrig.getCipherAlgorithm(), aevReload.getCipherAlgorithm());
assertArrayEquals(aevOrig.getEncryptedKey(), aevReload.getEncryptedKey());
assertArrayEquals(aevOrig.getEncryptedVerifier(), aevReload.getEncryptedVerifier());
assertArrayEquals(aevOrig.getEncryptedVerifierHash(), aevReload.getEncryptedVerifierHash());
assertEquals(aevOrig.getHashAlgorithm(), aevReload.getHashAlgorithm());
assertEquals(aevOrig.getKeySize(), aevReload.getKeySize());
assertArrayEquals(aevOrig.getSalt(), aevReload.getSalt());
assertEquals(aevOrig.getSpinCount(), aevReload.getSpinCount());
AgileDecryptor adOrig = (AgileDecryptor)infoOrig.getDecryptor();
AgileDecryptor adReload = (AgileDecryptor)infoReload.getDecryptor();
assertArrayEquals(adOrig.getIntegrityHmacKey(), adReload.getIntegrityHmacKey());
// doesn't work without mocking ... see above
// assertArrayEquals(adOrig.getIntegrityHmacValue(), adReload.getIntegrityHmacValue());
assertArrayEquals(adOrig.getSecretKey().getEncoded(), adReload.getSecretKey().getEncoded());
assertArrayEquals(adOrig.getVerifier(), adReload.getVerifier());
}
@Test
void smallFile() throws IOException, GeneralSecurityException {
// see https://stackoverflow.com/questions/61463301
final int tinyFileSize = 80_000_000;
final String pass = "s3cr3t";
File tmpFile = TempFile.createTempFile("tiny", ".bin");
// create/populate empty file
try (POIFSFileSystem poifs = new POIFSFileSystem();
FileOutputStream fos = new FileOutputStream(tmpFile)) {
poifs.writeFilesystem(fos);
}
EncryptionInfo info1 = new EncryptionInfo(EncryptionMode.agile);
Encryptor enc = info1.getEncryptor();
enc.confirmPassword(pass);
final MessageDigest md = getMessageDigest(HashAlgorithm.sha256);
// reopen as mmap-ed file
try (POIFSFileSystem poifs = new POIFSFileSystem(tmpFile, false)) {
try (OutputStream os = enc.getDataStream(poifs);
RandomStream rs = new RandomStream(md)) {
IOUtils.copy(rs, os, tinyFileSize);
}
poifs.writeFilesystem();
}
final byte[] digest1 = md.digest();
md.reset();
// reopen and check the digest
try (POIFSFileSystem poifs = new POIFSFileSystem(tmpFile)) {
EncryptionInfo info2 = new EncryptionInfo(poifs);
Decryptor dec = info2.getDecryptor();
boolean passOk = dec.verifyPassword(pass);
assertTrue(passOk);
try (InputStream is = dec.getDataStream(poifs);
DigestInputStream dis = new DigestInputStream(is, md);
NullOutputStream nos = new NullOutputStream()) {
IOUtils.copy(dis, nos);
}
}
final byte[] digest2 = md.digest();
assertArrayEquals(digest1, digest2);
boolean isDeleted = tmpFile.delete();
assertTrue(isDeleted);
}
private static final class RandomStream extends InputStream {
private final Random rand = new Random();
private final byte[] buf = new byte[1024];
private final MessageDigest md;
private RandomStream(MessageDigest md) {
this.md = md;
}
@Override
public int read() {
int ret = rand.nextInt(256);
md.update((byte)ret);
return ret;
}
@Override
public int read(byte[] b, final int off, int len) {
for (int start = off; start-off < len; start += buf.length) {
rand.nextBytes(buf);
int copyLen = Math.min(buf.length, len-(start-off));
System.arraycopy(buf, 0, b, start, copyLen);
md.update(buf, 0, copyLen);
}
return len;
}
}
}