--- /dev/null
+/* ====================================================================
+ 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 org.apache.poi.poifs.filesystem.DocumentInputStream;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.util.LittleEndian;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/**
+ * @author Maxim Valyanskiy
+ */
+public class Decryptor {
+ public static final String DEFAULT_PASSWORD="VelvetSweatshop";
+
+ private final EncryptionInfo info;
+ private byte[] passwordHash;
+
+ public Decryptor(EncryptionInfo info) {
+ this.info = info;
+ }
+
+ private void generatePasswordHash(String password) throws NoSuchAlgorithmException {
+ MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+
+ sha1.update(info.getVerifier().getSalt());
+ byte[] hash = sha1.digest(password.getBytes(Charset.forName("UTF-16LE")));
+
+ byte[] iterator = new byte[4];
+ for (int i = 0; i<50000; i++) {
+ sha1.reset();
+
+ LittleEndian.putInt(iterator, i);
+ sha1.update(iterator);
+ hash = sha1.digest(hash);
+ }
+
+ passwordHash = hash;
+ }
+
+ private byte[] generateKey(int block) throws NoSuchAlgorithmException {
+ MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+
+ sha1.update(passwordHash);
+ byte[] blockValue = new byte[4];
+ LittleEndian.putInt(blockValue, block);
+ byte[] finalHash = sha1.digest(blockValue);
+
+ int requiredKeyLength = info.getHeader().getKeySize()/8;
+
+ byte[] buff = new byte[64];
+
+ Arrays.fill(buff, (byte) 0x36);
+
+ for (int i=0; i<finalHash.length; i++) {
+ buff[i] = (byte) (buff[i] ^ finalHash[i]);
+ }
+
+ sha1.reset();
+ byte[] x1 = sha1.digest(buff);
+
+ Arrays.fill(buff, (byte) 0x5c);
+ for (int i=0; i<finalHash.length; i++) {
+ buff[i] = (byte) (buff[i] ^ finalHash[i]);
+ }
+
+ sha1.reset();
+ byte[] x2 = sha1.digest(buff);
+
+ byte[] x3 = new byte[x1.length + x2.length];
+ System.arraycopy(x1, 0, x3, 0, x1.length);
+ System.arraycopy(x2, 0, x3, x1.length, x2.length);
+
+ return Arrays.copyOf(x3, requiredKeyLength);
+ }
+
+ public boolean verifyPassword(String password) throws GeneralSecurityException {
+ generatePasswordHash(password);
+
+ Cipher cipher = getCipher();
+
+ byte[] verifier = cipher.doFinal(info.getVerifier().getVerifier());
+
+ MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+ byte[] calcVerifierHash = sha1.digest(verifier);
+
+ byte[] verifierHash = Arrays.copyOf(cipher.doFinal(info.getVerifier().getVerifierHash()), calcVerifierHash.length);
+
+ return Arrays.equals(calcVerifierHash, verifierHash);
+ }
+
+ private Cipher getCipher() throws GeneralSecurityException {
+ byte[] key = generateKey(0);
+ Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
+ SecretKey skey = new SecretKeySpec(key, "AES");
+ cipher.init(Cipher.DECRYPT_MODE, skey);
+
+ return cipher;
+ }
+
+ public InputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException {
+ DocumentInputStream dis = fs.createDocumentInputStream("EncryptedPackage");
+
+ long size = dis.readLong();
+
+ return new CipherInputStream(dis, getCipher());
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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 org.apache.poi.poifs.filesystem.DocumentInputStream;
+
+import java.io.IOException;
+
+/**
+ * @author Maxim Valyanskiy
+ */
+public class EncryptionHeader {
+ public static final int ALGORITHM_RC4 = 0x6801;
+ public static final int ALGORITHM_AES_128 = 0x660E;
+ public static final int ALGORITHM_AES_192 = 0x660F;
+ public static final int ALGORITHM_AES_256 = 0x6610;
+
+ public static final int HASH_SHA1 = 0x8004;
+
+ public static final int PROVIDER_RC4 = 1;
+ public static final int PROVIDER_AES = 0x18;
+
+ private final int flags;
+ private final int sizeExtra;
+ private final int algorithm;
+ private final int hashAlgorithm;
+ private final int keySize;
+ private final int providerType;
+ private final String cspName;
+
+ public EncryptionHeader(DocumentInputStream is) throws IOException {
+ flags = is.readInt();
+ sizeExtra = is.readInt();
+ algorithm = is.readInt();
+ hashAlgorithm = is.readInt();
+ keySize = is.readInt();
+ providerType = is.readInt();
+
+ is.readLong(); // skip reserved
+
+ StringBuilder builder = new StringBuilder();
+
+ while (true) {
+ char c = (char) is.readShort();
+
+ if (c == 0) {
+ break;
+ }
+
+ builder.append(c);
+ }
+
+ cspName = builder.toString();
+ }
+
+ public int getFlags() {
+ return flags;
+ }
+
+ public int getSizeExtra() {
+ return sizeExtra;
+ }
+
+ public int getAlgorithm() {
+ return algorithm;
+ }
+
+ public int getHashAlgorithm() {
+ return hashAlgorithm;
+ }
+
+ public int getKeySize() {
+ return keySize;
+ }
+
+ public int getProviderType() {
+ return providerType;
+ }
+
+ public String getCspName() {
+ return cspName;
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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 org.apache.poi.poifs.filesystem.DocumentInputStream;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+
+import java.io.IOException;
+
+/**
+ * @author Maxim Valyanskiy
+ */
+public class EncryptionInfo {
+ private final int versionMajor;
+ private final int versionMinor;
+ private final int encryptionFlags;
+
+ private final EncryptionHeader header;
+ private final EncryptionVerifier verifier;
+
+ public EncryptionInfo(POIFSFileSystem fs) throws IOException {
+ DocumentInputStream dis = fs.createDocumentInputStream("EncryptionInfo");
+
+ versionMajor = dis.readShort();
+ versionMinor = dis.readShort();
+ encryptionFlags = dis.readInt();
+
+ int hSize = dis.readInt();
+
+ header = new EncryptionHeader(dis);
+
+ if (header.getAlgorithm()==EncryptionHeader.ALGORITHM_RC4) {
+ verifier = new EncryptionVerifier(dis, 20);
+ } else {
+ verifier = new EncryptionVerifier(dis, 32);
+ }
+ }
+
+ public int getVersionMajor() {
+ return versionMajor;
+ }
+
+ public int getVersionMinor() {
+ return versionMinor;
+ }
+
+ public int getEncryptionFlags() {
+ return encryptionFlags;
+ }
+
+ public EncryptionHeader getHeader() {
+ return header;
+ }
+
+ public EncryptionVerifier getVerifier() {
+ return verifier;
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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 org.apache.poi.poifs.filesystem.DocumentInputStream;
+
+/**
+ * @author Maxim Valyanskiy
+ */
+public class EncryptionVerifier {
+ private final byte[] salt = new byte[16];
+ private final byte[] verifier = new byte[16];
+ private final byte[] verifierHash;
+ private final int verifierHashSize;
+
+ public EncryptionVerifier(DocumentInputStream is, int encryptedLength) {
+ int saltSize = is.readInt();
+
+ if (saltSize!=16) {
+ throw new RuntimeException("Salt size != 16 !?");
+ }
+
+ is.readFully(salt);
+ is.readFully(verifier);
+
+ verifierHashSize = is.readInt();
+
+ verifierHash = new byte[encryptedLength];
+ is.readFully(verifierHash);
+ }
+
+ public byte[] getSalt() {
+ return salt;
+ }
+
+ public byte[] getVerifier() {
+ return verifier;
+ }
+
+ public byte[] getVerifierHash() {
+ return verifierHash;
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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 junit.framework.TestCase;
+import org.apache.poi.POIDataSamples;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ * @author Maxim Valyanskiy
+ */
+public class DecryptorTest extends TestCase {
+ public void testPasswordVerification() throws IOException, GeneralSecurityException {
+ POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
+
+ EncryptionInfo info = new EncryptionInfo(fs);
+
+ Decryptor d = new Decryptor(info);
+
+ assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
+ }
+
+ public void testDecrypt() throws IOException, GeneralSecurityException {
+ POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
+
+ EncryptionInfo info = new EncryptionInfo(fs);
+
+ Decryptor d = new Decryptor(info);
+
+ d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
+
+ zipOk(fs, d);
+ }
+
+ private void zipOk(POIFSFileSystem fs, Decryptor d) throws IOException, GeneralSecurityException {
+ ZipInputStream zin = new ZipInputStream(d.getDataStream(fs));
+
+ while (true) {
+ ZipEntry entry = zin.getNextEntry();
+ if (entry==null) {
+ break;
+ }
+
+ while (zin.available()>0) {
+ zin.skip(zin.available());
+ }
+ }
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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 junit.framework.TestCase;
+import org.apache.poi.POIDataSamples;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+
+import java.io.IOException;
+
+/**
+ * @author Maxim Valyanskiy
+ */
+public class EncryptionInfoTest extends TestCase {
+ public void testEncryptionInfo() throws IOException {
+ POIFSFileSystem fs = new POIFSFileSystem(POIDataSamples.getPOIFSInstance().openResourceAsStream("protect.xlsx"));
+
+ EncryptionInfo info = new EncryptionInfo(fs);
+
+ assertEquals(3, info.getVersionMajor());
+ assertEquals(2, info.getVersionMinor());
+
+ assertEquals(EncryptionHeader.ALGORITHM_AES_128, info.getHeader().getAlgorithm());
+ assertEquals(EncryptionHeader.HASH_SHA1, info.getHeader().getHashAlgorithm());
+ assertEquals(128, info.getHeader().getKeySize());
+ assertEquals(EncryptionHeader.PROVIDER_AES, info.getHeader().getProviderType());
+ assertEquals("Microsoft Enhanced RSA and AES Cryptographic Provider", info.getHeader().getCspName());
+
+ assertEquals(32, info.getVerifier().getVerifierHash().length);
+ }
+}