| /* ==================================================================== |
| 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; |
| |
| import java.io.IOException; |
| import java.nio.charset.StandardCharsets; |
| |
| import org.apache.poi.EncryptedDocumentException; |
| import org.apache.poi.poifs.crypt.standard.EncryptionRecord; |
| import org.apache.poi.poifs.filesystem.DirectoryEntry; |
| import org.apache.poi.poifs.filesystem.DocumentEntry; |
| import org.apache.poi.poifs.filesystem.POIFSWriterEvent; |
| import org.apache.poi.poifs.filesystem.POIFSWriterListener; |
| import org.apache.poi.util.IOUtils; |
| import org.apache.poi.util.LittleEndianByteArrayOutputStream; |
| import org.apache.poi.util.LittleEndianConsts; |
| import org.apache.poi.util.LittleEndianInput; |
| import org.apache.poi.util.LittleEndianOutput; |
| import org.apache.poi.util.StringUtil; |
| |
| public class DataSpaceMapUtils { |
| |
| //arbitrarily selected; may need to increase |
| private static final int MAX_RECORD_LENGTH = 100_000; |
| |
| public static void addDefaultDataSpace(DirectoryEntry dir) throws IOException { |
| DataSpaceMapEntry dsme = new DataSpaceMapEntry( |
| new int[]{ 0 } |
| , new String[]{ Decryptor.DEFAULT_POIFS_ENTRY } |
| , "StrongEncryptionDataSpace" |
| ); |
| DataSpaceMap dsm = new DataSpaceMap(new DataSpaceMapEntry[]{dsme}); |
| createEncryptionEntry(dir, "\u0006DataSpaces/DataSpaceMap", dsm); |
| |
| DataSpaceDefinition dsd = new DataSpaceDefinition(new String[]{ "StrongEncryptionTransform" }); |
| createEncryptionEntry(dir, "\u0006DataSpaces/DataSpaceInfo/StrongEncryptionDataSpace", dsd); |
| |
| TransformInfoHeader tih = new TransformInfoHeader( |
| 1 |
| , "{FF9A3F03-56EF-4613-BDD5-5A41C1D07246}" |
| , "Microsoft.Container.EncryptionTransform" |
| , 1, 0, 1, 0, 1, 0 |
| ); |
| IRMDSTransformInfo irm = new IRMDSTransformInfo(tih, 0, null); |
| createEncryptionEntry(dir, "\u0006DataSpaces/TransformInfo/StrongEncryptionTransform/\u0006Primary", irm); |
| |
| DataSpaceVersionInfo dsvi = new DataSpaceVersionInfo("Microsoft.Container.DataSpaces", 1, 0, 1, 0, 1, 0); |
| createEncryptionEntry(dir, "\u0006DataSpaces/Version", dsvi); |
| } |
| |
| public static DocumentEntry createEncryptionEntry(DirectoryEntry dir, String path, EncryptionRecord out) throws IOException { |
| String[] parts = path.split("/"); |
| for (int i=0; i<parts.length-1; i++) { |
| dir = dir.hasEntry(parts[i]) |
| ? (DirectoryEntry)dir.getEntry(parts[i]) |
| : dir.createDirectory(parts[i]); |
| } |
| |
| final byte[] buf = new byte[5000]; |
| LittleEndianByteArrayOutputStream bos = new LittleEndianByteArrayOutputStream(buf, 0); |
| out.write(bos); |
| |
| String fileName = parts[parts.length-1]; |
| |
| if (dir.hasEntry(fileName)) { |
| dir.getEntry(fileName).delete(); |
| } |
| |
| return dir.createDocument(fileName, bos.getWriteIndex(), new POIFSWriterListener(){ |
| public void processPOIFSWriterEvent(POIFSWriterEvent event) { |
| try { |
| event.getStream().write(buf, 0, event.getLimit()); |
| } catch (IOException e) { |
| throw new EncryptedDocumentException(e); |
| } |
| } |
| }); |
| } |
| |
| public static class DataSpaceMap implements EncryptionRecord { |
| DataSpaceMapEntry[] entries; |
| |
| public DataSpaceMap(DataSpaceMapEntry[] entries) { |
| this.entries = entries.clone(); |
| } |
| |
| public DataSpaceMap(LittleEndianInput is) { |
| /*int length = */ is.readInt(); |
| int entryCount = is.readInt(); |
| entries = new DataSpaceMapEntry[entryCount]; |
| for (int i=0; i<entryCount; i++) { |
| entries[i] = new DataSpaceMapEntry(is); |
| } |
| } |
| |
| public void write(LittleEndianByteArrayOutputStream os) { |
| os.writeInt(8); |
| os.writeInt(entries.length); |
| for (DataSpaceMapEntry dsme : entries) { |
| dsme.write(os); |
| } |
| } |
| } |
| |
| public static class DataSpaceMapEntry implements EncryptionRecord { |
| final int[] referenceComponentType; |
| final String[] referenceComponent; |
| final String dataSpaceName; |
| |
| public DataSpaceMapEntry(int[] referenceComponentType, String[] referenceComponent, String dataSpaceName) { |
| this.referenceComponentType = referenceComponentType.clone(); |
| this.referenceComponent = referenceComponent.clone(); |
| this.dataSpaceName = dataSpaceName; |
| } |
| |
| public DataSpaceMapEntry(LittleEndianInput is) { |
| /*int length = */ is.readInt(); |
| int referenceComponentCount = is.readInt(); |
| referenceComponentType = new int[referenceComponentCount]; |
| referenceComponent = new String[referenceComponentCount]; |
| for (int i=0; i<referenceComponentCount; i++) { |
| referenceComponentType[i] = is.readInt(); |
| referenceComponent[i] = readUnicodeLPP4(is); |
| } |
| dataSpaceName = readUnicodeLPP4(is); |
| } |
| |
| public void write(LittleEndianByteArrayOutputStream os) { |
| int start = os.getWriteIndex(); |
| LittleEndianOutput sizeOut = os.createDelayedOutput(LittleEndianConsts.INT_SIZE); |
| os.writeInt(referenceComponent.length); |
| for (int i=0; i<referenceComponent.length; i++) { |
| os.writeInt(referenceComponentType[i]); |
| writeUnicodeLPP4(os, referenceComponent[i]); |
| } |
| writeUnicodeLPP4(os, dataSpaceName); |
| sizeOut.writeInt(os.getWriteIndex()-start); |
| } |
| } |
| |
| public static class DataSpaceDefinition implements EncryptionRecord { |
| String[] transformer; |
| |
| public DataSpaceDefinition(String[] transformer) { |
| this.transformer = transformer.clone(); |
| } |
| |
| public DataSpaceDefinition(LittleEndianInput is) { |
| /* int headerLength = */ is.readInt(); |
| int transformReferenceCount = is.readInt(); |
| transformer = new String[transformReferenceCount]; |
| for (int i=0; i<transformReferenceCount; i++) { |
| transformer[i] = readUnicodeLPP4(is); |
| } |
| } |
| |
| public void write(LittleEndianByteArrayOutputStream bos) { |
| bos.writeInt(8); |
| bos.writeInt(transformer.length); |
| for (String str : transformer) { |
| writeUnicodeLPP4(bos, str); |
| } |
| } |
| } |
| |
| public static class IRMDSTransformInfo implements EncryptionRecord { |
| TransformInfoHeader transformInfoHeader; |
| int extensibilityHeader; |
| String xrMLLicense; |
| |
| public IRMDSTransformInfo(TransformInfoHeader transformInfoHeader, int extensibilityHeader, String xrMLLicense) { |
| this.transformInfoHeader = transformInfoHeader; |
| this.extensibilityHeader = extensibilityHeader; |
| this.xrMLLicense = xrMLLicense; |
| } |
| |
| public IRMDSTransformInfo(LittleEndianInput is) { |
| transformInfoHeader = new TransformInfoHeader(is); |
| extensibilityHeader = is.readInt(); |
| xrMLLicense = readUtf8LPP4(is); |
| // finish with 0x04 (int) ??? |
| } |
| |
| public void write(LittleEndianByteArrayOutputStream bos) { |
| transformInfoHeader.write(bos); |
| bos.writeInt(extensibilityHeader); |
| writeUtf8LPP4(bos, xrMLLicense); |
| bos.writeInt(4); // where does this 4 come from??? |
| } |
| } |
| |
| public static class TransformInfoHeader implements EncryptionRecord { |
| int transformType; |
| String transformerId; |
| String transformerName; |
| int readerVersionMajor = 1, readerVersionMinor; |
| int updaterVersionMajor = 1, updaterVersionMinor; |
| int writerVersionMajor = 1, writerVersionMinor; |
| |
| public TransformInfoHeader( |
| int transformType, |
| String transformerId, |
| String transformerName, |
| int readerVersionMajor, int readerVersionMinor, |
| int updaterVersionMajor, int updaterVersionMinor, |
| int writerVersionMajor, int writerVersionMinor |
| ){ |
| this.transformType = transformType; |
| this.transformerId = transformerId; |
| this.transformerName = transformerName; |
| this.readerVersionMajor = readerVersionMajor; |
| this.readerVersionMinor = readerVersionMinor; |
| this.updaterVersionMajor = updaterVersionMajor; |
| this.updaterVersionMinor = updaterVersionMinor; |
| this.writerVersionMajor = writerVersionMajor; |
| this.writerVersionMinor = writerVersionMinor; |
| } |
| |
| public TransformInfoHeader(LittleEndianInput is) { |
| /* int length = */ is.readInt(); |
| transformType = is.readInt(); |
| transformerId = readUnicodeLPP4(is); |
| transformerName = readUnicodeLPP4(is); |
| readerVersionMajor = is.readShort(); |
| readerVersionMinor = is.readShort(); |
| updaterVersionMajor = is.readShort(); |
| updaterVersionMinor = is.readShort(); |
| writerVersionMajor = is.readShort(); |
| writerVersionMinor = is.readShort(); |
| } |
| |
| public void write(LittleEndianByteArrayOutputStream bos) { |
| int start = bos.getWriteIndex(); |
| LittleEndianOutput sizeOut = bos.createDelayedOutput(LittleEndianConsts.INT_SIZE); |
| bos.writeInt(transformType); |
| writeUnicodeLPP4(bos, transformerId); |
| sizeOut.writeInt(bos.getWriteIndex()-start); |
| writeUnicodeLPP4(bos, transformerName); |
| bos.writeShort(readerVersionMajor); |
| bos.writeShort(readerVersionMinor); |
| bos.writeShort(updaterVersionMajor); |
| bos.writeShort(updaterVersionMinor); |
| bos.writeShort(writerVersionMajor); |
| bos.writeShort(writerVersionMinor); |
| } |
| } |
| |
| public static class DataSpaceVersionInfo implements EncryptionRecord { |
| String featureIdentifier; |
| int readerVersionMajor = 1, readerVersionMinor; |
| int updaterVersionMajor = 1, updaterVersionMinor; |
| int writerVersionMajor = 1, writerVersionMinor; |
| |
| public DataSpaceVersionInfo(LittleEndianInput is) { |
| featureIdentifier = readUnicodeLPP4(is); |
| readerVersionMajor = is.readShort(); |
| readerVersionMinor = is.readShort(); |
| updaterVersionMajor = is.readShort(); |
| updaterVersionMinor = is.readShort(); |
| writerVersionMajor = is.readShort(); |
| writerVersionMinor = is.readShort(); |
| } |
| |
| public DataSpaceVersionInfo( |
| String featureIdentifier, |
| int readerVersionMajor, int readerVersionMinor, |
| int updaterVersionMajor, int updaterVersionMinor, |
| int writerVersionMajor, int writerVersionMinor |
| ){ |
| this.featureIdentifier = featureIdentifier; |
| this.readerVersionMajor = readerVersionMajor; |
| this.readerVersionMinor = readerVersionMinor; |
| this.updaterVersionMajor = updaterVersionMajor; |
| this.updaterVersionMinor = updaterVersionMinor; |
| this.writerVersionMajor = writerVersionMajor; |
| this.writerVersionMinor = writerVersionMinor; |
| } |
| |
| public void write(LittleEndianByteArrayOutputStream bos) { |
| writeUnicodeLPP4(bos, featureIdentifier); |
| bos.writeShort(readerVersionMajor); |
| bos.writeShort(readerVersionMinor); |
| bos.writeShort(updaterVersionMajor); |
| bos.writeShort(updaterVersionMinor); |
| bos.writeShort(writerVersionMajor); |
| bos.writeShort(writerVersionMinor); |
| } |
| } |
| |
| public static String readUnicodeLPP4(LittleEndianInput is) { |
| int length = is.readInt(); |
| if (length%2 != 0) { |
| throw new EncryptedDocumentException( |
| "UNICODE-LP-P4 structure is a multiple of 4 bytes. " |
| + "If Padding is present, it MUST be exactly 2 bytes long"); |
| } |
| |
| String result = StringUtil.readUnicodeLE(is, length/2); |
| if (length%4==2) { |
| // Padding (variable): A set of bytes that MUST be of the correct size such that the size of the |
| // UNICODE-LP-P4 structure is a multiple of 4 bytes. If Padding is present, it MUST be exactly |
| // 2 bytes long, and each byte MUST be 0x00. |
| is.readShort(); |
| } |
| |
| return result; |
| } |
| |
| public static void writeUnicodeLPP4(LittleEndianOutput os, String string) { |
| byte[] buf = StringUtil.getToUnicodeLE(string); |
| os.writeInt(buf.length); |
| os.write(buf); |
| if (buf.length%4==2) { |
| os.writeShort(0); |
| } |
| } |
| |
| public static String readUtf8LPP4(LittleEndianInput is) { |
| int length = is.readInt(); |
| if (length == 0 || length == 4) { |
| /* int skip = */ is.readInt(); |
| return length == 0 ? null : ""; |
| } |
| |
| byte[] data = IOUtils.safelyAllocate(length, MAX_RECORD_LENGTH); |
| is.readFully(data); |
| |
| // Padding (variable): A set of bytes that MUST be of correct size such that the size of the UTF-8-LP-P4 |
| // structure is a multiple of 4 bytes. If Padding is present, each byte MUST be 0x00. If |
| // the length is exactly 0x00000000, this specifies a null string, and the entire structure uses |
| // exactly 4 bytes. If the length is exactly 0x00000004, this specifies an empty string, and the |
| // entire structure also uses exactly 4 bytes |
| int scratchedBytes = length%4; |
| if (scratchedBytes > 0) { |
| for (int i=0; i<(4-scratchedBytes); i++) { |
| is.readByte(); |
| } |
| } |
| |
| return new String(data, 0, data.length, StandardCharsets.UTF_8); |
| } |
| |
| public static void writeUtf8LPP4(LittleEndianOutput os, String str) { |
| if (str == null || str.isEmpty()) { |
| os.writeInt(str == null ? 0 : 4); |
| os.writeInt(0); |
| } else { |
| byte[] buf = str.getBytes(StandardCharsets.UTF_8); |
| os.writeInt(buf.length); |
| os.write(buf); |
| int scratchBytes = buf.length%4; |
| if (scratchBytes > 0) { |
| for (int i=0; i<(4-scratchBytes); i++) { |
| os.writeByte(0); |
| } |
| } |
| } |
| } |
| |
| } |