diff options
author | Maxim Valyanskiy <maxcom@apache.org> | 2010-05-27 13:23:27 +0000 |
---|---|---|
committer | Maxim Valyanskiy <maxcom@apache.org> | 2010-05-27 13:23:27 +0000 |
commit | e100cb756f13824185d40eccc7beaac2b44c59ab (patch) | |
tree | ae0d551416e936c5405db50aae556e4dde8b2f62 | |
parent | 0565100c36ea4c4195732b833974b4295f3ccb14 (diff) | |
download | poi-e100cb756f13824185d40eccc7beaac2b44c59ab.tar.gz poi-e100cb756f13824185d40eccc7beaac2b44c59ab.zip |
Initial support for reading AES-encrypted/write-protected OOXML files
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@948825 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r-- | src/java/org/apache/poi/poifs/crypt/Decryptor.java | 133 | ||||
-rw-r--r-- | src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java | 97 | ||||
-rw-r--r-- | src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java | 72 | ||||
-rw-r--r-- | src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java | 57 | ||||
-rw-r--r-- | src/testcases/org/apache/poi/poifs/crypt/DecryptorTest.java | 68 | ||||
-rw-r--r-- | src/testcases/org/apache/poi/poifs/crypt/EncryptionInfoTest.java | 45 | ||||
-rw-r--r-- | test-data/poifs/protect.xlsx | bin | 0 -> 12968 bytes |
7 files changed, 472 insertions, 0 deletions
diff --git a/src/java/org/apache/poi/poifs/crypt/Decryptor.java b/src/java/org/apache/poi/poifs/crypt/Decryptor.java new file mode 100644 index 0000000000..a47100d696 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/Decryptor.java @@ -0,0 +1,133 @@ +/* ==================================================================== + 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()); + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java new file mode 100644 index 0000000000..893f8cc9c5 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java @@ -0,0 +1,97 @@ +/* ==================================================================== + 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; + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java new file mode 100644 index 0000000000..713e9d0543 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java @@ -0,0 +1,72 @@ +/* ==================================================================== + 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; + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java new file mode 100644 index 0000000000..0eccf338f0 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java @@ -0,0 +1,57 @@ +/* ==================================================================== + 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; + } +} diff --git a/src/testcases/org/apache/poi/poifs/crypt/DecryptorTest.java b/src/testcases/org/apache/poi/poifs/crypt/DecryptorTest.java new file mode 100644 index 0000000000..bb317d77c8 --- /dev/null +++ b/src/testcases/org/apache/poi/poifs/crypt/DecryptorTest.java @@ -0,0 +1,68 @@ +/* ==================================================================== + 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()); + } + } + } +} diff --git a/src/testcases/org/apache/poi/poifs/crypt/EncryptionInfoTest.java b/src/testcases/org/apache/poi/poifs/crypt/EncryptionInfoTest.java new file mode 100644 index 0000000000..eb84727e33 --- /dev/null +++ b/src/testcases/org/apache/poi/poifs/crypt/EncryptionInfoTest.java @@ -0,0 +1,45 @@ +/* ==================================================================== + 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); + } +} diff --git a/test-data/poifs/protect.xlsx b/test-data/poifs/protect.xlsx Binary files differnew file mode 100644 index 0000000000..1767b14377 --- /dev/null +++ b/test-data/poifs/protect.xlsx |