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