From 457100c358c52aa46491a41e6f94b2513d4a77c3 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Tue, 1 Nov 2016 01:30:48 +0000 Subject: [PATCH] Bug 60320 - issue opening password protected xlsx git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1767399 13f79535-47bb-0310-9956-ffa450edef68 --- .../org/apache/poi/TestAllFiles.java | 1 + .../poi/poifs/crypt/EncryptionHeader.java | 27 ++++ .../poi/poifs/crypt/EncryptionVerifier.java | 9 ++ .../standard/StandardEncryptionHeader.java | 2 +- .../standard/StandardEncryptionVerifier.java | 2 +- .../poi/poifs/crypt/agile/AgileDecryptor.java | 59 ++++---- .../crypt/agile/AgileEncryptionHeader.java | 14 +- .../crypt/agile/AgileEncryptionVerifier.java | 67 ++++++++- .../poi/poifs/crypt/agile/AgileEncryptor.java | 80 +++++----- .../apache/poi/poifs/crypt/TestDecryptor.java | 22 ++- .../poi/poifs/crypt/TestEncryptionInfo.java | 4 +- .../apache/poi/poifs/crypt/TestEncryptor.java | 142 +++++++++++++++++- test-data/poifs/60320-protected.xlsx | Bin 0 -> 15872 bytes 13 files changed, 345 insertions(+), 84 deletions(-) create mode 100644 test-data/poifs/60320-protected.xlsx diff --git a/src/integrationtest/org/apache/poi/TestAllFiles.java b/src/integrationtest/org/apache/poi/TestAllFiles.java index e0e2c0bde2..dcce6269f1 100644 --- a/src/integrationtest/org/apache/poi/TestAllFiles.java +++ b/src/integrationtest/org/apache/poi/TestAllFiles.java @@ -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"); diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java index c52e87c62b..f13c116e0b 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionHeader.java @@ -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() { diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java index 6e8059261a..df216c0714 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java @@ -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; } diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionHeader.java index 30f35581f7..a04f74d400 100644 --- a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionHeader.java +++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionHeader.java @@ -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 diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionVerifier.java index f00efecfb6..4fd284a86c 100644 --- a/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionVerifier.java +++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardEncryptionVerifier.java @@ -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) { diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java index 480137ef8a..ffe0cdf3f7 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileDecryptor.java @@ -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) { diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionHeader.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionHeader.java index 88bccabf6c..ebb64cb5be 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionHeader.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionHeader.java @@ -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"); } diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java index 53d4cd6ed2..6d9ec6d66f 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java @@ -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 certList = new ArrayList(); + 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(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); + } + } } diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java index 5ff45ad814..96aa9b5f9f 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java @@ -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("\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); } diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java index d2260dc998..19dc34b84e 100644 --- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java @@ -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(); + } } \ No newline at end of file diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java index 3227334429..204c2ed621 100644 --- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptionInfo.java @@ -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()); diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java index 02e021d7d8..f3e6c97e4c 100644 --- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java @@ -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(); + } } diff --git a/test-data/poifs/60320-protected.xlsx b/test-data/poifs/60320-protected.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..71c6b4cbe81e7591ee9a96d6d2a2bf461615f746 GIT binary patch literal 15872 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