git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1767399 13f79535-47bb-0310-9956-ffa450edef68pull/35/merge
@@ -210,6 +210,7 @@ public class TestAllFiles { | |||
//EXPECTED_FAILURES.add("poifs/extenxls_pwd123.xlsx"); | |||
//EXPECTED_FAILURES.add("poifs/protected_agile.docx"); | |||
EXPECTED_FAILURES.add("spreadsheet/58616.xlsx"); | |||
EXPECTED_FAILURES.add("poifs/60320-protected.xlsx"); | |||
// TODO: fails XMLExportTest, is this ok? | |||
EXPECTED_FAILURES.add("spreadsheet/CustomXMLMapping-singleattributenamespace.xlsx"); |
@@ -16,6 +16,9 @@ | |||
==================================================================== */ | |||
package org.apache.poi.poifs.crypt; | |||
import org.apache.poi.EncryptedDocumentException; | |||
import org.apache.poi.util.Removal; | |||
/** | |||
* Reads and processes OOXML Encryption Headers | |||
* The constants are largely based on ZIP constants. | |||
@@ -82,8 +85,19 @@ public abstract class EncryptionHeader implements Cloneable { | |||
protected void setCipherAlgorithm(CipherAlgorithm cipherAlgorithm) { | |||
this.cipherAlgorithm = cipherAlgorithm; | |||
if (cipherAlgorithm.allowedKeySize.length == 1) { | |||
setKeySize(cipherAlgorithm.defaultKeySize); | |||
} | |||
} | |||
public HashAlgorithm getHashAlgorithm() { | |||
return hashAlgorithm; | |||
} | |||
/** | |||
* @deprecated POI 3.16 beta 1. use {@link #getHashAlgorithm()} | |||
*/ | |||
@Removal(version="3.18") | |||
public HashAlgorithm getHashAlgorithmEx() { | |||
return hashAlgorithm; | |||
} | |||
@@ -96,8 +110,21 @@ public abstract class EncryptionHeader implements Cloneable { | |||
return keyBits; | |||
} | |||
/** | |||
* Sets the keySize (in bits). Before calling this method, make sure | |||
* to set the cipherAlgorithm, as the amount of keyBits gets validated against | |||
* the list of allowed keyBits of the corresponding cipherAlgorithm | |||
* | |||
* @param keyBits | |||
*/ | |||
protected void setKeySize(int keyBits) { | |||
this.keyBits = keyBits; | |||
for (int allowedBits : getCipherAlgorithm().allowedKeySize) { | |||
if (allowedBits == keyBits) { | |||
return; | |||
} | |||
} | |||
throw new EncryptedDocumentException("KeySize "+keyBits+" not allowed for cipher "+getCipherAlgorithm()); | |||
} | |||
public int getBlockSize() { |
@@ -16,6 +16,8 @@ | |||
==================================================================== */ | |||
package org.apache.poi.poifs.crypt; | |||
import org.apache.poi.util.Removal; | |||
/** | |||
* Used when checking if a key is valid for a document | |||
*/ | |||
@@ -48,10 +50,17 @@ public abstract class EncryptionVerifier implements Cloneable { | |||
return spinCount; | |||
} | |||
/** | |||
* @deprecated POI 3.16 beta 1. use {@link #getChainingMode()} | |||
*/ | |||
@Removal(version="3.18") | |||
public int getCipherMode() { | |||
return chainingMode.ecmaId; | |||
} | |||
/** | |||
* @deprecated POI 3.16 beta 1. use {@link #getCipherAlgorithm()} | |||
*/ | |||
public int getAlgorithm() { | |||
return cipherAlgorithm.ecmaId; | |||
} |
@@ -108,7 +108,7 @@ public class StandardEncryptionHeader extends EncryptionHeader implements Encryp | |||
bos.writeInt(getFlags()); | |||
bos.writeInt(0); // size extra | |||
bos.writeInt(getCipherAlgorithm().ecmaId); | |||
bos.writeInt(getHashAlgorithmEx().ecmaId); | |||
bos.writeInt(getHashAlgorithm().ecmaId); | |||
bos.writeInt(getKeySize()); | |||
bos.writeInt(getCipherProvider().ecmaId); | |||
bos.writeInt(0); // reserved1 |
@@ -56,7 +56,7 @@ public class StandardEncryptionVerifier extends EncryptionVerifier implements En | |||
setCipherAlgorithm(header.getCipherAlgorithm()); | |||
setChainingMode(header.getChainingMode()); | |||
setEncryptedKey(null); | |||
setHashAlgorithm(header.getHashAlgorithmEx()); | |||
setHashAlgorithm(header.getHashAlgorithm()); | |||
} | |||
protected StandardEncryptionVerifier(CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) { |
@@ -40,13 +40,13 @@ import javax.crypto.spec.RC2ParameterSpec; | |||
import javax.crypto.spec.SecretKeySpec; | |||
import org.apache.poi.EncryptedDocumentException; | |||
import org.apache.poi.poifs.crypt.ChainingMode; | |||
import org.apache.poi.poifs.crypt.ChunkedCipherInputStream; | |||
import org.apache.poi.poifs.crypt.CipherAlgorithm; | |||
import org.apache.poi.poifs.crypt.CryptoFunctions; | |||
import org.apache.poi.poifs.crypt.Decryptor; | |||
import org.apache.poi.poifs.crypt.EncryptionHeader; | |||
import org.apache.poi.poifs.crypt.EncryptionInfo; | |||
import org.apache.poi.poifs.crypt.EncryptionVerifier; | |||
import org.apache.poi.poifs.crypt.HashAlgorithm; | |||
import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry; | |||
import org.apache.poi.poifs.filesystem.DirectoryNode; | |||
@@ -93,10 +93,8 @@ public class AgileDecryptor extends Decryptor implements Cloneable { | |||
public boolean verifyPassword(String password) throws GeneralSecurityException { | |||
AgileEncryptionVerifier ver = (AgileEncryptionVerifier)getEncryptionInfo().getVerifier(); | |||
AgileEncryptionHeader header = (AgileEncryptionHeader)getEncryptionInfo().getHeader(); | |||
HashAlgorithm hashAlgo = header.getHashAlgorithmEx(); | |||
CipherAlgorithm cipherAlgo = header.getCipherAlgorithm(); | |||
int blockSize = header.getBlockSize(); | |||
int keySize = header.getKeySize()/8; | |||
byte[] pwHash = hashPassword(password, ver.getHashAlgorithm(), ver.getSalt(), ver.getSpinCount()); | |||
@@ -113,9 +111,9 @@ public class AgileDecryptor extends Decryptor implements Cloneable { | |||
* blockSize bytes. | |||
* 4. Use base64 to encode the result of step 3. | |||
*/ | |||
byte verfierInputEnc[] = hashInput(getEncryptionInfo(), pwHash, kVerifierInputBlock, ver.getEncryptedVerifier(), Cipher.DECRYPT_MODE); | |||
byte verfierInputEnc[] = hashInput(ver, pwHash, kVerifierInputBlock, ver.getEncryptedVerifier(), Cipher.DECRYPT_MODE); | |||
setVerifier(verfierInputEnc); | |||
MessageDigest hashMD = getMessageDigest(hashAlgo); | |||
MessageDigest hashMD = getMessageDigest(ver.getHashAlgorithm()); | |||
byte[] verifierHash = hashMD.digest(verfierInputEnc); | |||
/** | |||
@@ -130,8 +128,8 @@ public class AgileDecryptor extends Decryptor implements Cloneable { | |||
* blockSize bytes, pad the hash value with 0x00 to an integral multiple of blockSize bytes. | |||
* 4. Use base64 to encode the result of step 3. | |||
*/ | |||
byte verifierHashDec[] = hashInput(getEncryptionInfo(), pwHash, kHashedVerifierBlock, ver.getEncryptedVerifierHash(), Cipher.DECRYPT_MODE); | |||
verifierHashDec = getBlock0(verifierHashDec, hashAlgo.hashSize); | |||
byte verifierHashDec[] = hashInput(ver, pwHash, kHashedVerifierBlock, ver.getEncryptedVerifierHash(), Cipher.DECRYPT_MODE); | |||
verifierHashDec = getBlock0(verifierHashDec, ver.getHashAlgorithm().hashSize); | |||
/** | |||
* encryptedKeyValue: This attribute MUST be generated by using the following steps: | |||
@@ -146,9 +144,9 @@ public class AgileDecryptor extends Decryptor implements Cloneable { | |||
* blockSize bytes. | |||
* 4. Use base64 to encode the result of step 3. | |||
*/ | |||
byte keyspec[] = hashInput(getEncryptionInfo(), pwHash, kCryptoKeyBlock, ver.getEncryptedKey(), Cipher.DECRYPT_MODE); | |||
keyspec = getBlock0(keyspec, keySize); | |||
SecretKeySpec secretKey = new SecretKeySpec(keyspec, ver.getCipherAlgorithm().jceId); | |||
byte keyspec[] = hashInput(ver, pwHash, kCryptoKeyBlock, ver.getEncryptedKey(), Cipher.DECRYPT_MODE); | |||
keyspec = getBlock0(keyspec, header.getKeySize()/8); | |||
SecretKeySpec secretKey = new SecretKeySpec(keyspec, header.getCipherAlgorithm().jceId); | |||
/** | |||
* 1. Obtain the intermediate key by decrypting the encryptedKeyValue from a KeyEncryptor | |||
@@ -163,10 +161,11 @@ public class AgileDecryptor extends Decryptor implements Cloneable { | |||
* array with 0x00 to the next integral multiple of blockSize bytes. | |||
* 4. Assign the encryptedHmacKey attribute to the base64-encoded form of the result of step 3. | |||
*/ | |||
byte vec[] = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityKeyBlock, blockSize); | |||
Cipher cipher = getCipher(secretKey, cipherAlgo, ver.getChainingMode(), vec, Cipher.DECRYPT_MODE); | |||
byte vec[] = CryptoFunctions.generateIv(header.getHashAlgorithm(), header.getKeySalt(), kIntegrityKeyBlock, blockSize); | |||
CipherAlgorithm cipherAlgo = header.getCipherAlgorithm(); | |||
Cipher cipher = getCipher(secretKey, cipherAlgo, header.getChainingMode(), vec, Cipher.DECRYPT_MODE); | |||
byte hmacKey[] = cipher.doFinal(header.getEncryptedHmacKey()); | |||
hmacKey = getBlock0(hmacKey, hashAlgo.hashSize); | |||
hmacKey = getBlock0(hmacKey, header.getHashAlgorithm().hashSize); | |||
/** | |||
* 5. Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message), | |||
@@ -177,10 +176,10 @@ public class AgileDecryptor extends Decryptor implements Cloneable { | |||
* 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33. | |||
* 7. Assign the encryptedHmacValue attribute to the base64-encoded form of the result of step 6. | |||
*/ | |||
vec = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityValueBlock, blockSize); | |||
vec = CryptoFunctions.generateIv(header.getHashAlgorithm(), header.getKeySalt(), kIntegrityValueBlock, blockSize); | |||
cipher = getCipher(secretKey, cipherAlgo, ver.getChainingMode(), vec, Cipher.DECRYPT_MODE); | |||
byte hmacValue[] = cipher.doFinal(header.getEncryptedHmacValue()); | |||
hmacValue = getBlock0(hmacValue, hashAlgo.hashSize); | |||
hmacValue = getBlock0(hmacValue, header.getHashAlgorithm().hashSize); | |||
if (Arrays.equals(verifierHashDec, verifierHash)) { | |||
setSecretKey(secretKey); | |||
@@ -206,7 +205,7 @@ public class AgileDecryptor extends Decryptor implements Cloneable { | |||
public boolean verifyPassword(KeyPair keyPair, X509Certificate x509) throws GeneralSecurityException { | |||
AgileEncryptionVerifier ver = (AgileEncryptionVerifier)getEncryptionInfo().getVerifier(); | |||
AgileEncryptionHeader header = (AgileEncryptionHeader)getEncryptionInfo().getHeader(); | |||
HashAlgorithm hashAlgo = header.getHashAlgorithmEx(); | |||
HashAlgorithm hashAlgo = header.getHashAlgorithm(); | |||
CipherAlgorithm cipherAlgo = header.getCipherAlgorithm(); | |||
int blockSize = header.getBlockSize(); | |||
@@ -231,12 +230,12 @@ public class AgileDecryptor extends Decryptor implements Cloneable { | |||
byte certVerifier[] = x509Hmac.doFinal(ace.x509.getEncoded()); | |||
byte vec[] = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityKeyBlock, blockSize); | |||
cipher = getCipher(secretKey, cipherAlgo, ver.getChainingMode(), vec, Cipher.DECRYPT_MODE); | |||
cipher = getCipher(secretKey, cipherAlgo, header.getChainingMode(), vec, Cipher.DECRYPT_MODE); | |||
byte hmacKey[] = cipher.doFinal(header.getEncryptedHmacKey()); | |||
hmacKey = getBlock0(hmacKey, hashAlgo.hashSize); | |||
vec = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityValueBlock, blockSize); | |||
cipher = getCipher(secretKey, cipherAlgo, ver.getChainingMode(), vec, Cipher.DECRYPT_MODE); | |||
cipher = getCipher(secretKey, cipherAlgo, header.getChainingMode(), vec, Cipher.DECRYPT_MODE); | |||
byte hmacValue[] = cipher.doFinal(header.getEncryptedHmacValue()); | |||
hmacValue = getBlock0(hmacValue, hashAlgo.hashSize); | |||
@@ -257,18 +256,17 @@ public class AgileDecryptor extends Decryptor implements Cloneable { | |||
return fillSize; | |||
} | |||
protected static byte[] hashInput(EncryptionInfo encryptionInfo, byte pwHash[], byte blockKey[], byte inputKey[], int cipherMode) { | |||
EncryptionVerifier ver = encryptionInfo.getVerifier(); | |||
AgileDecryptor dec = (AgileDecryptor)encryptionInfo.getDecryptor(); | |||
int keySize = dec.getKeySizeInBytes(); | |||
int blockSize = dec.getBlockSizeInBytes(); | |||
/* package */ static byte[] hashInput(AgileEncryptionVerifier ver, byte pwHash[], byte blockKey[], byte inputKey[], int cipherMode) { | |||
CipherAlgorithm cipherAlgo = ver.getCipherAlgorithm(); | |||
ChainingMode chainMode = ver.getChainingMode(); | |||
int keySize = ver.getKeySize()/8; | |||
int blockSize = ver.getBlockSize(); | |||
HashAlgorithm hashAlgo = ver.getHashAlgorithm(); | |||
byte[] salt = ver.getSalt(); | |||
byte intermedKey[] = generateKey(pwHash, hashAlgo, blockKey, keySize); | |||
SecretKey skey = new SecretKeySpec(intermedKey, ver.getCipherAlgorithm().jceId); | |||
byte[] iv = generateIv(hashAlgo, salt, null, blockSize); | |||
Cipher cipher = getCipher(skey, ver.getCipherAlgorithm(), ver.getChainingMode(), iv, cipherMode); | |||
SecretKey skey = new SecretKeySpec(intermedKey, cipherAlgo.jceId); | |||
byte[] iv = generateIv(hashAlgo, ver.getSalt(), null, blockSize); | |||
Cipher cipher = getCipher(skey, cipherAlgo, chainMode, iv, cipherMode); | |||
byte[] hashFinal; | |||
try { | |||
@@ -281,7 +279,6 @@ public class AgileDecryptor extends Decryptor implements Cloneable { | |||
} | |||
@Override | |||
@SuppressWarnings("resource") | |||
public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException { | |||
DocumentInputStream dis = dir.createDocumentInputStream(DEFAULT_POIFS_ENTRY); | |||
_length = dis.readLong(); | |||
@@ -307,7 +304,7 @@ public class AgileDecryptor extends Decryptor implements Cloneable { | |||
byte[] blockKey = new byte[4]; | |||
LittleEndian.putInt(blockKey, 0, block); | |||
byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, header.getBlockSize()); | |||
byte[] iv = generateIv(header.getHashAlgorithm(), header.getKeySalt(), blockKey, header.getBlockSize()); | |||
AlgorithmParameterSpec aps; | |||
if (header.getCipherAlgorithm() == CipherAlgorithm.rc2) { |
@@ -45,18 +45,18 @@ public class AgileEncryptionHeader extends EncryptionHeader implements Cloneable | |||
throw new EncryptedDocumentException("Unable to parse keyData"); | |||
} | |||
setKeySize((int)keyData.getKeyBits()); | |||
setFlags(0); | |||
setSizeExtra(0); | |||
setCspName(null); | |||
setBlockSize(keyData.getBlockSize()); | |||
int keyBits = (int)keyData.getKeyBits(); | |||
CipherAlgorithm ca = CipherAlgorithm.fromXmlId(keyData.getCipherAlgorithm().toString(), keyBits); | |||
setCipherAlgorithm(ca); | |||
setCipherProvider(ca.provider); | |||
setKeySize(keyBits); | |||
setFlags(0); | |||
setSizeExtra(0); | |||
setCspName(null); | |||
setBlockSize(keyData.getBlockSize()); | |||
switch (keyData.getCipherChaining().intValue()) { | |||
case STCipherChaining.INT_CHAINING_MODE_CBC: | |||
setChainingMode(ChainingMode.cbc); | |||
@@ -73,7 +73,7 @@ public class AgileEncryptionHeader extends EncryptionHeader implements Cloneable | |||
HashAlgorithm ha = HashAlgorithm.fromEcmaId(keyData.getHashAlgorithm().toString()); | |||
setHashAlgorithm(ha); | |||
if (getHashAlgorithmEx().hashSize != hashSize) { | |||
if (getHashAlgorithm().hashSize != hashSize) { | |||
throw new EncryptedDocumentException("Unsupported hash algorithm: " + | |||
keyData.getHashAlgorithm() + " @ " + hashSize + " bytes"); | |||
} |
@@ -21,6 +21,7 @@ import java.security.GeneralSecurityException; | |||
import java.security.cert.CertificateFactory; | |||
import java.security.cert.X509Certificate; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.Iterator; | |||
import java.util.List; | |||
@@ -48,6 +49,8 @@ public class AgileEncryptionVerifier extends EncryptionVerifier implements Clone | |||
} | |||
private List<AgileCertificateEntry> certList = new ArrayList<AgileCertificateEntry>(); | |||
private int keyBits = -1; | |||
private int blockSize = -1; | |||
public AgileEncryptionVerifier(String descriptor) { | |||
this(AgileEncryptionInfoBuilder.parseDescriptor(descriptor)); | |||
@@ -66,10 +69,14 @@ public class AgileEncryptionVerifier extends EncryptionVerifier implements Clone | |||
} | |||
int keyBits = (int)keyData.getKeyBits(); | |||
CipherAlgorithm ca = CipherAlgorithm.fromXmlId(keyData.getCipherAlgorithm().toString(), keyBits); | |||
setCipherAlgorithm(ca); | |||
setKeySize(keyBits); | |||
int blockSize = keyData.getBlockSize(); | |||
setBlockSize(blockSize); | |||
int hashSize = keyData.getHashSize(); | |||
HashAlgorithm ha = HashAlgorithm.fromEcmaId(keyData.getHashAlgorithm().toString()); | |||
@@ -125,6 +132,8 @@ public class AgileEncryptionVerifier extends EncryptionVerifier implements Clone | |||
setCipherAlgorithm(cipherAlgorithm); | |||
setHashAlgorithm(hashAlgorithm); | |||
setChainingMode(chainingMode); | |||
setKeySize(keyBits); | |||
setBlockSize(blockSize); | |||
setSpinCount(100000); // TODO: use parameter | |||
} | |||
@@ -171,4 +180,60 @@ public class AgileEncryptionVerifier extends EncryptionVerifier implements Clone | |||
other.certList = new ArrayList<AgileCertificateEntry>(certList); | |||
return other; | |||
} | |||
/** | |||
* The keysize (in bits) of the verifier data. This usually equals the keysize of the header, | |||
* but only on a few exceptions, like files generated by Office for Mac, can be | |||
* different. | |||
* | |||
* @return the keysize (in bits) of the verifier. | |||
*/ | |||
public int getKeySize() { | |||
return keyBits; | |||
} | |||
/** | |||
* The blockSize (in bytes) of the verifier data. | |||
* This usually equals the blocksize of the header. | |||
* | |||
* @return the blockSize (in bytes) of the verifier, | |||
*/ | |||
public int getBlockSize() { | |||
return blockSize; | |||
} | |||
/** | |||
* Sets the keysize (in bits) of the verifier | |||
* | |||
* @param keyBits the keysize (in bits) | |||
*/ | |||
protected void setKeySize(int keyBits) { | |||
this.keyBits = keyBits; | |||
for (int allowedBits : getCipherAlgorithm().allowedKeySize) { | |||
if (allowedBits == keyBits) { | |||
return; | |||
} | |||
} | |||
throw new EncryptedDocumentException("KeySize "+keyBits+" not allowed for cipher "+getCipherAlgorithm()); | |||
} | |||
/** | |||
* Sets the blockSize (in bytes) of the verifier | |||
* | |||
* @param blockSize the blockSize (in bytes) | |||
*/ | |||
protected void setBlockSize(int blockSize) { | |||
this.blockSize = blockSize; | |||
} | |||
@Override | |||
protected void setCipherAlgorithm(CipherAlgorithm cipherAlgorithm) { | |||
super.setCipherAlgorithm(cipherAlgorithm); | |||
if (cipherAlgorithm.allowedKeySize.length == 1) { | |||
setKeySize(cipherAlgorithm.defaultKeySize); | |||
} | |||
} | |||
} |
@@ -86,9 +86,10 @@ public class AgileEncryptor extends Encryptor implements Cloneable { | |||
public void confirmPassword(String password) { | |||
// see [MS-OFFCRYPTO] - 2.3.3 EncryptionVerifier | |||
Random r = new SecureRandom(); | |||
int blockSize = getEncryptionInfo().getHeader().getBlockSize(); | |||
int keySize = getEncryptionInfo().getHeader().getKeySize()/8; | |||
int hashSize = getEncryptionInfo().getHeader().getHashAlgorithmEx().hashSize; | |||
AgileEncryptionHeader header = (AgileEncryptionHeader)getEncryptionInfo().getHeader(); | |||
int blockSize = header.getBlockSize(); | |||
int keySize = header.getKeySize()/8; | |||
int hashSize = header.getHashAlgorithm().hashSize; | |||
byte[] newVerifierSalt = new byte[blockSize] | |||
, newVerifier = new byte[blockSize] | |||
@@ -107,14 +108,14 @@ public class AgileEncryptor extends Encryptor implements Cloneable { | |||
@Override | |||
public void confirmPassword(String password, byte keySpec[], byte keySalt[], byte verifier[], byte verifierSalt[], byte integritySalt[]) { | |||
AgileEncryptionVerifier ver = (AgileEncryptionVerifier)getEncryptionInfo().getVerifier(); | |||
ver.setSalt(verifierSalt); | |||
AgileEncryptionHeader header = (AgileEncryptionHeader)getEncryptionInfo().getHeader(); | |||
ver.setSalt(verifierSalt); | |||
header.setKeySalt(keySalt); | |||
HashAlgorithm hashAlgo = ver.getHashAlgorithm(); | |||
int blockSize = header.getBlockSize(); | |||
pwHash = hashPassword(password, hashAlgo, verifierSalt, ver.getSpinCount()); | |||
pwHash = hashPassword(password, ver.getHashAlgorithm(), verifierSalt, ver.getSpinCount()); | |||
/** | |||
* encryptedVerifierHashInput: This attribute MUST be generated by using the following steps: | |||
@@ -129,7 +130,7 @@ public class AgileEncryptor extends Encryptor implements Cloneable { | |||
* blockSize bytes. | |||
* 4. Use base64 to encode the result of step 3. | |||
*/ | |||
byte encryptedVerifier[] = hashInput(getEncryptionInfo(), pwHash, kVerifierInputBlock, verifier, Cipher.ENCRYPT_MODE); | |||
byte encryptedVerifier[] = hashInput(ver, pwHash, kVerifierInputBlock, verifier, Cipher.ENCRYPT_MODE); | |||
ver.setEncryptedVerifier(encryptedVerifier); | |||
@@ -145,9 +146,9 @@ public class AgileEncryptor extends Encryptor implements Cloneable { | |||
* blockSize bytes, pad the hash value with 0x00 to an integral multiple of blockSize bytes. | |||
* 4. Use base64 to encode the result of step 3. | |||
*/ | |||
MessageDigest hashMD = getMessageDigest(hashAlgo); | |||
MessageDigest hashMD = getMessageDigest(ver.getHashAlgorithm()); | |||
byte[] hashedVerifier = hashMD.digest(verifier); | |||
byte encryptedVerifierHash[] = hashInput(getEncryptionInfo(), pwHash, kHashedVerifierBlock, hashedVerifier, Cipher.ENCRYPT_MODE); | |||
byte encryptedVerifierHash[] = hashInput(ver, pwHash, kHashedVerifierBlock, hashedVerifier, Cipher.ENCRYPT_MODE); | |||
ver.setEncryptedVerifierHash(encryptedVerifierHash); | |||
/** | |||
@@ -163,10 +164,10 @@ public class AgileEncryptor extends Encryptor implements Cloneable { | |||
* blockSize bytes. | |||
* 4. Use base64 to encode the result of step 3. | |||
*/ | |||
byte encryptedKey[] = hashInput(getEncryptionInfo(), pwHash, kCryptoKeyBlock, keySpec, Cipher.ENCRYPT_MODE); | |||
byte encryptedKey[] = hashInput(ver, pwHash, kCryptoKeyBlock, keySpec, Cipher.ENCRYPT_MODE); | |||
ver.setEncryptedKey(encryptedKey); | |||
SecretKey secretKey = new SecretKeySpec(keySpec, ver.getCipherAlgorithm().jceId); | |||
SecretKey secretKey = new SecretKeySpec(keySpec, header.getCipherAlgorithm().jceId); | |||
setSecretKey(secretKey); | |||
/* | |||
@@ -196,17 +197,17 @@ public class AgileEncryptor extends Encryptor implements Cloneable { | |||
this.integritySalt = integritySalt.clone(); | |||
try { | |||
byte vec[] = CryptoFunctions.generateIv(hashAlgo, header.getKeySalt(), kIntegrityKeyBlock, header.getBlockSize()); | |||
Cipher cipher = getCipher(secretKey, ver.getCipherAlgorithm(), ver.getChainingMode(), vec, Cipher.ENCRYPT_MODE); | |||
byte filledSalt[] = getBlock0(this.integritySalt, getNextBlockSize(this.integritySalt.length, blockSize)); | |||
byte encryptedHmacKey[] = cipher.doFinal(filledSalt); | |||
byte vec[] = CryptoFunctions.generateIv(header.getHashAlgorithm(), header.getKeySalt(), kIntegrityKeyBlock, header.getBlockSize()); | |||
Cipher cipher = getCipher(secretKey, header.getCipherAlgorithm(), header.getChainingMode(), vec, Cipher.ENCRYPT_MODE); | |||
byte hmacKey[] = getBlock0(this.integritySalt, getNextBlockSize(this.integritySalt.length, blockSize)); | |||
byte encryptedHmacKey[] = cipher.doFinal(hmacKey); | |||
header.setEncryptedHmacKey(encryptedHmacKey); | |||
cipher = Cipher.getInstance("RSA"); | |||
for (AgileCertificateEntry ace : ver.getCertificates()) { | |||
cipher.init(Cipher.ENCRYPT_MODE, ace.x509.getPublicKey()); | |||
ace.encryptedKey = cipher.doFinal(getSecretKey().getEncoded()); | |||
Mac x509Hmac = CryptoFunctions.getMac(hashAlgo); | |||
Mac x509Hmac = CryptoFunctions.getMac(header.getHashAlgorithm()); | |||
x509Hmac.init(getSecretKey()); | |||
ace.certVerifier = x509Hmac.doFinal(ace.x509.getEncoded()); | |||
} | |||
@@ -236,10 +237,12 @@ public class AgileEncryptor extends Encryptor implements Cloneable { | |||
// as the integrity hmac needs to contain the StreamSize, | |||
// it's not possible to calculate it on-the-fly while buffering | |||
// TODO: add stream size parameter to getDataStream() | |||
AgileEncryptionVerifier ver = (AgileEncryptionVerifier)getEncryptionInfo().getVerifier(); | |||
HashAlgorithm hashAlgo = ver.getHashAlgorithm(); | |||
AgileEncryptionHeader header = (AgileEncryptionHeader)getEncryptionInfo().getHeader(); | |||
int blockSize = header.getBlockSize(); | |||
HashAlgorithm hashAlgo = header.getHashAlgorithm(); | |||
Mac integrityMD = CryptoFunctions.getMac(hashAlgo); | |||
integrityMD.init(new SecretKeySpec(integritySalt, hashAlgo.jceHmacId)); | |||
byte hmacKey[] = getBlock0(this.integritySalt, getNextBlockSize(this.integritySalt.length, blockSize)); | |||
integrityMD.init(new SecretKeySpec(hmacKey, hashAlgo.jceHmacId)); | |||
byte buf[] = new byte[1024]; | |||
LittleEndian.putLong(buf, 0, oleStreamSize); | |||
@@ -256,12 +259,10 @@ public class AgileEncryptor extends Encryptor implements Cloneable { | |||
} | |||
byte hmacValue[] = integrityMD.doFinal(); | |||
byte hmacValueFilled[] = getBlock0(hmacValue, getNextBlockSize(hmacValue.length, blockSize)); | |||
AgileEncryptionHeader header = (AgileEncryptionHeader)getEncryptionInfo().getHeader(); | |||
int blockSize = header.getBlockSize(); | |||
byte iv[] = CryptoFunctions.generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), kIntegrityValueBlock, blockSize); | |||
byte iv[] = CryptoFunctions.generateIv(header.getHashAlgorithm(), header.getKeySalt(), kIntegrityValueBlock, blockSize); | |||
Cipher cipher = CryptoFunctions.getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), iv, Cipher.ENCRYPT_MODE); | |||
byte hmacValueFilled[] = getBlock0(hmacValue, getNextBlockSize(hmacValue.length, blockSize)); | |||
byte encryptedHmacValue[] = cipher.doFinal(hmacValueFilled); | |||
header.setEncryptedHmacValue(encryptedHmacValue); | |||
@@ -288,18 +289,21 @@ public class AgileEncryptor extends Encryptor implements Cloneable { | |||
keyPass.setSpinCount(ver.getSpinCount()); | |||
keyData.setSaltSize(header.getBlockSize()); | |||
keyPass.setSaltSize(header.getBlockSize()); | |||
keyPass.setSaltSize(ver.getBlockSize()); | |||
keyData.setBlockSize(header.getBlockSize()); | |||
keyPass.setBlockSize(header.getBlockSize()); | |||
keyPass.setBlockSize(ver.getBlockSize()); | |||
keyData.setKeyBits(header.getKeySize()); | |||
keyPass.setKeyBits(header.getKeySize()); | |||
keyPass.setKeyBits(ver.getKeySize()); | |||
HashAlgorithm hashAlgo = header.getHashAlgorithmEx(); | |||
keyData.setHashSize(hashAlgo.hashSize); | |||
keyPass.setHashSize(hashAlgo.hashSize); | |||
keyData.setHashSize(header.getHashAlgorithm().hashSize); | |||
keyPass.setHashSize(ver.getHashAlgorithm().hashSize); | |||
// header and verifier have to have the same cipher algorithm | |||
if (!header.getCipherAlgorithm().xmlId.equals(ver.getCipherAlgorithm().xmlId)) { | |||
throw new EncryptedDocumentException("Cipher algorithm of header and verifier have to match"); | |||
} | |||
STCipherAlgorithm.Enum xmlCipherAlgo = STCipherAlgorithm.Enum.forString(header.getCipherAlgorithm().xmlId); | |||
if (xmlCipherAlgo == null) { | |||
throw new EncryptedDocumentException("CipherAlgorithm "+header.getCipherAlgorithm()+" not supported."); | |||
@@ -320,12 +324,8 @@ public class AgileEncryptor extends Encryptor implements Cloneable { | |||
throw new EncryptedDocumentException("ChainingMode "+header.getChainingMode()+" not supported."); | |||
} | |||
STHashAlgorithm.Enum xmlHashAlgo = STHashAlgorithm.Enum.forString(hashAlgo.ecmaString); | |||
if (xmlHashAlgo == null) { | |||
throw new EncryptedDocumentException("HashAlgorithm "+hashAlgo+" not supported."); | |||
} | |||
keyData.setHashAlgorithm(xmlHashAlgo); | |||
keyPass.setHashAlgorithm(xmlHashAlgo); | |||
keyData.setHashAlgorithm(mapHashAlgorithm(header.getHashAlgorithm())); | |||
keyPass.setHashAlgorithm(mapHashAlgorithm(ver.getHashAlgorithm())); | |||
keyData.setSaltValue(header.getKeySalt()); | |||
keyPass.setSaltValue(ver.getSalt()); | |||
@@ -352,6 +352,14 @@ public class AgileEncryptor extends Encryptor implements Cloneable { | |||
return ed; | |||
} | |||
private static STHashAlgorithm.Enum mapHashAlgorithm(HashAlgorithm hashAlgo) { | |||
STHashAlgorithm.Enum xmlHashAlgo = STHashAlgorithm.Enum.forString(hashAlgo.ecmaString); | |||
if (xmlHashAlgo == null) { | |||
throw new EncryptedDocumentException("HashAlgorithm "+hashAlgo+" not supported."); | |||
} | |||
return xmlHashAlgo; | |||
} | |||
protected void marshallEncryptionDocument(EncryptionDocument ed, LittleEndianByteArrayOutputStream os) { | |||
XmlOptions xo = new XmlOptions(); | |||
@@ -371,7 +379,7 @@ public class AgileEncryptor extends Encryptor implements Cloneable { | |||
try { | |||
bos.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".getBytes("UTF-8")); | |||
ed.save(bos, xo); | |||
os.write(bos.toByteArray()); | |||
bos.writeTo(os); | |||
} catch (IOException e) { | |||
throw new EncryptedDocumentException("error marshalling encryption info document", e); | |||
} |
@@ -37,10 +37,6 @@ import org.apache.poi.util.IOUtils; | |||
import org.apache.poi.xssf.XSSFTestDataSamples; | |||
import org.junit.Test; | |||
/** | |||
* @author Maxim Valyanskiy | |||
* @author Gary King | |||
*/ | |||
public class TestDecryptor { | |||
@Test | |||
public void passwordVerification() throws IOException, GeneralSecurityException { | |||
@@ -162,4 +158,22 @@ public class TestDecryptor { | |||
//dec.verifyPassword(null); | |||
dec.getDataStream(pfs); | |||
} | |||
@Test | |||
public void bug60320() throws IOException, GeneralSecurityException { | |||
InputStream is = POIDataSamples.getPOIFSInstance().openResourceAsStream("60320-protected.xlsx"); | |||
POIFSFileSystem fs = new POIFSFileSystem(is); | |||
is.close(); | |||
EncryptionInfo info = new EncryptionInfo(fs); | |||
Decryptor d = Decryptor.getInstance(info); | |||
boolean b = d.verifyPassword("Test001!!"); | |||
assertTrue(b); | |||
zipOk(fs.getRoot(), d); | |||
fs.close(); | |||
} | |||
} |
@@ -35,7 +35,7 @@ public class TestEncryptionInfo { | |||
assertEquals(2, info.getVersionMinor()); | |||
assertEquals(CipherAlgorithm.aes128, info.getHeader().getCipherAlgorithm()); | |||
assertEquals(HashAlgorithm.sha1, info.getHeader().getHashAlgorithmEx()); | |||
assertEquals(HashAlgorithm.sha1, info.getHeader().getHashAlgorithm()); | |||
assertEquals(128, info.getHeader().getKeySize()); | |||
assertEquals(32, info.getVerifier().getEncryptedVerifierHash().length); | |||
assertEquals(CipherProvider.aes, info.getHeader().getCipherProvider()); | |||
@@ -54,7 +54,7 @@ public class TestEncryptionInfo { | |||
assertEquals(4, info.getVersionMinor()); | |||
assertEquals(CipherAlgorithm.aes256, info.getHeader().getCipherAlgorithm()); | |||
assertEquals(HashAlgorithm.sha512, info.getHeader().getHashAlgorithmEx()); | |||
assertEquals(HashAlgorithm.sha512, info.getHeader().getHashAlgorithm()); | |||
assertEquals(256, info.getHeader().getKeySize()); | |||
assertEquals(64, info.getVerifier().getEncryptedVerifierHash().length); | |||
assertEquals(CipherProvider.aes, info.getHeader().getCipherProvider()); |
@@ -36,7 +36,9 @@ import javax.crypto.Cipher; | |||
import org.apache.poi.POIDataSamples; | |||
import org.apache.poi.openxml4j.opc.ContentTypes; | |||
import org.apache.poi.openxml4j.opc.OPCPackage; | |||
import org.apache.poi.poifs.crypt.agile.AgileDecryptor; | |||
import org.apache.poi.poifs.crypt.agile.AgileEncryptionHeader; | |||
import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier; | |||
import org.apache.poi.poifs.filesystem.DirectoryNode; | |||
import org.apache.poi.poifs.filesystem.DocumentNode; | |||
import org.apache.poi.poifs.filesystem.Entry; | |||
@@ -87,7 +89,7 @@ public class TestEncryptor { | |||
assertArrayEquals(payloadExpected.toByteArray(), payloadActual.toByteArray()); | |||
} | |||
@Test | |||
public void agileEncryption() throws Exception { | |||
int maxKeyLen = Cipher.getMaxAllowedKeyLength("AES"); | |||
@@ -379,4 +381,142 @@ public class TestEncryptor { | |||
} | |||
} | |||
} | |||
/* | |||
* this test simulates the generation of bugs 60320 sample file | |||
* as the padding bytes of the EncryptedPackage stream are random or in POIs case PKCS5-padded | |||
* one would need to mock those bytes to get the same hmacValues - see diff below | |||
* | |||
* this use-case is experimental - for the time being the setters of the encryption classes | |||
* are spreaded between two packages and are protected - so you would need to violate | |||
* the packages rules and provide a helper class in the *poifs.crypt package-namespace. | |||
* the default way of defining the encryption settings is via the EncryptionInfo class | |||
*/ | |||
@Test | |||
public void bug60320CustomEncrypt() throws Exception { | |||
// --- src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java (revision 1766745) | |||
// +++ src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java (working copy) | |||
// @@ -208,6 +208,13 @@ | |||
// protected int invokeCipher(int posInChunk, boolean doFinal) throws GeneralSecurityException { | |||
// byte plain[] = (_plainByteFlags.isEmpty()) ? null : _chunk.clone(); | |||
// | |||
// + if (posInChunk < 4096) { | |||
// + _cipher.update(_chunk, 0, posInChunk, _chunk); | |||
// + byte bla[] = { (byte)0x7A,(byte)0x0F,(byte)0x27,(byte)0xF0,(byte)0x17,(byte)0x6E,(byte)0x77,(byte)0x05,(byte)0xB9,(byte)0xDA,(byte)0x49,(byte)0xF9,(byte)0xD7,(byte)0x8E,(byte)0x03,(byte)0x1D }; | |||
// + System.arraycopy(bla, 0, _chunk, posInChunk-2, bla.length); | |||
// + return posInChunk-2+bla.length; | |||
// + } | |||
// + | |||
// int ciLen = (doFinal) | |||
// ? _cipher.doFinal(_chunk, 0, posInChunk, _chunk) | |||
// : _cipher.update(_chunk, 0, posInChunk, _chunk); | |||
// | |||
// --- src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java (revision 1766745) | |||
// +++ src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java (working copy) | |||
// | |||
// @@ -300,7 +297,7 @@ | |||
// protected static Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk, EncryptionInfo encryptionInfo, SecretKey skey, int encryptionMode) | |||
// throws GeneralSecurityException { | |||
// EncryptionHeader header = encryptionInfo.getHeader(); | |||
// - String padding = (lastChunk ? "PKCS5Padding" : "NoPadding"); | |||
// + String padding = "NoPadding"; // (lastChunk ? "PKCS5Padding" : "NoPadding"); | |||
// if (existing == null || !existing.getAlgorithm().endsWith(padding)) { | |||
// existing = getCipher(skey, header.getCipherAlgorithm(), header.getChainingMode(), header.getKeySalt(), encryptionMode, padding); | |||
// } | |||
InputStream is = POIDataSamples.getPOIFSInstance().openResourceAsStream("60320-protected.xlsx"); | |||
POIFSFileSystem fsOrig = new POIFSFileSystem(is); | |||
is.close(); | |||
EncryptionInfo infoOrig = new EncryptionInfo(fsOrig); | |||
Decryptor decOrig = infoOrig.getDecryptor(); | |||
boolean b = decOrig.verifyPassword("Test001!!"); | |||
assertTrue(b); | |||
InputStream decIn = decOrig.getDataStream(fsOrig); | |||
byte[] zipInput = IOUtils.toByteArray(decIn); | |||
decIn.close(); | |||
InputStream epOrig = fsOrig.getRoot().createDocumentInputStream("EncryptedPackage"); | |||
// ignore the 16 padding bytes | |||
byte[] epOrigBytes = IOUtils.toByteArray(epOrig, 9400); | |||
epOrig.close(); | |||
EncryptionInfo eiNew = new EncryptionInfo(EncryptionMode.agile); | |||
AgileEncryptionHeader aehHeader = (AgileEncryptionHeader)eiNew.getHeader(); | |||
aehHeader.setCipherAlgorithm(CipherAlgorithm.aes128); | |||
aehHeader.setHashAlgorithm(HashAlgorithm.sha1); | |||
AgileEncryptionVerifier aehVerifier = (AgileEncryptionVerifier)eiNew.getVerifier(); | |||
// this cast might look strange - if the setters would be public, it will become obsolete | |||
// see http://stackoverflow.com/questions/5637650/overriding-protected-methods-in-java | |||
((EncryptionVerifier)aehVerifier).setCipherAlgorithm(CipherAlgorithm.aes256); | |||
aehVerifier.setHashAlgorithm(HashAlgorithm.sha512); | |||
Encryptor enc = eiNew.getEncryptor(); | |||
enc.confirmPassword("Test001!!", | |||
infoOrig.getDecryptor().getSecretKey().getEncoded(), | |||
infoOrig.getHeader().getKeySalt(), | |||
infoOrig.getDecryptor().getVerifier(), | |||
infoOrig.getVerifier().getSalt(), | |||
infoOrig.getDecryptor().getIntegrityHmacKey() | |||
); | |||
NPOIFSFileSystem fsNew = new NPOIFSFileSystem(); | |||
OutputStream os = enc.getDataStream(fsNew); | |||
os.write(zipInput); | |||
os.close(); | |||
ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |||
fsNew.writeFilesystem(bos); | |||
fsNew.close(); | |||
NPOIFSFileSystem fsReload = new NPOIFSFileSystem(new ByteArrayInputStream(bos.toByteArray())); | |||
InputStream epReload = fsReload.getRoot().createDocumentInputStream("EncryptedPackage"); | |||
byte[] epNewBytes = IOUtils.toByteArray(epReload, 9400); | |||
epReload.close(); | |||
assertArrayEquals(epOrigBytes, epNewBytes); | |||
EncryptionInfo infoReload = new EncryptionInfo(fsOrig); | |||
Decryptor decReload = infoReload.getDecryptor(); | |||
b = decReload.verifyPassword("Test001!!"); | |||
assertTrue(b); | |||
AgileEncryptionHeader aehOrig = (AgileEncryptionHeader)infoOrig.getHeader(); | |||
AgileEncryptionHeader aehReload = (AgileEncryptionHeader)infoReload.getHeader(); | |||
assertEquals(aehOrig.getBlockSize(), aehReload.getBlockSize()); | |||
assertEquals(aehOrig.getChainingMode(), aehReload.getChainingMode()); | |||
assertEquals(aehOrig.getCipherAlgorithm(), aehReload.getCipherAlgorithm()); | |||
assertEquals(aehOrig.getCipherProvider(), aehReload.getCipherProvider()); | |||
assertEquals(aehOrig.getCspName(), aehReload.getCspName()); | |||
assertArrayEquals(aehOrig.getEncryptedHmacKey(), aehReload.getEncryptedHmacKey()); | |||
// this only works, when the paddings are mocked to be the same ... | |||
// assertArrayEquals(aehOrig.getEncryptedHmacValue(), aehReload.getEncryptedHmacValue()); | |||
assertEquals(aehOrig.getFlags(), aehReload.getFlags()); | |||
assertEquals(aehOrig.getHashAlgorithm(), aehReload.getHashAlgorithm()); | |||
assertArrayEquals(aehOrig.getKeySalt(), aehReload.getKeySalt()); | |||
assertEquals(aehOrig.getKeySize(), aehReload.getKeySize()); | |||
AgileEncryptionVerifier aevOrig = (AgileEncryptionVerifier)infoOrig.getVerifier(); | |||
AgileEncryptionVerifier aevReload = (AgileEncryptionVerifier)infoReload.getVerifier(); | |||
assertEquals(aevOrig.getBlockSize(), aevReload.getBlockSize()); | |||
assertEquals(aevOrig.getChainingMode(), aevReload.getChainingMode()); | |||
assertEquals(aevOrig.getCipherAlgorithm(), aevReload.getCipherAlgorithm()); | |||
assertArrayEquals(aevOrig.getEncryptedKey(), aevReload.getEncryptedKey()); | |||
assertArrayEquals(aevOrig.getEncryptedVerifier(), aevReload.getEncryptedVerifier()); | |||
assertArrayEquals(aevOrig.getEncryptedVerifierHash(), aevReload.getEncryptedVerifierHash()); | |||
assertEquals(aevOrig.getHashAlgorithm(), aevReload.getHashAlgorithm()); | |||
assertEquals(aevOrig.getKeySize(), aevReload.getKeySize()); | |||
assertArrayEquals(aevOrig.getSalt(), aevReload.getSalt()); | |||
assertEquals(aevOrig.getSpinCount(), aevReload.getSpinCount()); | |||
AgileDecryptor adOrig = (AgileDecryptor)infoOrig.getDecryptor(); | |||
AgileDecryptor adReload = (AgileDecryptor)infoReload.getDecryptor(); | |||
assertArrayEquals(adOrig.getIntegrityHmacKey(), adReload.getIntegrityHmacKey()); | |||
// doesn't work without mocking ... see above | |||
// assertArrayEquals(adOrig.getIntegrityHmacValue(), adReload.getIntegrityHmacValue()); | |||
assertArrayEquals(adOrig.getSecretKey().getEncoded(), adReload.getSecretKey().getEncoded()); | |||
assertArrayEquals(adOrig.getVerifier(), adReload.getVerifier()); | |||
fsReload.close(); | |||
} | |||
} |