From 138bd6f94c47adb06d109b5b1a342a0b37547c0c Mon Sep 17 00:00:00 2001 From: Maxim Valyanskiy Date: Tue, 10 May 2011 10:38:17 +0000 Subject: [PATCH] bug#51165: Add support for OOXML Agile Encryption git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1101397 13f79535-47bb-0310-9956-ffa450edef68 --- build.xml | 9 + .../content/xdocs/encryption.xml | 84 ++++++ .../poi/poifs/crypt/AgileDecryptor.java | 244 ++++++++++++++++++ .../org/apache/poi/poifs/crypt/Decryptor.java | 170 ++++-------- .../apache/poi/poifs/crypt/EcmaDecryptor.java | 132 ++++++++++ .../poi/poifs/crypt/EncryptionHeader.java | 88 ++++++- .../poi/poifs/crypt/EncryptionInfo.java | 31 ++- .../poi/poifs/crypt/EncryptionVerifier.java | 114 +++++++- .../poi/poifs/crypt/AllPOIFSCryptoTests.java | 37 +++ ...{DecryptorTest.java => TestDecryptor.java} | 21 +- ...nInfoTest.java => TestEncryptionInfo.java} | 2 +- test-data/poifs/protected_agile.docx | Bin 0 -> 19456 bytes 12 files changed, 789 insertions(+), 143 deletions(-) create mode 100644 src/documentation/content/xdocs/encryption.xml create mode 100644 src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java create mode 100644 src/java/org/apache/poi/poifs/crypt/EcmaDecryptor.java create mode 100644 src/testcases/org/apache/poi/poifs/crypt/AllPOIFSCryptoTests.java rename src/testcases/org/apache/poi/poifs/crypt/{DecryptorTest.java => TestDecryptor.java} (78%) rename src/testcases/org/apache/poi/poifs/crypt/{EncryptionInfoTest.java => TestEncryptionInfo.java} (97%) create mode 100644 test-data/poifs/protected_agile.docx diff --git a/build.xml b/build.xml index 9112de255f..5f4c5aada4 100644 --- a/build.xml +++ b/build.xml @@ -122,6 +122,9 @@ under the License. + + @@ -166,6 +169,7 @@ under the License. + @@ -295,6 +299,7 @@ under the License. + @@ -311,6 +316,10 @@ under the License. + + + + diff --git a/src/documentation/content/xdocs/encryption.xml b/src/documentation/content/xdocs/encryption.xml new file mode 100644 index 0000000000..61a2a200dd --- /dev/null +++ b/src/documentation/content/xdocs/encryption.xml @@ -0,0 +1,84 @@ + + + + + + +
+ Apache POI - Encryption support + + + +
+ + +
Overview +

Apache POI contains support for reading few variants of encrypted office files:

+
    +
  • XLS - RC4 Encryption
  • +
  • XML-based formats (XLSX, DOCX and etc) - AES Encryption
  • +
+ +

Some "write-protected" files are encrypted with build-in password, POI can read that files too.

+
+ +
XLS +

When HSSF receive encrypted file, it tries to decode it with MSOffice build-in password. + Use static method setCurrentUserPassword(String password) of org.apache.poi.hssf.record.crypto.Biff8EncryptionKey to + set password. It sets thread local variable. Do not forget to reset it to null after text extraction. +

+
+ +
XML-based formats +

XML-based formats are stored in OLE-package stream "EncryptedPackage". Use org.apache.poi.poifs.crypt.Decryptor + to decode file:

+ + +EncryptionInfo info = new EncryptionInfo(filesystem); +Decryptor d = new Decryptor(info); + +try { + if (!d.verifyPassword(password)) { + throw new RuntimeException("Unable to process: document is encrypted"); + } + + InputStream dataStream = d.getDataStream(filesystem); + + // parse dataStream + +} catch (GeneralSecurityException ex) { + throw new RuntimeException("Unable to process encrypted document", ex); +} + + +

If you want to read file encrypted with build-in password, use Decryptor.DEFAULT_PASSWORD.

+
+ + +
+ + Copyright (c) @year@ The Apache Software Foundation. All rights reserved. + +
+
+ + + + diff --git a/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java b/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java new file mode 100644 index 0000000000..17ef47bb83 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/AgileDecryptor.java @@ -0,0 +1,244 @@ +/* ==================================================================== + 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 java.util.Arrays; +import java.io.IOException; +import java.io.InputStream; +import java.io.FilterInputStream; +import java.io.ByteArrayInputStream; +import java.security.MessageDigest; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.EncryptedDocumentException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.crypto.spec.IvParameterSpec; + +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.poifs.filesystem.DocumentInputStream; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.util.LittleEndian; + +/** + * @author Gary King + */ +public class AgileDecryptor extends Decryptor { + + private final EncryptionInfo _info; + private SecretKey _secretKey; + + private static final byte[] kVerifierInputBlock; + private static final byte[] kHashedVerifierBlock; + private static final byte[] kCryptoKeyBlock; + + static { + kVerifierInputBlock = + new byte[] { (byte)0xfe, (byte)0xa7, (byte)0xd2, (byte)0x76, + (byte)0x3b, (byte)0x4b, (byte)0x9e, (byte)0x79 }; + kHashedVerifierBlock = + new byte[] { (byte)0xd7, (byte)0xaa, (byte)0x0f, (byte)0x6d, + (byte)0x30, (byte)0x61, (byte)0x34, (byte)0x4e }; + kCryptoKeyBlock = + new byte[] { (byte)0x14, (byte)0x6e, (byte)0x0b, (byte)0xe7, + (byte)0xab, (byte)0xac, (byte)0xd0, (byte)0xd6 }; + } + + public boolean verifyPassword(String password) throws GeneralSecurityException { + EncryptionVerifier verifier = _info.getVerifier(); + int algorithm = verifier.getAlgorithm(); + int mode = verifier.getCipherMode(); + + byte[] pwHash = hashPassword(_info, password); + byte[] iv = generateIv(algorithm, verifier.getSalt(), null); + + SecretKey skey; + skey = new SecretKeySpec(generateKey(pwHash, kVerifierInputBlock), "AES"); + Cipher cipher = getCipher(algorithm, mode, skey, iv); + byte[] verifierHashInput = cipher.doFinal(verifier.getVerifier()); + + MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); + byte[] trimmed = new byte[verifier.getSalt().length]; + System.arraycopy(verifierHashInput, 0, trimmed, 0, trimmed.length); + byte[] hashedVerifier = sha1.digest(trimmed); + + skey = new SecretKeySpec(generateKey(pwHash, kHashedVerifierBlock), "AES"); + iv = generateIv(algorithm, verifier.getSalt(), null); + cipher = getCipher(algorithm, mode, skey, iv); + byte[] verifierHash = cipher.doFinal(verifier.getVerifierHash()); + trimmed = new byte[hashedVerifier.length]; + System.arraycopy(verifierHash, 0, trimmed, 0, trimmed.length); + + if (Arrays.equals(trimmed, hashedVerifier)) { + skey = new SecretKeySpec(generateKey(pwHash, kCryptoKeyBlock), "AES"); + iv = generateIv(algorithm, verifier.getSalt(), null); + cipher = getCipher(algorithm, mode, skey, iv); + byte[] inter = cipher.doFinal(verifier.getEncryptedKey()); + byte[] keyspec = new byte[_info.getHeader().getKeySize() / 8]; + System.arraycopy(inter, 0, keyspec, 0, keyspec.length); + _secretKey = new SecretKeySpec(keyspec, "AES"); + return true; + } else { + return false; + } + } + + public InputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException { + DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage"); + long size = dis.readLong(); + return new ChunkedCipherInputStream(dis, size); + } + + protected AgileDecryptor(EncryptionInfo info) { + _info = info; + } + + private class ChunkedCipherInputStream extends InputStream { + private int _lastIndex = 0; + private long _pos = 0; + private final long _size; + private final DocumentInputStream _stream; + private byte[] _chunk; + private Cipher _cipher; + + public ChunkedCipherInputStream(DocumentInputStream stream, long size) + throws GeneralSecurityException { + _size = size; + _stream = stream; + _cipher = getCipher(_info.getHeader().getAlgorithm(), + _info.getHeader().getCipherMode(), + _secretKey, _info.getHeader().getKeySalt()); + } + + public int read() throws IOException { + byte[] b = new byte[1]; + if (read(b) == 1) + return b[0]; + return -1; + } + + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + public int read(byte[] b, int off, int len) throws IOException { + int total = 0; + + while (len > 0) { + if (_chunk == null) { + try { + _chunk = nextChunk(); + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException(e.getMessage()); + } + } + int count = (int)(4096L - (_pos & 0xfff)); + count = Math.min(available(), Math.min(count, len)); + System.arraycopy(_chunk, (int)(_pos & 0xfff), b, off, count); + off += count; + len -= count; + _pos += count; + if ((_pos & 0xfff) == 0) + _chunk = null; + total += count; + } + + return total; + } + + public long skip(long n) throws IOException { + long start = _pos; + long skip = Math.min(available(), n); + + if ((((_pos + skip) ^ start) & ~0xfff) != 0) + _chunk = null; + _pos += skip; + return skip; + } + + public int available() throws IOException { return (int)(_size - _pos); } + public void close() throws IOException { _stream.close(); } + public boolean markSupported() { return false; } + + private byte[] nextChunk() throws GeneralSecurityException, IOException { + int index = (int)(_pos >> 12); + byte[] blockKey = new byte[4]; + LittleEndian.putInt(blockKey, index); + byte[] iv = generateIv(_info.getHeader().getAlgorithm(), + _info.getHeader().getKeySalt(), blockKey); + _cipher.init(Cipher.DECRYPT_MODE, _secretKey, new IvParameterSpec(iv)); + if (_lastIndex != index) + _stream.skip((index - _lastIndex) << 12); + + byte[] block = new byte[Math.min(_stream.available(), 4096)]; + _stream.readFully(block); + _lastIndex = index + 1; + return _cipher.doFinal(block); + } + } + + private Cipher getCipher(int algorithm, int mode, SecretKey key, byte[] vec) + throws GeneralSecurityException { + String name = null; + String chain = null; + + if (algorithm == EncryptionHeader.ALGORITHM_AES_128 || + algorithm == EncryptionHeader.ALGORITHM_AES_192 || + algorithm == EncryptionHeader.ALGORITHM_AES_256) + name = "AES"; + + if (mode == EncryptionHeader.MODE_CBC) + chain = "CBC"; + else if (mode == EncryptionHeader.MODE_CFB) + chain = "CFB"; + + Cipher cipher = Cipher.getInstance(name + "/" + chain + "/NoPadding"); + IvParameterSpec iv = new IvParameterSpec(vec); + cipher.init(Cipher.DECRYPT_MODE, key, iv); + return cipher; + } + + private byte[] getBlock(int algorithm, byte[] hash) { + byte[] result = new byte[getBlockSize(algorithm)]; + Arrays.fill(result, (byte)0x36); + System.arraycopy(hash, 0, result, 0, Math.min(result.length, hash.length)); + return result; + } + + private byte[] generateKey(byte[] hash, byte[] blockKey) throws NoSuchAlgorithmException { + MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); + sha1.update(hash); + return getBlock(_info.getVerifier().getAlgorithm(), sha1.digest(blockKey)); + } + + protected byte[] generateIv(int algorithm, byte[] salt, byte[] blockKey) + throws NoSuchAlgorithmException { + + + if (blockKey == null) + return getBlock(algorithm, salt); + + MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); + sha1.update(salt); + return getBlock(algorithm, sha1.digest(blockKey)); + } +} \ No newline at end of file diff --git a/src/java/org/apache/poi/poifs/crypt/Decryptor.java b/src/java/org/apache/poi/poifs/crypt/Decryptor.java index e97f41b41d..fb5b11f7b3 100644 --- a/src/java/org/apache/poi/poifs/crypt/Decryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/Decryptor.java @@ -19,150 +19,74 @@ package org.apache.poi.poifs.crypt; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; -import java.security.GeneralSecurityException; import java.security.MessageDigest; +import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; -import java.util.Arrays; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -import org.apache.poi.poifs.filesystem.DirectoryNode; -import org.apache.poi.poifs.filesystem.DocumentInputStream; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.EncryptedDocumentException; import org.apache.poi.util.LittleEndian; -/** - * @author Maxim Valyanskiy - */ -public class Decryptor { +public abstract class Decryptor { public static final String DEFAULT_PASSWORD="VelvetSweatshop"; - private final EncryptionInfo info; - private byte[] passwordHash; + public abstract InputStream getDataStream(DirectoryNode dir) + throws IOException, GeneralSecurityException; + + public abstract boolean verifyPassword(String password) + throws GeneralSecurityException; + + public static Decryptor getInstance(EncryptionInfo info) { + int major = info.getVersionMajor(); + int minor = info.getVersionMinor(); - public Decryptor(EncryptionInfo info) { - this.info = info; + if (major == 4 && minor == 4) + return new AgileDecryptor(info); + else if (minor == 2 && (major == 3 || major == 4)) + return new EcmaDecryptor(info); + else + throw new EncryptedDocumentException("Unsupported version"); } - private void generatePasswordHash(String password) throws NoSuchAlgorithmException { + public InputStream getDataStream(NPOIFSFileSystem fs) throws IOException, GeneralSecurityException { + return getDataStream(fs.getRoot()); + } + + public InputStream getDataStream(POIFSFileSystem fs) throws IOException, GeneralSecurityException { + return getDataStream(fs.getRoot()); + } + + protected static int getBlockSize(int algorithm) { + switch (algorithm) { + case EncryptionHeader.ALGORITHM_AES_128: return 16; + case EncryptionHeader.ALGORITHM_AES_192: return 24; + case EncryptionHeader.ALGORITHM_AES_256: return 32; + } + throw new EncryptedDocumentException("Unknown block size"); + } + + protected byte[] hashPassword(EncryptionInfo info, + String password) throws NoSuchAlgorithmException { MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); - - byte[] passwordBytes; + byte[] bytes; try { - passwordBytes = password.getBytes("UTF-16LE"); - } catch(UnsupportedEncodingException e) { - throw new RuntimeException("Your JVM is broken - UTF16 not found!"); + bytes = password.getBytes("UTF-16LE"); + } catch (UnsupportedEncodingException e) { + throw new EncryptedDocumentException("UTF16 not supported"); } sha1.update(info.getVerifier().getSalt()); - byte[] hash = sha1.digest(passwordBytes); - + byte[] hash = sha1.digest(bytes); byte[] iterator = new byte[4]; - for (int i = 0; i<50000; i++) { - sha1.reset(); + for (int i = 0; i < info.getVerifier().getSpinCount(); 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 source.length) { - for(int i=source.length; i source.length) { + for(int i=source.length; ip6R$ZB@IfcAfJcR!vPJZRFNsZ36u-#0vxp^!f%31o4-4P(U5{&wE55Ado-q zU*F!|{%nQ>RG z|4sj!G@uC3`TvAK03P$FsPBIAe??h(cfFc$W5Dlm#Q%@LA_8O!WCY|0rK0IL5oy}v8G&o}?0d|!Y7fPWMh-jA;mK+<=qZ2^(u zzmEU?H2v+#22A>2=uiIt%zDZIZUz8uP5>R*0qQn@wbTK4m;e1dc>#$6{y2aHfS3R! zJ>W_M!~(<$kWCa&V+Pb1erpp1Xi^x^^WG}}XlDRun*q>555xw<_}e%iAX&hA|Eyj9 z-roP7tbf|+z5J8@R!1^`J=*`-{SQklK>T@+9q;cA?;idSCjbV#zc>EL;cxj%05trr z-+zv9fPjCCfA8_?ukWgVoqgpo_gmcMi2J?#Yv2FWhnW8x@%Q~X!T*O4e`}|IdH?_M z{CW4~zei%ZzweusaQ<(eKmTa|e=FaA<-q)h{@-o?{#^Zg6!ZuGFtoqr4*_`II|5>i z4Z!Q(-va(sLjG^)|J_%I|6ug@&d$GL0XzWzKYs`Pv;P0CC&K@;mxX^-0f6!EeE+2X z#&n?ni6~%z+57;e+?wiM0~s-V7LLJ96juv0n-QoOV~Q`5ScqW+jG*OhGuX3g$T2GkP)W)Q2}?*Y85&zFxQJ6Z{OLFUF8{58e{10Xe;R=K<9G1v zL8o;tuJkDpHzt_;fBLcKq^Oc=VqoGZD7?~1Xp>f16Y#K( z^O|P|PB(8u^5>vDG8>#=JhO2no`YM*Y7Q0r^qR!th%PCW9oE{LLjTf&MKtVRzS}%W zb5<&vG%;r&-|h%e63ul~P(S=IkO2nSZ;Eooq;Rb<{`p8^vI1n9%#a_Ya=9dV`slWL zLzb)Q9%X~dgFTm*p$H52>}2`Hf>6KnKHt7{=iCmljS0e+!y&%ofHIo>4bfRF1P@V# z?FB8G5QsFGOQel)>eH(pR`L1jWs?2RNN{v2sJPOAbvf|8UWa&@eoaWF4^z59PMrAl zv>>+_OlGnYGukmfkqM^ns@CBYroeOUY%sy%z%@l!-7S7>ZgP29Xq*u~u{##;L0dmg zbI4vWUZKU1V?{og3^V67yyYd(#(1$^j|OJr%yhHdO;5unpGRZC57>_7W5s_yGVweR zlVI^?Dxi(WY&s5+&7SJG^{U12Zx|N{`TnHODKgm4*DKAxzWxwTJ(<>D1>R1v@87pL z77RI_%^h!0TcnzlQ$@#${!`~IW|z64H@qs}2cF5qk-|YaZIano65q=_J2>scx#Vg= zNXNd!eOwj<>a*`1(s7vKbWh8D=%BTOG03e%5|r9}2wI&7HK?LHv9rc}$+!s5(V@W@ zN+fKol(?aNOK{ePDpOyZ6~oY%0oiY6n}vhTbym zb7G2<%1HLh;nNud$N*9qqc&U9Ubd1VoZINP6TzVb76a(KIBwL=$R`gcSx3QW&aVtc zcCQw8&rFu?h7uZHOhB{=W{`9w?(Hvf`KmpbudCnAKXEt$X<2_0iQcc4N9pPOj^I7; ziLiuEwpRrnLBjABx^8r;=BW2heX!EMqkNX)CT=xra3eO-fqci?u|@NRQ^tJSp9EtJ z8A^W3pj*uDzC8y&DRG){MQ_c1-{%;2oQ7kSVaTQoxFTJ2NKF6{I}eLqW3uGViEu|E zhe+bHp|~r>PzBgH0pk?4HmsWVN6$m|nWQc7<1_z&1-8#ky9&F+@%@7W9a z(Wb#t_H?jSoIiYM4HK3V{0kaT;fze5%=}9~&=rW^@o34me8lX&Rfi?JfoMOIES*SJ zgc+)ooy#fQcE{)XX;C0?t*5$KYylirAB>wS)d4xLS0PoCFnoDm|MSVumq9V*#4YR8 z+}M;&YDZ=nhm6Fbs~2_rnGvDS3PZNyR}zV)KC5hq@Pm`P&o&DwOM{Ycch=Y|ww)!Z zymI2Sq$skWvEOrcF^|7&EFlSq812v(;;s-ZS|-*FSA`YAlZ?M0i~!P%5X6S*=~L*%miP*UmEW3m}* z3qQsW9p<~-H`?!yOagOal2LLVW1n(E@|dn0;r-WR7S!`RpiIce7~mon2EZA?Grr~` z5S&WhyuJ~Gh=fJ_+!nebIKnk4HT&2fp$h?D zWEA?~qgdN!8;Y=;r;VY4@Tn@S)vzxvEY7g~YFwR8jUDaRGO2XJ{@bBK7Y@+%^y4V! z*VS(;0RvZ`%>&QI8<)FWz^mNOKRT!*afQC_324tzo_UM)XY?F%JX(%!W>t`){K#m(EEI(&+U*O!S-p79!9XL z0(UbDR0+5lKSWYed}H#UL5UZrOV}9b7t`Q$zJx#QK#d=VItr+N?Ix`v3sg~S+OX2s z7`7yNX!|-fpO!OR{&rAmFnu=|i+LBJvZ-hmI&ffPKG^kH@0QfXN=it?(Sv3c_#sj@ z-13K5l1xOX$1&=N-f8g#QnKyyu(5Qp1L#uxB)au_?h=g@0|bYpN- zD;KTv$!|80_TOwo64XUsX6fLCRJLgxwvH~mo*12`e#k*m=nQOyCYLYRwzD%!ry#R+ z!l@w$PjH<$=C$cWwnBadHqQw2<8C9?{c`zs&RZ00mZ)2#0s5UAZ47EzVX?L$^|(pi z8L|w7`Y@sH_@GxliPkk3buE8%EamP8Mb>Q@v?X5N)=$^qH_xW#ngGjYP2aQTJ`#)+ z;#@EJs}BAZW0*D9KyD?4G{;q29qi|$V1%G~c`PMcgVPq8Z&b~&XxyLLMWLE#*6MSP zO>rHkeh89f*WkKOb+~7Hx>lQeLeI9ulM*J~6HOd%^p6BUmYtpEoS7N_m;ly)M)$tH z3_UT2NJ8XLzLT~O@Ye~Bx(!aaA`|1ef&1twEl`_HZ#pHn)YuBdPj36OO|O02?&mx; z2tu)CC%V)~0Ft72w1%TVhInNbCzkWTi0#3mTE*P#Jk0dVb`=;iGF$mI*e2t~2#e^N zG}ivq{El|WOD{<(44}y66i)(Fvc6k*nPH@ew?oS!#{_e&~s`Y`b zu}c3y_ebJ!r7pu6wtHe=o`Vd~3=<#laeoH&YG3H6y~K+%RgEE{0Pw6mCElQ5#~kdS zk=>VlXO9+bgPoHGBj8YVl#>@Nh5HE5W~W4-2R86*HMLegdNjU=t}dBokb}*;}5{oR&} zpA{1wH;Fx*cb6ykbZ~+q1Q^hSgD%Sx|J(v6Ukg` z$e6nxOx?!WA3K#PD{IrNv5E}DN)(EMS}6R2pvk!^-^9n{y`tGi>922&fs`mL-Kv#U zr>FP~ef?FOGSYjG3A$EB`XQcq>9{{s=tLiJaiz87n1)Ex+q|h@Kv%zsi6OM@Ci+AeLo8bN#O}Py0FmmA?^mmb9h1}LIW?t2rG)1DLWD4>%`f0LsbsN(Uco| zRxEeO7T|1U>QiZS&iV_~fy@JmMQbxT>r0{VRlSXfh?-ABGE~QnQp4Bj!cM>>b^-cV z%}WN3x4+Cj{9-SrRUXp_`_j#kvS0D#-UVJ(Ynay-56`i{kcKkF-1okg2K~bAUlpagLF8`+3 zH`dg_I>|)?E=CV2)5+`65Q(T`I{8giUo2G2L_6STo&Qf=l(L;nFKVL8Rg^b|y+Liz zc!fjRkkwZV;?dieNRfW*y_x{i8r!Ib0+2Z3dl~1rY-Ee|{NYHGfKs*6`TTtZxUFWs zMl+<9HKkT3W;rm4=2rd{j+s$#6=k_M%uPspfRy5E*GV3I=&)e^VXFA5z#M#AZR-O7aWog&;Eb!wU zUuX&Z`2IGGDNMmzpaK(RH=8}{aYK`IJ8$Df;$?JDGtHN-5&MC%h?1L3Rr~f__U5sm z>UO~#j1rZ?f8?Yyg-aU|DHHRHk2o=?u5+osdsLCeN(ZMD)STFxee57A->DV9SFMVe zPB>5R535{5H6_y>A*}gTZK{vr-xIrF(JAt6&aoS6_G~$=Xs7~8epZwDJ;&$NnJy&$ ztlLo~yK7atR&gLnWs1g?@#r>XDbo0n#k#NAho86+#^jr6&apcFfC>q0qf7bLXb*TO zkwhU@mD@jqDKTm2ZJ#MD70k)is#J%U-jeeZp?8^XWnAoVnbN_o;PiD*Sntc#L*?>g zknmYX`P&A7LC$)|eJN4wv1EtWkf0D##U$<4sbCTtha&XMyc)T3sg6mZ_G;pX93^wn zVO-XIV(dp6YCI`N|ETXUGoVYR?t7!N*NyVnS-FL5C3%7+tVG)(T$)2N2$n72Fwx9t zDe-IA2ACVh;B43}qiizWcn>>(0}T}n9)`O>5u6B+E0l}vSI?95!0SJ0FIA-)JQ)p_bSiG}+hQ=BYh??)V6K~305Z2OPiSY;R zFAeK@r%q90Aw{M<+s1%oD5JxNc7B34GMbgEqf~L$9#`L$hX3S|MBLtX-AY%sCY#Yx zy;-8%&4R`bbk8ho1wKqQay_Gq1MwTMehzU&OG!Bzr|3eF21SePxQp6{`f)-l%--(+ zIfGWp*tAv%DKWFj@#w_PoGLu^-!aiwK#xBs)aXm7{~rrp4;Y zd$;Q+s$Cs+((U#FkLw&;QfX%Hz$W?;z6&m2-!1Dgf?G$-1UwGL$*tg*_&n3AS$^y( z4#JYPjf=_RB_0G-34YNQs%u2&Bms8WtN%$kOia2EQWMG5j8rF#ipVdxZ76>u+&$lp z3uT50$hDGPOiC=3#kT=H0?b7^=J2fYzewf0U(Hm+{`8X`2>1Ec`298wr!7OzU5 z4+!D`v#t4iP`m-uVEn~YH~uEHC3danM{Y?zay?tghpZp6=94I1^bUvhQF_U!sJ;ed zQQ+KB)L>wu+Ru&u3iC{I^G+klAf81Qt}I$D+`IiI0fq;8x4Pml(Li)>rddu)d5U z@0W5^Pw1K+8#?&RqursBy#XMG4F3cz3h}&`UMFQ&q(`&9+q>zuXUuV`U~!>KwZU(v zjT%H)SeGIz2mS>WOYKDNkCXcvT23CD7g8{l5%+z<-xrXd?=kK`nGR;@d>NC((+FTR zC9;vJTorp+7Z`+Z3`a^?W0=2G*mL&MJ6NMkz|EOQjg05qT!dlk%j*N74DwnW%7;L7 zY$wH~YdcO%VsOFlocu5!EbOW4VRBB+iV2}us1$m*G_CHOxcm$q?ox6RPYp?8?>5Oc zAlhLF0xdrZ{X*}+_=5_09?gSCnhlD-?~~V@{HGc!(1r-@^&QZop}qxDcmyrDz!r*N#mPRvoBTwFf1g_?&MpBS|_p+U7kQf4Vqm(0&;EH7Q$V& zVHzMNwp3qigl-ZfSXogc&56Fq8g<~absz9DVpHCZ88v_4mIbc<@Os~r^;odr^Vryy zT04cQt{g4xg_9yew^^JBT_Jf55BB^L3t4C*W7fY$Ia?TN%$Wzy z)g8Arjn73y$ZS0x<1r@+^LJ_%Z>VS&tjWVxifi|NT+0ggHzx15ce)JRB`_US4kHsw zIjt8qH5v+YH=6Wu4WDlJaVfrO*0=tG82N-oVosAxXg~p` zK6F!?XPFahnJSLTk+e~L zcRRTFK2K<|(_%Ui@5%M4`fwB2mgO4r2TzJTopN=s@W-$T6=Bc71lBuExQ5NLAW_bs ztD4(SA*=W;PZVEgxGBbH46A!8GqmW-l|)f!64}kl6*WlBPbPDXlKu3io9Z_B11E3j zFKPKuCn6Q{6QSb#ch%_&SX{@As0Z}YBJ^c$I=OUphm(ncw^)*)aiL##$BhX=c=X*{ z`Le6@NQ`({*4*_D+nSd9`)08?NeZ;K)#)i$_gX7<&_=z!uRa`9IG>t>DePxVmniID zNZ4$k?wM<1rAwxskm2Sm9Gz5A?;xBXfY!4Y?Z#uHy5S|=EsXw3yPYp8tJ|z z+mzxY8q`a4?hV%6(dd0z+*zd1rgC+|AgAhwP8w1P!oZT2xijMZK#=;?E70o(FHHQY zukjZVFk6s(y@&~SZ3))kj%OZrBs(^Uxh!mlgNwo_D|>?^ViVW^0)#9~2GWMIuTONp ztzQA!?l4C(A7&u6MVeje1C+KYwd%NK-6_>~+t3Os&2z{-^cz7ns8i~LX2ZBdjCJj*e?3;woFND;RBe!_qT=0G=z094$f&i!e6Sf`M&l)bb$#k#4^81xcgN+Qx?i3PBJB}Hx zsC`>Gxx3g;uzwOIVDcSM@YV{K@Bd;={otN#tD{*XQGncR?o~pSx5M%4vw4pritlc1 z&oQ5Kh)Y$I%u;hp%z8B)H<>k6eR8Aihe#ueJCsS>_{paQ#32~grw)q_z>?}4I*fdV1d|OYUMi&;yXa=qo;)-() z#yL>C6SP7voQ)yRejbo-c+s!QTCy}g`f##xqc*yGT=CK~(R82PEpCW++!qkB8Zszi zx%Y$1$|{eqr=WzNGAv!2c0tn~kx?62ULA_dJqt3==~RS190=27pi6_o#dTM==aE6z zHJPd*(B^yx~qLAB3gSCU|l3mSU`_|gjMyf$f z#U3z8*a*7an*e2-uGz(P1j_tt2zWtsp2r2zni~d=z8wg&u9y2S{1|wZ&S8qQ-_g`2 zmG8@n+TdxXd0B5MEh6^?de8zX-(uMBf^W0phDdc=8dJzVg7ZFfd6X~-)+Iou*5h5I zruLgo&aG9=oQ=(23ppM>6X9!>sq1L517mkN(`gCP@^mcL`iU9kImgnV^r|pQ z3Yv=|E^h_PLeyeF`NpqV?ivVfRuo&neWa?JU1@BXp~)h}q{?yu9q%aPINj3RX#f6s zz(Ezlm&BzP2N;>YEFYu~jWmzx?MqG4f+C*0!NEAv6jLe3u$B5+Mw-BuQb-7bZ1NJA zBC3t9t3D652Xg}yBjgB7ubEvxyFOxvPR`6C(P6X;gFXkp*nR?|(&v8Asm5pqio)WP z`evWXD3e9u3IBMVTFqmt5bK8-k;C9eRm$U%QgS@k8!0oNGxwz#D&hL$Af1n}IXH?Y zs6F|laR`@#&IG20p4AB=QMoS9D;^ zx%JoEP#j8L25_LAOlt<^0_LP)CKIlS$TG_9ZdM1D9{1NQzzl}d)#4_q`KzJ%p)iyQ z4|{c;h+0F=$=(#vIa`U(&=GM0Jnxu@@($qS-j28@cksL|r`Hsf%b?<` zr8@t1fzZ!)k(K{?8or0K0OI1E>zfN@iiXj!UbHmY^cGCAb$vn(;pS9DQFB~r=?ty| zViEkBYo=EnY`A)zWWFU$jEh2l2LUs4hJU7^n5qWa-7CG#_;TZ8A!{X(KBgcyhsR1z-Pkniv~Yro zX_KhTE7W`57jFvbj-T0GpI=$m@%Oc5(%6ra#&F?kdN;l885uzhke;)>j5;GqX+j_N zreH33yVRbmc29$PKPY=HIk*%()E@rm*ADs4+SSQ5QOzfpn;E1YuE%=EA#1D*4&NLW zl6m7VhuC?WDOXvgwf*$A7VtMfLZea}yj3x|LXmLBt9m z&6>C)De`Zu=?P*cH9V;awVU%}2SRxLSb1%_#Qh`CGOO=7gr-M_KAyz%RH<2c;{zL3 z%4wi9q1~l82fm)cWY1J0b#N-*0~oA2y%)dV7$%R$wB)aQq}CepzAqb}k{-R&w6bVN zxyI970lE?guX24 zb(?GNI=f$^UHlPtTk-o9pSL+E?!Q6JGVA{a6W>B_43 ztLM3zoevN`X&1ee3pz`iH_)gW!?_Xr1`J7u%VrUc)A&)HP>+oF?rbqq^p+y?52Sy%uWPd1ze1P+Y_C=q?167K0E?1#ek!zxe3G z9)R7+JE#JE>DZ?ph#60eX6jE#_$55-&bC*dY5CC(e!%k@_7wO!&F2jRSudWi1(|sB zfMKF#yG)(5EADSFfbDf{DMD-Sj^ql@(ehyCQle;~_l)EF;UZ#D$fYN-wkkY}CPpNRm)<_b9jkES?y0u` z4~(2nH(!eo`VkwOTu)6OJ?xUg_hIm<2e?&MD4YT;;?NS3?EBQZq^eD}TEawxsf_>U zwp+2{kRiMnXG^w`-gp>nAv+QD=o-m<&}Q zDoBIpeE%K%%(i_!3cVkX50(UhC!X?gZx(WD;MS#2wuUIMV;V>oW;#8Bc{Qt#q$}!9 zF{G`O?HU8X)FkD_#nmTn7?&`*%*PKiN>VeZV$)8gr*u$qzR+B)ETsYntdpTEdP}RL zX~=n>EEU&vhrrk1B~{xS5`+XNiN|oL6FQ;wd5a6NnR06)Z4Zi`RMkfgIyQG>zg>{> ziH%yUP=O8awWOw?}i#s#CN@Sb2}2;6ea9ILNK(dsVJ#C8@od(!%9+9 z(%^fBCX!8CTvewS_Zq?V+z}U19q{=0CQT{f@a&}&!byV!oUVJ%muj6E@dh}e3mbWFS#Rx5#SWHLTa|a*>`hmW2J|^_1t$wL<`^I$!Dm|StU+Xe57t0G};zmP9#xTmsEWeESBM)&&?pzs7 zx&&cTBVyCl?~+o@sP*e|LP}jDl)ZC9MZfgS_JCvCDA${wgL+FrBQCoXMUZX$tJ>1% z327;GqE}Od-OpgZ0j~+Nf9}{O5U%Kc+|IJORhm=R0DOZEYnf{P+TkTPRbTZWmW1^4 z)-j#0Z`V&nr+`mZi^OPFa$;g1?@}OsI368pOxzpW#JSYOEZQU?eU}O3>q+rY2E2?! zUgfy#0UsSaTS&r!@y?v6xgW(^v$EDDZXbaeSE*S1G-3I&6@m%-4vL2`T_*b~?lqS1 zI+J)gic)D=)s$p6WyLI_U0EqRo?1~Fga>vMBo>2LX2Dw=;GeoZa(+UMHk9_}@i}>G zziMg*Ez+Ob-YSx{?5|Nhqk=ES#x-29Ln5Z zYj5OlUucA&+rFX@Rmr@4)e|PLwff1TF@??Pv65%k#;8eo9uBiX;crx__q9u!MPR|s z(ppm?YDIvzHk`YCA!%unpm?b~H`)nHTeJ91jJZq_oVwuB@9hI(o%Pd)Z1?uwIDQWH zW1MeVLs+3n#4)Jn&Gk>CK+cqlubrgD7)RWFLxTxsXfoQAd>#<#!X^}U2xRBtzk*$4 z>+2s>f_)NB42tHW)vYG7lFtimg_iccnFmZR-qzsm0`hP3)ie7#7F|>?reoD(+WEvD z7&<=SR7!{p(b28Kt9;mpYu;zgznmRTwHhbIYB+k-YpOG+s zqU>eZb0^o_;Nsda0GzVJ^HUpSiRE;>NBXj}Q5|D?mhx~0xujTRq*p0_=R7LGHdVGp zs|>wF9Fi|qeUiD@Gp0hZ#}wHzS_mA0oLiNnF=0g=v!;Q)!B#=qY^{SM zFHQTd;tLlK1FbDw%84+BEniv93FS9H(%XagR{&INdWF*&^-xJda`!zHRN-IsSOngHNdbqFG>Jse#1K(nwNy zJ=wyE-UMlwMbb=nG7?cx#-(^Ebw6E%}Q$SZRQk_md=bPXb&8 z5O}>ASo!j3(>`I!>`khqY2qbyI;d`sZ=i+X^2Mwe6)~zJtuzS{wu(Vv`z+Q*LN9p~ zs6Va~oUiiI#!fwH4q-F1;7RPnLh%Qh4Z`fIn`&q{s%C%|mv^)GsK3^G9P-a)Kl&XY%|Ly9I zp&w*<;>f<8P2dBpP){fTWtv3x@l z@;kliYg;9e{eK><1<_5 zz^O&Bz})g$CW{rgEQQ`z*X?Dfu2B1YwttDmYIhSWtAP2*l+D*2j5tR^JMmQ1gAus!_h zr+*=3-KHfNSKA@9ifvM^VQr;rAof^1oqwuYQ_4vTNRp$Ysbs%Dw)BMs(O@@{zOuJ) zXhq=~Rk0x(d#;o~(MVuPbwPb8v|D)iXd*3$8SPfNI3wB5Q3KP!H=QTX%>i8L?**o~ zS7OC!9P~UZi+QyMZ&R7Lfg>_ji)v&GZ|k>KMRCkQ~2eDw*zce`Dv!RjKfb=T$ay6p+h9W zEKwn|y|F*(FfneIJSlEFkThLV4j(978H4NdMKI#pNV84s# zbz)aJ<%+$m+8!9^jt%a5)?nmBtJRwkWg6jZ42d=3ohAs&5`RU4Mb6=!bQ#(v_+o!r z-(RB|$Ejl=E4@xw`L7U@)q6LkS%N#m(<7)@&>sXU-EZgkpxbcjky=ZhFLXdHSBtg9 zJi-)Fq$2iIW3eA(%if}4iFYSE;+S6#zQ0Zq6cG>vwKTR~2h9w2EW%2?L>8y=!1Bn` zD~P@%K`OC8_R<`(_lq=>#d+VPJ%>veh0IWEVgXGF8U(c$iQhVA!!{h}bF~3?-?a`_ zocqGDP{w)~G7FuOlT5`der-^;dP@5#4VSr6V4W$$$;}SIrXBk2(dwliWZ}3lO#! z;OimQM+>0-(8ZD8u~!QSC}fxN`VO@+A}3QCrfk^KA zS^6o(P;61_NX5@AE>o4P2hP&-PsASbTN4CZ#VM>Hz(3C>NZqQl!CBOZVb7zBPZC7w z@C^r)eaKFyy#~2^nypB598ky$A8u)JrgzFKVP*&};^*K%4*OD`jSJ>GAL$~m&uSsW z#>W9oRtVj(^f?*@a`eE}rd*)MC#@l}GsyilVA&+i37^!qVeCZI<`Zz&u1w~ozyD{U zHD;z!>zfwIwB&x?9wyE2c)kzOlD6fb&}+^!K@B`D?c3;|okbK@h;1J|^_Y-t+ddVk z>&w?vC|(d_r%;)K-}ZYQA#jebb$EpRfSHM7*3cRJJ{7ulH_RhvzuMosPlq3IM-vbo z@dl4)>3}yy5_Q{d)Snp1>!H02+oFjS$_Tr!ta9KVdm?0L4rH)bhHm*C$(|7se&G?< z(wz8ylV3-?a|Sx|`%8GDKxV8Lf3t|#+oNE9I@B@wU0q_v+)T%gnIwA?0q72)|6xjD z=Y<}CcLnQPz5GG9@fX^+h4QR_|fP=`#v2%g+BWFY;HVDpBIXi# z-i1$yKJkbvyt&^eI8*_%3y3g--DWw4kJBvQijv8C$fQD8f&@uCIf_C>lG{a!GN1%ykmO zuW)!K1uVSKr?)ogEu(E})NUDhv1uR83PtiVmQMFZ4V4V~aqLJg{pJ%rGAVBPJBu<7 zPJ9UfpNj(4jzVvWL0ohUN~rUC*kWvn^QskF5F%ZWTfacmsy6$}eXLu% z%9CVOQKeyHaML>9;9zGW{iqX$IH$m{X&z9`^A!%IVqp((yraUvuSHcAo&Nm4B4p_WkMPE7YWBipNX(;g!~wvJ0kw z(GiE*3bS{#ng^aO16caoEnNFI!w_?*9sT1_h!DJs)ssmQ)XNn#{ClBp^c7q6rNb=S zL7j-9Q-g&$Hhhk(?4<;%0n=b`#$XtPp(yMCTsec;Qn6vxz-!XpQTAhkT;9zc!*ES% z1XeYbhahUyQ5muHSQ_e;kC#%$5#<|AlTSN#$r#&yKS1KmBu^8@A7`qAN5#HXZ-=t*-(5hT{fmLF$&ZSxO&EduE&OQIJDox zkwEy$+|EU3IUQ&Gp?AG^%QepFCVyP#YXZqLk=4C|0R;6I5QE|u;s7Ec#erUgR5Y3h1J9O*OP7q{Sfiew2xKwt)8)iRD zg+byj0S=m45*Chta)=O^b`WQ3H6Q?@CQw^NCG^(hq_ugDii%e5>sJain1v=%aMzTY(GtYtV#guvyU)8zaa)6sk=weIyT~Mdx$dIW?!qQ zI-8Mcqvz$%-}Xlh5O%XM09&+qwdNz$PUp{#y;ai{4}IgwETbnJpKXCVGdZ;YP0gHm z_!|0|fIc{}ib0z0MBpPtBzmU)atJV-YqiZAy`T!XJP?2P{a})f4!RI&^OQ^CHUDx zZFU|%2XS2&_oJdJbE?C$!WxfgsvhtSXQYHT#5%V`*4k%vBC0c%qu%KFc(sWi;>U{Q zB`0xLi@ET$pONVWKAB=Mow~qHvSu8CweC2S`X`hM-WM~L(u~1S3>3x1%dIMXM!cme z%+=Xk3^T>6Ic}+P{MtQ}1B~OYVxiZ_1U;ce*fRf=evX#-9KDsRe|?JKc<>2DD8Q~m z*AWwrMpJLK%)BKcg>w`xmn8fa*}a6On4J$9FWr~_WvLFm%!!xAB=Z-DigpkU3)wn` zoU>Hu63Fo}ZLl8t9)^2viT`L1`6szVweERS!JhP9i^Nz0g? zF=Zc9BHKB?ZI_y}$RF+@EN=!upL`<~hgHO7(L`Pr2TRvwTV*h@ibur1x??k05}lMF zf^<|(F9+1&BregT6rNsuNW|C#T{AeQ+0P}099dY?>#?WyI95{n+ELD?E^jer{eiE4 z7iTQVkl$K*fZ>y{|5J~~t!YD`vSCg5!^cgig<=V^Pe}(}M9u5N3b?oft*IA!7C+kz zrkmt;2HAs829ih%;;H-?EBkQ}JlfeVwl3PO?6cOu&=HOik+xUl2GIK0<0UAS=*!{E z)u3+}rJ1u@rm}91biI~xEAcFHEu1}mr>(!^BVD0R6pM%+p zx7iXT>@;K~5(qA#{T&%ofBbsgCnf9nX;7(8YDp8~ATKKI7o#X3m4-;y$fk_tHJdY-xW~n)W+P+(20HOPC=>|S(Or&FxNdxT8Q5wE zqa2aHw&hwj{4#^*T<43}Dw~xr@$|tSOp;IMBvW@yZkn8#F04-17pA>>iy$tk?z;7w z9f(O{BhXC4DOSux34{V!2ZmBFIH?MmT%wIkxS1I7e|tZM4gRLa6uio&)^ z&J4~r(tvEu|C&GeNA{tjqNov-sEn++u%@Ssl9(flA(c5hyPc&Sn+qcYi@1xSGm`?F zlDe6rkg{_dCi>)&tCzJl2 zeovnK&y>yo=QB9vSQOl4?byW46;$0_6fH#TnS>>61ppbG|EY0R6GsbE3lm3iKt`#A zt-T9iH}b*&rAnI1NXbiiIx#VON{NUnD8A=?{{2~h*o#eB+Q>@IRh>~o$$;HNLX^=| zTv^f8R7A*CQ&v!&&cc*MR-Vn(Ma} jj%L57o6`N&y1d-~