WinZipAesEngine.php 9.3 KB

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