git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@948825 13f79535-47bb-0310-9956-ffa450edef68tags/REL_3_7_BETA1
@@ -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); | |||
} | |||
} |