]> source.dussan.org Git - poi.git/commitdiff
Initial support for reading AES-encrypted/write-protected OOXML files
authorMaxim Valyanskiy <maxcom@apache.org>
Thu, 27 May 2010 13:23:27 +0000 (13:23 +0000)
committerMaxim Valyanskiy <maxcom@apache.org>
Thu, 27 May 2010 13:23:27 +0000 (13:23 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@948825 13f79535-47bb-0310-9956-ffa450edef68

src/java/org/apache/poi/poifs/crypt/Decryptor.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java [new file with mode: 0644]
src/testcases/org/apache/poi/poifs/crypt/DecryptorTest.java [new file with mode: 0644]
src/testcases/org/apache/poi/poifs/crypt/EncryptionInfoTest.java [new file with mode: 0644]
test-data/poifs/protect.xlsx [new file with mode: 0644]

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 (file)
index 0000000..a47100d
--- /dev/null
@@ -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 (file)
index 0000000..893f8cc
--- /dev/null
@@ -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 (file)
index 0000000..713e9d0
--- /dev/null
@@ -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 (file)
index 0000000..0eccf33
--- /dev/null
@@ -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 (file)
index 0000000..bb317d7
--- /dev/null
@@ -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 (file)
index 0000000..eb84727
--- /dev/null
@@ -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
new file mode 100644 (file)
index 0000000..1767b14
Binary files /dev/null and b/test-data/poifs/protect.xlsx differ