EncryptDecryptFuzzer.java

// Copyright 2023 Google LLC
//
// Licensed 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;

import static org.apache.poi.poifs.crypt.Decryptor.DEFAULT_POIFS_ENTRY;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.util.Arrays;

import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
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.Encryptor;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.util.HexDump;
import org.apache.poi.util.IOUtils;

import com.code_intelligence.jazzer.api.FuzzedDataProvider;

public class EncryptDecryptFuzzer {
	public static void fuzzerTestOneInput(FuzzedDataProvider data) throws IOException, GeneralSecurityException {
		try {
			EncryptionMode encryptionMode = EncryptionMode.values()[(data.consumeInt(0, EncryptionMode.values().length - 1))];
			String password = data.consumeString(20);
			byte[] testData = data.consumeRemainingAsBytes();

			EncryptionInfo infoEnc = new EncryptionInfo(encryptionMode);
			Encryptor enc = infoEnc.getEncryptor();
			enc.confirmPassword(password);

			byte[] encData;
			byte[] encDocument;
			try (POIFSFileSystem fsEnc = new POIFSFileSystem()) {
				try (OutputStream os = enc.getDataStream(fsEnc)) {
					os.write(testData);
				}

				UnsynchronizedByteArrayOutputStream bos = UnsynchronizedByteArrayOutputStream.builder().get();
				fsEnc.writeFilesystem(bos);

				bos.close();
				encData = bos.toByteArray();

				DocumentInputStream dis = fsEnc.getRoot().createDocumentInputStream(DEFAULT_POIFS_ENTRY);
				/*long _length =*/
				dis.readLong();
				encDocument = IOUtils.toByteArray(dis);
			}

			byte[] actualData;
			try (POIFSFileSystem fsDec = new POIFSFileSystem(new ByteArrayInputStream(encData))) {
				EncryptionInfo infoDec = new EncryptionInfo(fsDec);
				Decryptor dec = infoDec.getDecryptor();
				if (!dec.verifyPassword(password)) {
					throw new IllegalStateException("Could not verify password when decrypting " + password + "\n" +
							HexDump.dump(testData, 0, 0) + " and encrypted \n" +
							HexDump.dump(encDocument, 0, 0) + " full encrypted \n" +
							HexDump.dump(encData, 0, 0));
				}
				InputStream is = dec.getDataStream(fsDec);

				actualData = IOUtils.toByteArray(is);
				is.close();
			}

			// input-data and resulting decrypted data should be equal
			if (!Arrays.equals(testData, actualData)) {
				throw new IllegalStateException("Resulting array was not equal to input-array, "
						+ "having " + testData.length + " bytes, had actual length " + actualData.length + " and expected \n" +
						HexDump.dump(testData, 0, 0) + " and actual \n" +
						HexDump.dump(actualData, 0, 0) + " encrypted \n" +
						HexDump.dump(encDocument, 0, 0) + " full encrypted \n" +
						HexDump.dump(encData, 0, 0));
			}
		} catch (IOException | EncryptedDocumentException e) {
			// expected, e.g. when something is not supported
		}
	}
}