WinZipAesEngine.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. <?php
  2. namespace PhpZip\Crypto;
  3. use PhpZip\Exception\RuntimeException;
  4. use PhpZip\Exception\ZipAuthenticationException;
  5. use PhpZip\Exception\ZipCryptoException;
  6. use PhpZip\Exception\ZipException;
  7. use PhpZip\Extra\Fields\WinZipAesEntryExtraField;
  8. use PhpZip\Model\ZipEntry;
  9. use PhpZip\Util\CryptoUtil;
  10. /**
  11. * WinZip Aes Encryption Engine.
  12. *
  13. * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification
  14. * @author Ne-Lexa alexey@nelexa.ru
  15. * @license MIT
  16. */
  17. class WinZipAesEngine implements ZipEncryptionEngine
  18. {
  19. /**
  20. * The block size of the Advanced Encryption Specification (AES) Algorithm
  21. * in bits (AES_BLOCK_SIZE_BITS).
  22. */
  23. const AES_BLOCK_SIZE_BITS = 128;
  24. const PWD_VERIFIER_BITS = 16;
  25. /**
  26. * The iteration count for the derived keys of the cipher, KLAC and MAC.
  27. */
  28. const ITERATION_COUNT = 1000;
  29. /**
  30. * @var ZipEntry
  31. */
  32. private $entry;
  33. /**
  34. * WinZipAesEngine constructor.
  35. * @param ZipEntry $entry
  36. */
  37. public function __construct(ZipEntry $entry)
  38. {
  39. $this->entry = $entry;
  40. }
  41. /**
  42. * Decrypt from stream resource.
  43. *
  44. * @param string $content Input stream buffer
  45. * @return string
  46. * @throws ZipAuthenticationException
  47. * @throws ZipCryptoException
  48. * @throws \PhpZip\Exception\ZipException
  49. */
  50. public function decrypt($content)
  51. {
  52. $extraFieldsCollection = $this->entry->getExtraFieldsCollection();
  53. if (!isset($extraFieldsCollection[WinZipAesEntryExtraField::getHeaderId()])) {
  54. throw new ZipCryptoException($this->entry->getName() . " (missing extra field for WinZip AES entry)");
  55. }
  56. /**
  57. * @var WinZipAesEntryExtraField $field
  58. */
  59. $field = $extraFieldsCollection[WinZipAesEntryExtraField::getHeaderId()];
  60. // Get key strength.
  61. $keyStrengthBits = $field->getKeyStrength();
  62. $keyStrengthBytes = $keyStrengthBits / 8;
  63. $pos = $keyStrengthBytes / 2;
  64. $salt = substr($content, 0, $pos);
  65. $passwordVerifier = substr($content, $pos, self::PWD_VERIFIER_BITS / 8);
  66. $pos += self::PWD_VERIFIER_BITS / 8;
  67. $sha1Size = 20;
  68. // Init start, end and size of encrypted data.
  69. $start = $pos;
  70. $endPos = strlen($content);
  71. $footerSize = $sha1Size / 2;
  72. $end = $endPos - $footerSize;
  73. $size = $end - $start;
  74. if (0 > $size) {
  75. throw new ZipCryptoException($this->entry->getName() . " (false positive WinZip AES entry is too short)");
  76. }
  77. // Load authentication code.
  78. $authenticationCode = substr($content, $end, $footerSize);
  79. if ($end + $footerSize !== $endPos) {
  80. // This should never happen unless someone is writing to the
  81. // end of the file concurrently!
  82. throw new ZipCryptoException("Expected end of file after WinZip AES authentication code!");
  83. }
  84. $password = $this->entry->getPassword();
  85. assert($password !== null);
  86. assert(self::AES_BLOCK_SIZE_BITS <= $keyStrengthBits);
  87. // WinZip 99-character limit
  88. // @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
  89. $password = substr($password, 0, 99);
  90. $ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
  91. $iv = str_repeat(chr(0), $ctrIvSize);
  92. do {
  93. // Here comes the strange part about WinZip AES encryption:
  94. // Its unorthodox use of the Password-Based Key Derivation
  95. // Function 2 (PBKDF2) of PKCS #5 V2.0 alias RFC 2898.
  96. // Yes, the password verifier is only a 16 bit value.
  97. // So we must use the MAC for password verification, too.
  98. $keyParam = hash_pbkdf2(
  99. "sha1",
  100. $password,
  101. $salt,
  102. self::ITERATION_COUNT,
  103. (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8,
  104. true
  105. );
  106. $key = substr($keyParam, 0, $keyStrengthBytes);
  107. $sha1MacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes);
  108. // Verify password.
  109. } while (!$passwordVerifier === substr($keyParam, 2 * $keyStrengthBytes));
  110. $content = substr($content, $start, $size);
  111. $mac = hash_hmac('sha1', $content, $sha1MacParam, true);
  112. if (substr($mac, 0, 10) !== $authenticationCode) {
  113. throw new ZipAuthenticationException($this->entry->getName() .
  114. " (authenticated WinZip AES entry content has been tampered with)");
  115. }
  116. return self::aesCtrSegmentIntegerCounter($content, $key, $iv, false);
  117. }
  118. /**
  119. * Decryption or encryption AES-CTR with Segment Integer Count (SIC).
  120. *
  121. * @param string $str Data
  122. * @param string $key Key
  123. * @param string $iv IV
  124. * @param bool $encrypted If true encryption else decryption
  125. * @return string
  126. */
  127. private static function aesCtrSegmentIntegerCounter($str, $key, $iv, $encrypted = true)
  128. {
  129. $numOfBlocks = ceil(strlen($str) / 16);
  130. $ctrStr = '';
  131. for ($i = 0; $i < $numOfBlocks; ++$i) {
  132. for ($j = 0; $j < 16; ++$j) {
  133. $n = ord($iv[$j]);
  134. if (++$n === 0x100) {
  135. // overflow, set this one to 0, increment next
  136. $iv[$j] = chr(0);
  137. } else {
  138. // no overflow, just write incremented number back and abort
  139. $iv[$j] = chr($n);
  140. break;
  141. }
  142. }
  143. $data = substr($str, $i * 16, 16);
  144. $ctrStr .= $encrypted ?
  145. self::encryptCtr($data, $key, $iv) :
  146. self::decryptCtr($data, $key, $iv);
  147. }
  148. return $ctrStr;
  149. }
  150. /**
  151. * Encrypt AES-CTR.
  152. *
  153. * @param string $data Raw data
  154. * @param string $key Aes key
  155. * @param string $iv Aes IV
  156. * @return string Encrypted data
  157. */
  158. private static function encryptCtr($data, $key, $iv)
  159. {
  160. if (extension_loaded("openssl")) {
  161. $numBits = strlen($key) * 8;
  162. /** @noinspection PhpComposerExtensionStubsInspection */
  163. return openssl_encrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv);
  164. } elseif (extension_loaded("mcrypt")) {
  165. /** @noinspection PhpDeprecationInspection */
  166. /** @noinspection PhpComposerExtensionStubsInspection */
  167. return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
  168. } else {
  169. throw new RuntimeException('Extension openssl or mcrypt not loaded');
  170. }
  171. }
  172. /**
  173. * Decrypt AES-CTR.
  174. *
  175. * @param string $data Encrypted data
  176. * @param string $key Aes key
  177. * @param string $iv Aes IV
  178. * @return string Raw data
  179. */
  180. private static function decryptCtr($data, $key, $iv)
  181. {
  182. if (extension_loaded("openssl")) {
  183. $numBits = strlen($key) * 8;
  184. /** @noinspection PhpComposerExtensionStubsInspection */
  185. return openssl_decrypt($data, 'AES-' . $numBits . '-CTR', $key, OPENSSL_RAW_DATA, $iv);
  186. } elseif (extension_loaded("mcrypt")) {
  187. /** @noinspection PhpDeprecationInspection */
  188. /** @noinspection PhpComposerExtensionStubsInspection */
  189. return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, "ctr", $iv);
  190. } else {
  191. throw new RuntimeException('Extension openssl or mcrypt not loaded');
  192. }
  193. }
  194. /**
  195. * Encryption string.
  196. *
  197. * @param string $content
  198. * @return string
  199. * @throws \PhpZip\Exception\ZipException
  200. */
  201. public function encrypt($content)
  202. {
  203. // Init key strength.
  204. $password = $this->entry->getPassword();
  205. if ($password === null) {
  206. throw new ZipException('No password was set for the entry "'.$this->entry->getName().'"');
  207. }
  208. // WinZip 99-character limit
  209. // @see https://sourceforge.net/p/p7zip/discussion/383044/thread/c859a2f0/
  210. $password = substr($password, 0, 99);
  211. $keyStrengthBits = WinZipAesEntryExtraField::getKeyStrangeFromEncryptionMethod($this->entry->getEncryptionMethod());
  212. $keyStrengthBytes = $keyStrengthBits / 8;
  213. $salt = CryptoUtil::randomBytes($keyStrengthBytes / 2);
  214. $keyParam = hash_pbkdf2("sha1", $password, $salt, self::ITERATION_COUNT, (2 * $keyStrengthBits + self::PWD_VERIFIER_BITS) / 8, true);
  215. $sha1HMacParam = substr($keyParam, $keyStrengthBytes, $keyStrengthBytes);
  216. // Can you believe they "forgot" the nonce in the CTR mode IV?! :-(
  217. $ctrIvSize = self::AES_BLOCK_SIZE_BITS / 8;
  218. $iv = str_repeat(chr(0), $ctrIvSize);
  219. $key = substr($keyParam, 0, $keyStrengthBytes);
  220. $content = self::aesCtrSegmentIntegerCounter($content, $key, $iv, true);
  221. $mac = hash_hmac('sha1', $content, $sha1HMacParam, true);
  222. return ($salt .
  223. substr($keyParam, 2 * $keyStrengthBytes, self::PWD_VERIFIER_BITS / 8) .
  224. $content .
  225. substr($mac, 0, 10)
  226. );
  227. }
  228. }