mirror of https://github.com/apache/poi.git
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
This commit is contained in:
parent
0565100c36
commit
e100cb756f
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Binary file not shown.
Loading…
Reference in New Issue