| /* ==================================================================== |
| 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.xor; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.security.GeneralSecurityException; |
| |
| import javax.crypto.Cipher; |
| import javax.crypto.SecretKey; |
| import javax.crypto.spec.SecretKeySpec; |
| |
| import org.apache.poi.EncryptedDocumentException; |
| import org.apache.poi.poifs.crypt.ChunkedCipherInputStream; |
| import org.apache.poi.poifs.crypt.CryptoFunctions; |
| import org.apache.poi.poifs.crypt.Decryptor; |
| import org.apache.poi.poifs.crypt.EncryptionInfo; |
| import org.apache.poi.poifs.filesystem.DirectoryNode; |
| import org.apache.poi.util.LittleEndian; |
| |
| public class XORDecryptor extends Decryptor implements Cloneable { |
| private long length = -1L; |
| private int chunkSize = 512; |
| |
| protected XORDecryptor() { |
| } |
| |
| @Override |
| public boolean verifyPassword(String password) { |
| XOREncryptionVerifier ver = (XOREncryptionVerifier)getEncryptionInfo().getVerifier(); |
| int keyVer = LittleEndian.getUShort(ver.getEncryptedKey()); |
| int verifierVer = LittleEndian.getUShort(ver.getEncryptedVerifier()); |
| int keyComp = CryptoFunctions.createXorKey1(password); |
| int verifierComp = CryptoFunctions.createXorVerifier1(password); |
| if (keyVer == keyComp && verifierVer == verifierComp) { |
| byte xorArray[] = CryptoFunctions.createXorArray1(password); |
| setSecretKey(new SecretKeySpec(xorArray, "XOR")); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| @Override |
| public Cipher initCipherForBlock(Cipher cipher, int block) |
| throws GeneralSecurityException { |
| return null; |
| } |
| |
| protected static Cipher initCipherForBlock(Cipher cipher, int block, |
| EncryptionInfo encryptionInfo, SecretKey skey, int encryptMode) |
| throws GeneralSecurityException { |
| return null; |
| } |
| |
| @Override |
| public ChunkedCipherInputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException { |
| throw new EncryptedDocumentException("not supported"); |
| } |
| |
| @Override |
| public InputStream getDataStream(InputStream stream, int size, int initialPos) |
| throws IOException, GeneralSecurityException { |
| return new XORCipherInputStream(stream, initialPos); |
| } |
| |
| |
| @Override |
| public long getLength() { |
| if (length == -1L) { |
| throw new IllegalStateException("Decryptor.getDataStream() was not called"); |
| } |
| |
| return length; |
| } |
| |
| @Override |
| public void setChunkSize(int chunkSize) { |
| this.chunkSize = chunkSize; |
| } |
| |
| @Override |
| public XORDecryptor clone() throws CloneNotSupportedException { |
| return (XORDecryptor)super.clone(); |
| } |
| |
| private class XORCipherInputStream extends ChunkedCipherInputStream { |
| private final int initialOffset; |
| private int recordStart = 0; |
| private int recordEnd = 0; |
| |
| public XORCipherInputStream(InputStream stream, int initialPos) |
| throws GeneralSecurityException { |
| super(stream, Integer.MAX_VALUE, chunkSize); |
| this.initialOffset = initialPos; |
| } |
| |
| @Override |
| protected Cipher initCipherForBlock(Cipher existing, int block) |
| throws GeneralSecurityException { |
| return XORDecryptor.this.initCipherForBlock(existing, block); |
| } |
| |
| @Override |
| protected int invokeCipher(int totalBytes, boolean doFinal) { |
| final int pos = (int)getPos(); |
| final byte xorArray[] = getEncryptionInfo().getDecryptor().getSecretKey().getEncoded(); |
| final byte chunk[] = getChunk(); |
| final byte plain[] = getPlain(); |
| final int posInChunk = pos & getChunkMask(); |
| |
| /* |
| * From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5 |
| * |
| * The initial value for XorArrayIndex is as follows: |
| * XorArrayIndex = (FileOffset + Data.Length) % 16 |
| * |
| * The FileOffset variable in this context is the stream offset into the Workbook stream at |
| * the time we are about to write each of the bytes of the record data. |
| * This (the value) is then incremented after each byte is written. |
| */ |
| final int xorArrayIndex = initialOffset+recordEnd+(pos-recordStart); |
| |
| for (int i=0; pos+i < recordEnd && i < totalBytes; i++) { |
| // The following is taken from the Libre Office implementation |
| // It seems that the encrypt and decrypt method is mixed up |
| // in the MS-OFFCRYPTO docs |
| byte value = plain[posInChunk+i]; |
| value = rotateLeft(value, 3); |
| value ^= xorArray[(xorArrayIndex+i) & 0x0F]; |
| chunk[posInChunk+i] = value; |
| } |
| |
| // the other bytes will be encoded, when setNextRecordSize is called the next time |
| return totalBytes; |
| } |
| |
| private byte rotateLeft(byte bits, int shift) { |
| return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift))); |
| } |
| |
| |
| /** |
| * Decrypts a xor obfuscated byte array. |
| * The data is decrypted in-place |
| * |
| * @see <a href="http://msdn.microsoft.com/en-us/library/dd908506.aspx">2.3.7.3 Binary Document XOR Data Transformation Method 1</a> |
| */ |
| @Override |
| public void setNextRecordSize(int recordSize) { |
| final int pos = (int)getPos(); |
| final byte chunk[] = getChunk(); |
| final int chunkMask = getChunkMask(); |
| recordStart = pos; |
| recordEnd = recordStart+recordSize; |
| int nextBytes = Math.min(recordSize, chunk.length-(pos & chunkMask)); |
| invokeCipher(nextBytes, true); |
| } |
| } |
| } |