WinZipAesExtraField.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. <?php
  2. namespace PhpZip\Model\Extra\Fields;
  3. use PhpZip\Constants\ZipCompressionMethod;
  4. use PhpZip\Constants\ZipEncryptionMethod;
  5. use PhpZip\Exception\InvalidArgumentException;
  6. use PhpZip\Exception\ZipException;
  7. use PhpZip\Exception\ZipUnsupportMethodException;
  8. use PhpZip\Model\Extra\ZipExtraField;
  9. use PhpZip\Model\ZipEntry;
  10. /**
  11. * WinZip AES Extra Field.
  12. *
  13. * @see http://www.winzip.com/win/en/aes_tips.htm AES Coding Tips for Developers
  14. */
  15. class WinZipAesExtraField implements ZipExtraField
  16. {
  17. /** @var int Header id */
  18. const HEADER_ID = 0x9901;
  19. /**
  20. * @var int Data size (currently 7, but subject to possible increase
  21. * in the future)
  22. */
  23. const DATA_SIZE = 7;
  24. /**
  25. * @var int The vendor ID field should always be set to the two ASCII
  26. * characters "AE"
  27. */
  28. const VENDOR_ID = 0x4541; // 'A' | ('E' << 8)
  29. /**
  30. * @var int Entries of this type do include the standard ZIP CRC-32 value.
  31. * For use with {@see WinZipAesExtraField::setVendorVersion()}.
  32. */
  33. const VERSION_AE1 = 1;
  34. /**
  35. * @var int Entries of this type do not include the standard ZIP CRC-32 value.
  36. * For use with {@see WinZipAesExtraField::setVendorVersion().
  37. */
  38. const VERSION_AE2 = 2;
  39. /** @var int integer mode value indicating AES encryption 128-bit strength */
  40. const KEY_STRENGTH_128BIT = 0x01;
  41. /** @var int integer mode value indicating AES encryption 192-bit strength */
  42. const KEY_STRENGTH_192BIT = 0x02;
  43. /** @var int integer mode value indicating AES encryption 256-bit strength */
  44. const KEY_STRENGTH_256BIT = 0x03;
  45. /** @var int[] */
  46. private static $allowVendorVersions = [
  47. self::VERSION_AE1,
  48. self::VERSION_AE2,
  49. ];
  50. /** @var array<int, int> */
  51. private static $encryptionStrengths = [
  52. self::KEY_STRENGTH_128BIT => 128,
  53. self::KEY_STRENGTH_192BIT => 192,
  54. self::KEY_STRENGTH_256BIT => 256,
  55. ];
  56. /** @var array<int, int> */
  57. private static $MAP_KEY_STRENGTH_METHODS = [
  58. self::KEY_STRENGTH_128BIT => ZipEncryptionMethod::WINZIP_AES_128,
  59. self::KEY_STRENGTH_192BIT => ZipEncryptionMethod::WINZIP_AES_192,
  60. self::KEY_STRENGTH_256BIT => ZipEncryptionMethod::WINZIP_AES_256,
  61. ];
  62. /** @var int Integer version number specific to the zip vendor */
  63. private $vendorVersion = self::VERSION_AE1;
  64. /** @var int Integer mode value indicating AES encryption strength */
  65. private $keyStrength = self::KEY_STRENGTH_256BIT;
  66. /** @var int The actual compression method used to compress the file */
  67. private $compressionMethod;
  68. /**
  69. * @param int $vendorVersion Integer version number specific to the zip vendor
  70. * @param int $keyStrength Integer mode value indicating AES encryption strength
  71. * @param int $compressionMethod The actual compression method used to compress the file
  72. *
  73. * @throws ZipUnsupportMethodException
  74. */
  75. public function __construct($vendorVersion, $keyStrength, $compressionMethod)
  76. {
  77. $this->setVendorVersion($vendorVersion);
  78. $this->setKeyStrength($keyStrength);
  79. $this->setCompressionMethod($compressionMethod);
  80. }
  81. /**
  82. * @return string
  83. */
  84. public function __toString()
  85. {
  86. return sprintf(
  87. '0x%04x WINZIP AES: VendorVersion=%d KeyStrength=0x%02x CompressionMethod=%s',
  88. __CLASS__,
  89. $this->vendorVersion,
  90. $this->keyStrength,
  91. $this->compressionMethod
  92. );
  93. }
  94. /**
  95. * @param ZipEntry $entry
  96. *
  97. * @throws ZipUnsupportMethodException
  98. *
  99. * @return WinZipAesExtraField
  100. */
  101. public static function create(ZipEntry $entry)
  102. {
  103. $keyStrength = array_search($entry->getEncryptionMethod(), self::$MAP_KEY_STRENGTH_METHODS, true);
  104. if ($keyStrength === false) {
  105. throw new InvalidArgumentException('Not support encryption method ' . $entry->getEncryptionMethod());
  106. }
  107. // WinZip 11 will continue to use AE-2, with no CRC, for very small files
  108. // of less than 20 bytes. It will also use AE-2 for files compressed in
  109. // BZIP2 format, because this format has internal integrity checks
  110. // equivalent to a CRC check built in.
  111. //
  112. // https://www.winzip.com/win/en/aes_info.html
  113. $vendorVersion = (
  114. $entry->getUncompressedSize() < 20
  115. || $entry->getCompressionMethod() === ZipCompressionMethod::BZIP2
  116. )
  117. ? self::VERSION_AE2
  118. : self::VERSION_AE1;
  119. $field = new self($vendorVersion, $keyStrength, $entry->getCompressionMethod());
  120. $entry->getLocalExtraFields()->add($field);
  121. $entry->getCdExtraFields()->add($field);
  122. return $field;
  123. }
  124. /**
  125. * Returns the Header ID (type) of this Extra Field.
  126. * The Header ID is an unsigned short integer (two bytes)
  127. * which must be constant during the life cycle of this object.
  128. *
  129. * @return int
  130. */
  131. public function getHeaderId()
  132. {
  133. return self::HEADER_ID;
  134. }
  135. /**
  136. * Populate data from this array as if it was in local file data.
  137. *
  138. * @param string $buffer the buffer to read data from
  139. * @param ZipEntry|null $entry
  140. *
  141. * @throws ZipException on error
  142. *
  143. * @return WinZipAesExtraField
  144. */
  145. public static function unpackLocalFileData($buffer, ZipEntry $entry = null)
  146. {
  147. $size = \strlen($buffer);
  148. if ($size !== self::DATA_SIZE) {
  149. throw new ZipException(
  150. sprintf(
  151. 'WinZip AES Extra data invalid size: %d. Must be %d',
  152. $size,
  153. self::DATA_SIZE
  154. )
  155. );
  156. }
  157. $data = unpack('vvendorVersion/vvendorId/ckeyStrength/vcompressionMethod', $buffer);
  158. if ($data['vendorId'] !== self::VENDOR_ID) {
  159. throw new ZipException(
  160. sprintf(
  161. 'Vendor id invalid: %d. Must be %d',
  162. $data['vendorId'],
  163. self::VENDOR_ID
  164. )
  165. );
  166. }
  167. return new self(
  168. $data['vendorVersion'],
  169. $data['keyStrength'],
  170. $data['compressionMethod']
  171. );
  172. }
  173. /**
  174. * Populate data from this array as if it was in central directory data.
  175. *
  176. * @param string $buffer the buffer to read data from
  177. * @param ZipEntry|null $entry
  178. *
  179. * @throws ZipException
  180. *
  181. * @return WinZipAesExtraField
  182. */
  183. public static function unpackCentralDirData($buffer, ZipEntry $entry = null)
  184. {
  185. return self::unpackLocalFileData($buffer, $entry);
  186. }
  187. /**
  188. * The actual data to put into local file data - without Header-ID
  189. * or length specifier.
  190. *
  191. * @return string the data
  192. */
  193. public function packLocalFileData()
  194. {
  195. return pack(
  196. 'vvcv',
  197. $this->vendorVersion,
  198. self::VENDOR_ID,
  199. $this->keyStrength,
  200. $this->compressionMethod
  201. );
  202. }
  203. /**
  204. * The actual data to put into central directory - without Header-ID or
  205. * length specifier.
  206. *
  207. * @return string the data
  208. */
  209. public function packCentralDirData()
  210. {
  211. return $this->packLocalFileData();
  212. }
  213. /**
  214. * Returns the vendor version.
  215. *
  216. * @return int
  217. *
  218. * @see WinZipAesExtraField::VERSION_AE2
  219. * @see WinZipAesExtraField::VERSION_AE1
  220. */
  221. public function getVendorVersion()
  222. {
  223. return $this->vendorVersion;
  224. }
  225. /**
  226. * Sets the vendor version.
  227. *
  228. * @param int $vendorVersion the vendor version
  229. *
  230. * @see WinZipAesExtraField::VERSION_AE2
  231. * @see WinZipAesExtraField::VERSION_AE1
  232. */
  233. public function setVendorVersion($vendorVersion)
  234. {
  235. $vendorVersion = (int) $vendorVersion;
  236. if (!\in_array($vendorVersion, self::$allowVendorVersions, true)) {
  237. throw new InvalidArgumentException(
  238. sprintf(
  239. 'Unsupport WinZip AES vendor version: %d',
  240. $vendorVersion
  241. )
  242. );
  243. }
  244. $this->vendorVersion = $vendorVersion;
  245. }
  246. /**
  247. * Returns vendor id.
  248. *
  249. * @return int
  250. */
  251. public function getVendorId()
  252. {
  253. return self::VENDOR_ID;
  254. }
  255. /**
  256. * @return int
  257. */
  258. public function getKeyStrength()
  259. {
  260. return $this->keyStrength;
  261. }
  262. /**
  263. * Set key strength.
  264. *
  265. * @param int $keyStrength
  266. */
  267. public function setKeyStrength($keyStrength)
  268. {
  269. $keyStrength = (int) $keyStrength;
  270. if (!isset(self::$encryptionStrengths[$keyStrength])) {
  271. throw new InvalidArgumentException(
  272. sprintf(
  273. 'Key strength %d not support value. Allow values: %s',
  274. $keyStrength,
  275. implode(', ', array_keys(self::$encryptionStrengths))
  276. )
  277. );
  278. }
  279. $this->keyStrength = $keyStrength;
  280. }
  281. /**
  282. * @return int
  283. */
  284. public function getCompressionMethod()
  285. {
  286. return $this->compressionMethod;
  287. }
  288. /**
  289. * @param int $compressionMethod
  290. *
  291. * @throws ZipUnsupportMethodException
  292. */
  293. public function setCompressionMethod($compressionMethod)
  294. {
  295. $compressionMethod = (int) $compressionMethod;
  296. ZipCompressionMethod::checkSupport($compressionMethod);
  297. $this->compressionMethod = $compressionMethod;
  298. }
  299. /**
  300. * @return int
  301. */
  302. public function getEncryptionStrength()
  303. {
  304. return self::$encryptionStrengths[$this->keyStrength];
  305. }
  306. /**
  307. * @return int
  308. */
  309. public function getEncryptionMethod()
  310. {
  311. $keyStrength = $this->getKeyStrength();
  312. if (!isset(self::$MAP_KEY_STRENGTH_METHODS[$keyStrength])) {
  313. throw new InvalidArgumentException('Invalid encryption method');
  314. }
  315. return self::$MAP_KEY_STRENGTH_METHODS[$keyStrength];
  316. }
  317. /**
  318. * @return bool
  319. */
  320. public function isV1()
  321. {
  322. return $this->vendorVersion === self::VERSION_AE1;
  323. }
  324. /**
  325. * @return bool
  326. */
  327. public function isV2()
  328. {
  329. return $this->vendorVersion === self::VERSION_AE2;
  330. }
  331. /**
  332. * @return int
  333. */
  334. public function getSaltSize()
  335. {
  336. return (int) ($this->getEncryptionStrength() / 8 / 2);
  337. }
  338. }